web-dev-qa-db-ja.com

関数型プログラミングは、依存性注入パターンの実行可能な代替手段ですか?

私は最近 C#の関数型プログラミング というタイトルの本を読んでいますが、関数型プログラミングの不変でステートレスな性質が依存関係注入パターンと同様の結果を達成し、特により良いアプローチであることに気付きますユニットテストに関して。

両方のアプローチの経験を持っている人が主な質問に答えるために自分の考えと経験を共有できるとありがたいです:関数型プログラミングは依存性注入パターンの実行可能な代替ですか?

23
Matt Cashatt

依存関係の管理は、OOPにおいて次の2つの理由で大きな問題です。

  • データとコードの密結合。
  • 副作用のユビキタスな使用。

ほとんどのOOプログラマーは、データとコードの密結合は完全に有益であると考えていますが、それにはコストが伴います。レイヤーを介したデータの流れの管理は、あらゆるパラダイムでのプログラミングの避けられない部分です。データとコードを結合すると、特定のポイントで関数を使用したい場合、そのオブジェクトを取得する方法を見つける必要があるという追加の問題が追加されますその点。

副作用を使用すると、同様の問題が生じます。一部の機能に副作用を使用し、その実装をスワップアウトできるようにしたい場合、その依存関係を注入する以外にほとんど選択肢はありません。

例として、電子メールアドレスのWebページを削ってから電子メールで送信するスパマープログラムを考えてみます。 DIの考え方をお持ちの場合は、現在、インターフェイスの背後にカプセル化するサービスと、どこに注入されるサービスかを考えています。そのデザインは読者のための演習として残しておきます。 FPマインドセットがある場合、現在、次のような関数の最下位層の入力と出力について考えています。

  • Webページのアドレスを入力し、そのページのテキストを出力します。
  • ページのテキストを入力し、そのページからのリンクのリストを出力します。
  • ページのテキストを入力し、そのページのメールアドレスのリストを出力します。
  • メールアドレスのリストを入力し、重複が削除されたメールアドレスのリストを出力します。
  • メールアドレスを入力し、そのアドレスのスパムメールを出力します。
  • スパムメールを入力し、SMTPコマンドを出力してそのメールを送信します。

入力と出力の観点から考えると、関数の依存関係はなく、データの依存関係しかありません。これにより、ユニットテストが非常に簡単になります。次のレイヤーは、ある関数の出力が次の入力に供給されるように調整し、必要に応じてさまざまな実装を簡単に交換できます。

非常に現実的な意味では、関数型プログラミングは当然、関数の依存関係を常に逆にするように促します。したがって、通常、事後的にそうするために特別な対策を講じる必要はありません。その場合、高次関数、クロージャー、部分適用などのツールを使用すると、定型文を少なくして簡単に実行できます。

問題があるのは依存関係自体ではないことに注意してください。 間違った方向を指すのは依存関係です。次のレイヤーは次のような関数を持っているかもしれません:

processText = spamToSMTP . emailAddressToSpam . removeEmailDups . textToEmailAddresses

このレイヤーがこのようにハードコードされた依存関係を持つことは完全に問題ありません。なぜなら、その唯一の目的は、下位レイヤーの機能を一緒に接着することだからです。実装の入れ替えは、異なるコンポジションを作成するのと同じくらい簡単です。

processTextFancy = spamToSMTP . emailAddressToFancySpam . removeEmailDups . textToEmailAddresses

この簡単な再構成は、副作用がないために可能になっています。下位層の機能は互いに完全に独立しています。次のレイヤーでは、いくつかのユーザー設定に基づいて、実際に使用するprocessTextを選択できます。

actuallyUsedProcessText = if (config == "Fancy") then processTextFancy else processText

繰り返しますが、すべての依存関係が一方向を指しているため、問題ではありません。純粋な関数がすでにそうするように強制したので、それらをすべて同じ方法で指すようにするために一部の依存関係を反転する必要はありません。

一番上でチェックする代わりにconfigを最下層に渡すことで、これをさらに結合できることに注意してください。 FPを使用しても、これを防ぐことはできませんが、試してみると非常に煩わしくなります。

29
Karl Bielefeldt

関数型プログラミングは依存性注入パターンの実行可能な代替手段ですか?

これは奇妙な質問です。関数型プログラミングのアプローチは、主に依存性注入に接しています。

確かに、不変の状態があると、副作用があるか、関数間の暗黙的なコントラクトとしてクラスの状態を使用することで、「だまされない」ようにプッシュできます。これにより、データの受け渡しがより明確になります。これは、依存性注入の最も基本的な形式だと思います。そして、関数を渡すという関数型プログラミングの概念は、それをずっと簡単にします。

ただし、依存関係は削除されません。状態が変更可能であった場合、操作には必要なすべてのデータ/操作が必要です。そして、それらの依存関係を何らかの方法でそこに取得する必要があります。したがって、関数型プログラミングがreplace DIに近づくとは言いません。したがって、代替手段はありません。

どちらかと言えば、プログラマーがめったに考えないよりもOOコードが暗黙的な依存関係を作成する可能性があることを示しています。

8
Telastyn

あなたの質問への簡単な答えは次のとおりです:いいえ

しかし、他の人が主張しているように、この質問は2つのやや関連のない概念と結婚しています。

これを一歩ずつやっていきましょう。

DIが機能しないスタイルになる

関数プログラミングの中心には、純粋な関数があります-入力を出力にマップする関数なので、特定の入力に対して常に同じ出力を取得します。

DItypicalは、噴射によって出力が変化する可能性があるため、ユニットが純粋ではなくなったことを意味します。たとえば、次の関数では:

const bookSeats = ( seatCount, getBookedSeatCount ) => { ... }

getBookedSeatCount(関数)は異なる場合があり、同じ特定の入力に対して異なる結果が得られます。これにより、bookSeatsも不純になります。

これには例外があります。異なるアルゴリズムを使用していますが、同じ入出力マッピングを実装する2つのソートアルゴリズムの1つを挿入できます。しかし、これらは例外です。

システムは純粋ではあり得ない

システムが純粋ではあり得ないという事実は、関数型プログラミングソースで主張されているため、同様に無視されます。

システムには副作用があり、明らかな例は次のとおりです。

  • UI
  • データベース
  • API(クライアント/サーバーアーキテクチャ)

したがって、システムの一部には副作用が含まれている必要があり、その部分には命令スタイル、つまりOOスタイルも含まれる場合があります。

シェルコアのパラダイム

Gary Bernhardtの優れた境界に関する講演 から用語を借用すると、優れたシステム(またはモジュール)アーキテクチャには次の2つの層が含まれます。

  • コア
    • 純粋な機能
    • 分岐
    • 依存関係なし
  • シェル
    • 不純(副作用)
    • 分岐なし
    • 依存関係
    • OOスタイルなど).

重要なポイントは、システムを純粋な部分(コア)と不純な部分(シェル)に「分割」することです。

少し欠陥のあるソリューション(および結論)を提供していますが、 このMark Seemannの記事 は、まったく同じ概念を提案しています。 Haskellの実装は、FPを使用してすべて実行できることを示しているため、特に洞察に富んでいます。

DIおよびFP

アプリケーションの大部分が純粋であっても、DIの採用は完全に合理的です。重要なのは、DIを不純なシェル内に制限することです。

APIスタブがその例です。実際のAPIを本番環境で使用したいが、テストではスタブを使用します。シェルコアモデルを順守することは、ここで大いに役立ちます。

結論

したがってFPとDIは正確な代替ではありません。システムに両方が存在する可能性が高いため、システムの純粋な部分と不純な部分を確実に分離することをお勧めします。ここでFPとDIはそれぞれ存在します。

6
Izhaki

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.
1
Den