依存関係の逆転は、高階関数とどのように関連していますか?


41

今日、F#開発におけるSOLID原則の関連性を説明したこの記事を見ました。

F#および設計原則–ソリッド

そして最後の1つである「依存性反転の原理」に取り組んでいる間、著者は次のように述べています。

機能的な観点から、これらのコンテナとインジェクションの概念は、単純な高次関数、または言語に直接組み込まれている中間者型パターンで解決できます。

しかし、彼はそれをさらに説明しませんでした。だから、私の質問は、依存関係の逆転は高階関数にどのように関連していますか?

回答:


38

OOPの依存性反転とは、オブジェクトに対する実装によって提供されるインターフェイスに対してコーディングすることを意味します。

上位言語関数をサポートする言語は、オブジェクト指向ではインターフェイスを実装するオブジェクトではなく関数として動作を渡すことで、単純な依存関係の逆転の問題を解決できることがよくあります。

このような言語では、関数のシグネチャがインターフェイスになり、従来のオブジェクトの代わりに関数が渡されて、目的の動作が提供されます。中央のパターンの穴は、この良い例です。

(OOP)インターフェースに準拠するクラス全体を実装して、呼び出し元に目的の動作を提供する必要がないため、より少ないコードとより表現力のある同じ結果を達成できます。代わりに、単純な関数定義を渡すことができます。要するに、高次関数を使用すると、コードの保守が容易になり、表現力と柔軟性が向上することがよくあります。

C#の例

従来のアプローチ:

public IEnumerable<Customer> FilterCustomers(IFilter<Customer> filter, IEnumerable<Customers> customers)
{
    foreach(var customer in customers)
    {
        if(filter.Matches(customer))
        {
            yield return customer;
        }
    }
}

//now you've got to implement all these filters
class CustomerNameFilter : IFilter<Customer> /*...*/
class CustomerBirthdayFilter : IFilter<Customer> /*...*/

//the invocation looks like this
var filteredDataByName = FilterCustomers(new CustomerNameFilter("SomeName"), customers);
var filteredDataBybirthDay = FilterCustomers(new CustomerBirthdayFilter(SomeDate), customers);

高階関数の場合:

public IEnumerable<Customer> FilterCustomers(Func<Customer, bool> filter, IEnumerable<Customers> customers)
{
    foreach(var customer in customers)
    {
        if(filter(customer))
        {
            yield return customer;
        }
    }
}

実装と呼び出しが面倒になりました。もうIFilter実装を提供する必要はありません。フィルターのクラスを実装する必要はもうありません。

var filteredDataByName = FilterCustomers(x => x.Name.Equals("CustomerName"), customers);
var filteredDataByBirthday = FilterCustomers(x => x.Birthday == SomeDateTime, customers);

もちろん、これはすでにC#のLinQで実行できます。この例を使用して、インターフェイスを実装するオブジェクトの代わりに高階関数を使用する方が簡単かつ柔軟であることを示しました。


3
いい例です。しかし、Gulshanのように、私は関数型プログラミングについてもっと調べようとしていますが、この種の「関数型DI」は「オブジェクト指向型DI」に比べて厳密さと重要性を犠牲にしないのではないかと考えていました。上位のシグネチャは、渡された関数がパラメーターとしてCustomerを取り、boolを返す必要があることのみを示していますが、OOバージョン、渡されたオブジェクトがフィルターであるという事実を強制します(IFilter <Customer>を実装します)。また、フィルターの概念を明示的にします。これは、ドメインの中心的な概念である場合には良いことです(DDDを参照)。どう思いますか ?
-guillaume31

2
@ ian31:これは確かに興味深いトピックです!FilterCustomerに渡されるものは、何らかのフィルターとして暗黙的に動作します。フィルターの概念がドメインの重要な部分であり、システム全体で複数回使用される複雑なフィルタールールが必要な場合は、それらをカプセル化することをお勧めします。そうでないか、非常に低い程度であれば、技術的なシンプルさと実用性を目指します。
ファルコン

5
@ ian31:私はまったく同意しません。実装IFilter<Customer>は強制ではありません。高階関数は非常に柔軟性が高く、これは大きな利点であり、インラインで記述できることも大きな利点です。ラムダはローカル変数をキャプチャするのもはるかに簡単です。
-DeadMG

3
@ ian31:関数はコンパイル時に検証することもできます。また、関数を作成して名前を付け、明白なコントラクトを完全に満たす限り(引数を取得し、boolを返す)、引数として渡すこともできます。必ずしもラムダ式を渡す必要はありません。そのため、表現力の欠如をある程度カバーできます。ただし、契約とその意図は明確に表現されていません。それは時々大きな欠点です。全体として、表現力、言語、カプセル化の問題です。各ケースを単独で判断する必要があると思います。
ファルコン

2
挿入された関数のセマンティックな意味を明確にしたい場合は、デリゲートを使用してC#で関数シグネチャに名前を付けることができますpublic delegate bool CustomerFilter(Customer customer)。haskellのような純粋な関数型言語では、エイリアシングタイプは簡単です。– type customerFilter = Customer -> Bool
sara

8

関数の動作を変更する場合

doThis(Foo)

別の関数を渡すことができます

doThisWith(Foo, anotherFunction)

異なる動作を実装します。

「doThisWith」は、別の関数を引数として取るため、高階関数です。

たとえば、あなたが持つことができます

storeValues(Foo, writeToDatabase)
storeValues(Foo, imitateDatabase)

5

簡潔な答え:

古典的な依存性注入/制御の反転では、依存機能のプレースホルダーとしてクラスインターフェイスを使用します。このインターフェイスはクラスによって実装されます。

Interface / ClassImplementationの代わりに、デリゲート関数を使用すると、多くの依存関係を簡単に実装できます。

両方の例は、c#のioc-factory-pros-and-contras-for-interface-versus-delegatesにあります。


0

これを比較してください:

String[] names = {"Fred", "Susan"};
List<String> namesBeginningWithS = new LinkedList<String>();
for (String name : names) {
    if (name.startsWith("S")) {
        namesBeginningWithS.add(name);
    }
}

で:

String[] names = {"Fred", "Susan"};
List<String> namesBeginningWithS = names.stream().filter(n <- n.startsWith("S")).collect();

2番目のバージョンは、Java 8のボイラープレートコード(ループなど)を削減する方法です。filterこのような高次関数を提供することにより、最低限(つまり、注入される依存関係-ラムダ式)を渡すことができます。


0

LennyProgrammersの例の貯金箱...

他の例で見逃したことの1つは、高次関数を部分関数アプリケーション(PFA)と一緒に使用して、依存関係を(引数リストを介して)関数にバインド(または「注入」)して新しい関数を作成できることです。

代わりに:

doThisWith(Foo, anotherFunction)

(PFAの通常のやり方では慣習的である)私たちは(引数の順序を交換する)低レベルのワーカー関数を持っています:

doThisWith( anotherFunction, Foo )

その後、doThisWithを次のように部分的に適用できます

doThis = doThisWith( anotherFunction )  // note that "Foo" is still missing, argument list is partial

これにより、後で新しい関数を次のように使用できます。

doThis(Foo)

あるいは:

doThat = doThisWith( yetAnotherDependencyFunction )
...
doThat( Bar )

参照:https : //ramdajs.com/docs/#partial

...そして、ええ、加算器/乗算器は想像を絶する例です。より良い例は、「コンシューマ」関数が依存関係として渡したものに応じて、メッセージを取得し、ログに記録するか、電子メールで送信する関数です。

この考え方を拡張し、さらに長い引数リストは、ますます短い引数リストを持つますます特殊化された関数に徐々に絞り込むことができます。もちろん、これらの関数はいずれも、部分的に適用される依存関係として他の関数に渡すことができます。

OOPは、密接に関連する複数の操作を含むバンドルが必要な場合に便利ですが、それぞれが「The Kingdom of Nouns」のような単一のパブリック「do it」メソッドを持つクラスの束を作成する作業になります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.