私は最近、C#での関数型プログラミングというタイトルの本を読んでいます。関数型プログラミングの不変でステートレスな性質は、依存性注入パターンと同様の結果を達成し、特にユニットテストに関してはより良いアプローチである可能性があります。
両方のアプローチの経験がある人なら誰でも、主な質問に答えるために自分の考えや経験を共有できれば感謝しています。
私は最近、C#での関数型プログラミングというタイトルの本を読んでいます。関数型プログラミングの不変でステートレスな性質は、依存性注入パターンと同様の結果を達成し、特にユニットテストに関してはより良いアプローチである可能性があります。
両方のアプローチの経験がある人なら誰でも、主な質問に答えるために自分の考えや経験を共有できれば感謝しています。
回答:
依存関係管理は、次の2つの理由でOOPの大きな問題です。
ほとんどのオブジェクト指向プログラマーは、データとコードの密結合は完全に有益であると考えていますが、それにはコストがかかります。レイヤーを通るデータの流れを管理することは、どのようなパラダイムでもプログラミングの避けられない部分です。データとコードを結合すると、特定のポイントで関数を使用する場合に、そのオブジェクトをそのポイントに到達させる方法を見つける必要があるという追加の問題が追加されます。
副作用を使用すると、同様の問題が生じます。一部の機能に副作用を使用しているが、その実装を交換できるようにしたい場合は、その依存関係を注入する以外に選択肢はほとんどありません。
例として、電子メールアドレスのWebページをスクレイピングしてから電子メールで送信するスパマープログラムを考えます。DIの考え方をお持ちの場合、現在、インターフェイスの背後にカプセル化するサービスと、どのサービスがどこに注入されるかを考えています。このデザインは読者の演習として残しておきます。FPの考え方をお持ちの場合、現時点では、次のような機能の最下層の入力と出力を考えています。
入力と出力の観点から考えると、関数の依存関係はなく、データの依存関係のみがあります。それがユニットテストをとても簡単にするものです。次のレイヤーは、ある関数の出力が次の関数の入力に供給されるように調整し、必要に応じてさまざまな実装を簡単に交換できます。
非常に現実的な意味で、関数型プログラミングは自然に、常に関数の依存関係を反転するように促します。そのため、通常、事後に特別な対策を講じる必要はありません。その場合、高階関数、クロージャ、部分適用などのツールを使用すると、少ない定型文で簡単に実行できます。
問題があるのは依存関係そのものではないことに注意してください。間違った方向を指しているのは依存関係です。次の層には次のような機能があります。
processText = spamToSMTP . emailAddressToSpam . removeEmailDups . textToEmailAddresses
このレイヤーがこのようにハードコードされた依存関係を持つことは完全に大丈夫です、なぜならその唯一の目的は下位レイヤーの機能を一緒に接着することだからです。実装の交換は、異なる構成を作成するのと同じくらい簡単です。
processTextFancy = spamToSMTP . emailAddressToFancySpam . removeEmailDups . textToEmailAddresses
この簡単な再構成は、副作用がないために可能になります。下位層の機能は互いに完全に独立しています。次の層でprocessText
は、ユーザー設定に基づいて実際に使用されるものを選択できます。
actuallyUsedProcessText = if (config == "Fancy") then processTextFancy else processText
繰り返しますが、すべての依存関係が一方向を指しているため、問題ではありません。すべての依存関係を同じようにするために、いくつかの依存関係を逆にする必要はありません。純粋な関数がすでにそれを強制しているからです。
config
一番上でチェックするのではなく、最下層にパススルーすることで、これをさらに結合させることができます。FPはこれを行うことを妨げるものではありませんが、試してみるとはるかに迷惑になる傾向があります。
System.String
ます。モジュールシステムではSystem.String
、文字列実装の選択がハードコーディングされないように変数で置き換えることができますが、コンパイル時に解決されます。
関数型プログラミングは依存性注入パターンの実行可能な代替手段ですか?
これは奇妙な質問です。関数型プログラミングのアプローチは、主に依存性注入の正接です。
確かに、不変の状態があると、副作用があるか、クラス状態を関数間の暗黙のコントラクトとして使用することで、「チート」しないようにプッシュできます。これにより、データの受け渡しがより明確になります。これは、依存性注入の最も基本的な形式だと思います。また、関数を渡すという関数型プログラミングの概念により、それがはるかに簡単になります。
ただし、依存関係は削除されません。状態が変更可能だったときに必要だったすべてのデータ/操作が操作に必要です。そして、それらの依存関係を何らかの形でそこに取得する必要があります。したがって、関数型プログラミングのアプローチが DIに取って代わるとは言いませんので、代替手段はありません。
どちらかといえば、プログラマーがめったに考えないよりも、オブジェクト指向コードが暗黙的な依存関係を作成する方法を示しています。
あなたの質問への簡単な答えは、いいえです。
しかし、他の人が主張しているように、この質問は2つのやや無関係な概念と結婚しています。
これを段階的にやってみましょう。
関数プログラミングの中核には純粋な関数があります。これは、入力を出力にマップする関数であるため、特定の入力に対して常に同じ出力を取得します。
DI は、通常、出力が注入によって異なるため、ユニットがもはや純粋ではないことを意味します。たとえば、次の関数では:
const bookSeats = ( seatCount, getBookedSeatCount ) => { ... }
getBookedSeatCount
(関数)は、同じ入力に対して異なる結果をもたらす可能性があります。これbookSeats
も不純になります。
これには例外があります-異なるアルゴリズムを使用していても、同じ入出力マッピングを実装する2つのソートアルゴリズムのいずれかを挿入できます。しかし、これらは例外です。
システムは純粋ではありえないという事実は、関数型プログラミングソースで主張されているため、同様に無視されます。
システムには次のような明らかな例がある副作用がなければなりません。
したがって、システムの一部には副作用が含まれる必要があり、その一部には命令型スタイルまたはオブジェクト指向スタイルも含まれる場合があります。
Gary Bernhardtの境界に関する素晴らしい講演から用語を借用すると、優れたシステム(またはモジュール)アーキテクチャには次の2つの層が含まれます。
重要なポイントは、システムを純粋な部分(コア)と不純な部分(シェル)に「分割」することです。
少し欠陥のあるソリューション(および結論)を提供していますが、このMark Seemannの記事はまったく同じ概念を提案しています。Haskellの実装は、FPを使用してすべて実行できることを示しているため、特に洞察力に富んでいます。
アプリケーションの大部分が純粋であっても、DIを使用することは完全に合理的です。重要なのは、DIを不純なシェル内に閉じ込めることです。
例としてAPIスタブがあります-本番環境では実際のAPIが必要ですが、テストではスタブを使用します。ここでは、シェルコアモデルに準拠することが非常に役立ちます。
したがって、FPとDIは正確な選択肢ではありません。システムに両方がある可能性が高いため、FPとDIがそれぞれ存在するシステムの純粋な部分と不純な部分を確実に分離することをお勧めします。
OOPの観点からは、関数は単一メソッドのインターフェイスと見なすことができます。
インターフェイスは関数よりも強力なコントラクトです。
機能的アプローチを使用しており、多くのDIを実行している場合、OOPアプローチを使用する場合と比較して、各依存関係に対してより多くの候補が得られます。
void DoStuff(Func<DateTime> getDateTime) {}; //Anything that satisfies the signature can be injected.
対
void DoStuff(IDateTimeProvider dateTimeProvider) {}; //Only types implementing the interface can be injected.