今日、F#開発におけるSOLID原則の関連性を説明したこの記事を見ました。
そして最後の1つである「依存性反転の原理」に取り組んでいる間、著者は次のように述べています。
機能的な観点から、これらのコンテナとインジェクションの概念は、単純な高次関数、または言語に直接組み込まれている中間者型パターンで解決できます。
しかし、彼はそれをさらに説明しませんでした。だから、私の質問は、依存関係の逆転は高階関数にどのように関連していますか?
今日、F#開発におけるSOLID原則の関連性を説明したこの記事を見ました。
そして最後の1つである「依存性反転の原理」に取り組んでいる間、著者は次のように述べています。
機能的な観点から、これらのコンテナとインジェクションの概念は、単純な高次関数、または言語に直接組み込まれている中間者型パターンで解決できます。
しかし、彼はそれをさらに説明しませんでした。だから、私の質問は、依存関係の逆転は高階関数にどのように関連していますか?
回答:
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で実行できます。この例を使用して、インターフェイスを実装するオブジェクトの代わりに高階関数を使用する方が簡単かつ柔軟であることを示しました。
IFilter<Customer>
は強制ではありません。高階関数は非常に柔軟性が高く、これは大きな利点であり、インラインで記述できることも大きな利点です。ラムダはローカル変数をキャプチャするのもはるかに簡単です。
public delegate bool CustomerFilter(Customer customer)
。haskellのような純粋な関数型言語では、エイリアシングタイプは簡単です。– type customerFilter = Customer -> Bool
簡潔な答え:
古典的な依存性注入/制御の反転では、依存機能のプレースホルダーとしてクラスインターフェイスを使用します。このインターフェイスはクラスによって実装されます。
Interface / ClassImplementationの代わりに、デリゲート関数を使用すると、多くの依存関係を簡単に実装できます。
両方の例は、c#のioc-factory-pros-and-contras-for-interface-versus-delegatesにあります。
これを比較してください:
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
このような高次関数を提供することにより、最低限(つまり、注入される依存関係-ラムダ式)を渡すことができます。
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」メソッドを持つクラスの束を作成する作業になります。