web-dev-qa-db-ja.com

依存性注入が多すぎますか?

私は、クラスの依存関係である文字通りすべての(Spring)Dependency Injectionを使用するプロジェクトで作業しています。 Spring構成ファイルが約4000行に増えたところです。少し前に、ボブおじさんの講演の1つをYouTubeで見ました(残念ながら、リンクが見つかりませんでした)。彼は、メインのコンポーネントにいくつかの中心的な依存関係(たとえば、ファクトリー、データベースなど)だけを注入することを推奨しています。配布されます。

このアプローチの利点は、DIフレームワークがアプリケーションのほとんどの部分から切り離されていることです。また、工場には以前の構成に含まれていたものの多くが含まれるため、Spring構成がよりクリーンになります。逆に、これにより、多くのファクトリクラス間で作成ロジックが分散し、テストがより困難になる可能性があります。

だから私の質問は本当にあなたがどちらか一方のアプローチであなたが見る他の利点または欠点です。ベストプラクティスはありますか?ご回答ありがとうございます!

39
Antimon

いつものように、それはDepends™です。答えは、解決しようとしている問題によって異なります。この回答では、私はいくつかの一般的な動機づけ力に対処しようとします:

より小さなコードベースを優先する

Spring構成コードが4,000行ある場合、コードベースには数千のクラスがあると思います。

事後に対処することはほとんど問題ではありませんが、経験則として、私はコードベースが小さい、より小さなアプリケーションを好む傾向があります。 Domain-Driven Design を使用している場合は、たとえば、境界コンテキストごとにコードベースを作成できます。

私のキャリアのほとんどでWebベースの基幹業務コードを書いてきたので、このアドバイスは私の限られた経験に基づいています。デスクトップアプリケーションや組み込みシステムなどを開発している場合は、物事を分離するのが難しいと想像できます。

この最初のアドバイスは実用性に欠けることが容易に理解できますが、それが最も重要であると私は信じています。コードの複雑さは、コードベースのサイズに応じて非線形(場合によっては指数関数的に)に変化します。

純粋なDIを優先

この質問が既存の状況を表すことを私はまだ認識していますが、私は Pure DI をお勧めします。 DIコンテナを使用しないでください。ただし、使用する場合は、 少なくとも、これを使用して、コンベンションベースのコンポジションを実装します

私はSpringの実践的な経験はありませんが、構成ファイルによって、XMLファイルが暗黙に含まれていると想定しています。

XMLを使用して依存関係を構成することは、両方の世界で最悪です。まず、コンパイル時の型安全性は失われますが、何も得られません。 XML構成ファイルは、置き換えようとするコードと同じ大きさになる可能性があります。

対処するつもりの問題と比較して、依存関係注入構成ファイルは 構成の複雑さのクロック の間違った場所を占めています。

粗粒度の依存性注入の事例

大まかな依存性注入のケースを作ることができます。また、依存関係を細かく注入することもできます(次のセクションを参照)。

いくつかの「中央」依存関係のみを注入する場合、ほとんどのクラスは次のようになります。

public class Foo
{
    private readonly Bar bar;

    public Foo()
    {
        this.bar = new Bar();
    }

    // Members go here...
}

Fooが構成するため、これは Design Patternsクラス継承よりもオブジェクト構成を優先しますBar。保守性の観点からは、構成を変更する必要がある場合はFooのソースコードを編集するだけなので、これは保守可能と見なすことができます。

これは、依存性注入よりも保守性がほとんどありません。実際、依存関係注入に固有の間接的な方法に従う必要はなく、Barを使用するクラスを直接編集する方が簡単だと思います。

依存性注入に関する私の本 の初版では、揮発性と安定性の依存関係を区別しています。

揮発性依存関係は、注入を検討する必要がある依存関係です。彼らは含まれています

  • コンパイル後に再構成可能でなければならない依存関係
  • 別のチームが並行して開発した依存関係
  • 非決定的な動作を伴う依存関係、または副作用を伴う動作

一方、安定した依存関係は、明確に定義された方法で動作する依存関係です。ある意味では、この区別が大まかな依存性注入のケースを作っていると主張することができますが、私が本を書いたときにそれを完全には認識していなかったことを認めなければなりません。

ただし、テストの観点から見ると、これは単体テストを難しくします。 Fooから独立してBarを単体テストすることはできなくなりました。 J.B。としてRainsbergerは説明します 、統合テストは組み合わせの複雑さの爆発に悩まされています。 4〜5個のクラスを統合してすべてのパスをカバーしたい場合は、文字通り何万ものテストケースを記述する必要があります。

それに対する反論は、多くの場合、あなたの仕事はクラスをプログラムすることではないということです。あなたの仕事は、いくつかの特定の問題を解決するシステムを開発することです。これが Behaviour-Driven Development (BDD)の背後にある動機です。

これに関する別の見解は、 TDDはテストに起因する設計の損傷 につながると主張するDHHによって提示されています。彼はまた、粗粒度の統合テストを支持しています。

ソフトウェア開発についてこの見方をすれば、大まかな依存性注入は理にかなっています。

きめの細かい依存性注入の事例

一方、きめの細かい依存性注入は、すべてのものを注入すると表現できます。

大まかな依存性注入に関する私の主な懸念は、J.B。Rainsbergerが表明した批判です。統合テストですべてのコードパスをカバーすることはできません。すべてのコードパスをカバーするために、文字通り数千または数万のテストケースを記述する必要があるためです。

BDDの支持者は、すべてのコードパスをテストでカバーする必要がないという議論に対抗します。ビジネス価値を生み出すものだけをカバーする必要があります。

ただし、私の経験では、すべての「エキゾチックな」コードパスも大規模な展開で実行され、テストされない場合、それらの多くに欠陥があり、実行時例外(多くの場合null参照例外)が発生します。

これにより、すべてのオブジェクトの不変条件を個別にテストできるため、きめ細かい依存性注入が好まれました。

関数型プログラミングを好む

私はきめ細かな依存性注入に傾倒していますが、 それは本質的にテスト可能 であるため、とりわけ、関数型プログラミングに重点を移しました。

SOLIDコードに移行するほど、 より機能的になります 。遅かれ早かれ、思い切って服用することもできます。 機能アーキテクチャはポートとアダプタのアーキテクチャ であり、 依存性注入も試行であり、ポートとアダプタ です。ただし、違いは、Haskellのような言語が、その型システムを介してそのアーキテクチャーを実施することです。

静的に型付けされた関数型プログラミングを優先する

この時点では、私は本質的にオブジェクト指向プログラミング(OOP)をあきらめていますが、OOPの問題の多くは、概念自体よりもJavaやC#などの主流の言語に本質的に結びついています。

主流のOOP言語の問題は、テストされていない場合に実行時例外が発生する組み合わせ爆発の問題を回避することがほぼ不可能であることです。一方、HaskellやF#などの静的型付き言語では、型システムで多くの決定点をエンコードできます。これは、何千ものテストを作成する代わりに、コンパイラーはすべての可能なコードパスを処理したかどうかを通知することを意味します(ある程度まで、それは特効薬ではありません)。

また、 依存性注入は機能しません 。真の関数型プログラミングは、 依存関係の概念全体を拒否する必要があります 。結果はより単純なコードです。

概要

C#での作業を余儀なくされる場合は、コードベース全体を管理可能な数のテストケースでカバーできるため、粒度の細かい依存性注入を好みます。

結局、私の動機は迅速なフィードバックです。それでも、 単体テストはフィードバックを得るための唯一の方法ではありません

45
Mark Seemann

巨大なDIセットアップクラスが問題です。しかし、置き換えられるコードを考えてみてください。

これらすべてのサービスをインスタンス化する必要があります。そのためのコードは、app.main()の開始点にあり、手動で挿入されるか、クラス内でthis.myService = new MyService();として密結合されます。

セットアップクラスを複数のセットアップクラスに分割してサイズを小さくし、プログラムの開始点から呼び出します。すなわち。

main()
{
   var c = new diContainer();
   var service1 = diSetupClass.SetupService1(c);
   var service2 = diSetupClass.SetupService2(c, service1); //if service1 is required by service2
   //etc

   //main logic
}

他のciセットアップメソッドに渡す場合を除いて、service1または2を参照する必要はありません。

これは、セットアップを呼び出す順序を強制するため、コンテナから取得しようとするよりも優れています。

1
Ewan