web-dev-qa-db-ja.com

依存関係注入パターンを使用することが適切でないのはいつですか?

自動テストを学び(そして愛して)以来、ほとんどすべてのプロジェクトで依存関係注入パターンを使用していることに気付きました。自動テストで作業するときに、このパターンを使用することは常に適切ですか?依存性注入の使用を避けるべき状況はありますか?

133
Tom Squires

基本的に、依存性注入は、オブジェクトの性質についていくつかの(通常は常に有効ではない)仮定を行います。これらが間違っている場合、DIは最適なソリューションではない可能性があります。

  • まず、最も基本的には、DIはオブジェクト実装の密結合が常に悪いことを前提としています。これが、依存関係逆転原理の本質です。「依存性は、具体化に基づいて行われるべきではなく、抽象化に対してのみ行われるべきです」。

    これにより、具象実装の変更に基づいて、依存オブジェクトが変更されて変更されます。 ConsoleWriterに依存するクラスは、出力を代わりにファイルに送る必要がある場合は特に変更する必要がありますが、クラスがWrite()メソッドを公開するIWriterのみに依存している場合は、現在使用されているConsoleWriterをFileWriterおよび依存クラスは違いを知りません(Liskhov Substitution Principle)。

    ただし、設計がすべてのタイプの変更に閉じられることはありません。 IWriterインターフェイス自体の設計が変更された場合、パラメーターをWrite()に追加するには、実装オブジェクト/メソッドとその使用法に加えて、追加のコードオブジェクト(IWriterインターフェイス)を変更する必要があります。実際のインターフェースの変更が、そのインターフェースの実装の変更よりも可能性が高い場合、疎結合(および疎結合の依存関係のDI化)により、解決できない問題が発生する可能性があります。

  • 次に、当然のことですが、DIは、依存するクラスが依存関係を作成するのに適した場所ではないことを前提としています。これは単一責任原則に当てはまります。依存関係を作成し、それを使用するコードがある場合、依存クラスを変更する必要がある2つの理由があります(使用法の変更OR実装))。SRPに違反します。

    ただし、ここでも、DIの間接層を追加することで、存在しない問題を解決できる場合があります。依存関係にロジックをカプセル化することが論理的であるが、そのロジックが依存関係のそのような実装のみである場合、依存関係(注入、サービスの場所、ファクトリ)の疎結合解決をコーディングすることは、実際よりも困難です。 newを使用してそれを忘れるだけです。

  • 最後に、本質的にDIは、すべての依存関係とその実装に関する知識を一元化します。これにより、注入を実行するアセンブリが持つ必要のある参照の数が増加し、ほとんどの場合、実際の依存クラスのアセンブリが必要とする参照の数は減少しません。

    SOMETHING、SOMEWHEREは、「ドットを接続」してその依存関係を満たすために、依存関係、依存関係インターフェース、および依存関係実装の知識を持っている必要があります。 DIは、IoCコンテナー、または依存関係をハイドレート(またはファクトリーメソッドを提供)する必要のあるメインフォームやコントローラーなどの「メイン」オブジェクトを作成するコードに、すべての知識を非常に高いレベルで配置する傾向があります。これにより、アプリの高レベルで多くの必然的に密結合されたコードと多くのアセンブリ参照を配置できます。実際の依存クラスからそれを「隠す」ためにこの知識だけが必要です(非常に基本的な観点から、この知識を得るのに最適な場所、それが使用される場所)。

    また、通常、コードの下位から上記の参照を削除しません。依存関係は、依存関係のインターフェイスを含むライブラリを参照する必要があります。これは、次の3つの場所のいずれかにあります。

    • 非常にアプリケーション中心になる単一の「インターフェース」アセンブリのすべて
    • 依存関係が変更されたときに依存関係を再コンパイルする必要がないという利点を取り除いて、それぞれをプライマリ実装と一緒に、または
    • 凝集度の高いアセンブリで1つまたは2つがあり、アセンブリ数が膨れ上がると、「フルビルド」時間が劇的に増加し、アプリケーションのパフォーマンスが低下します。

    これらすべて、何もないかもしれない場所で問題を解決するために。

124
KeithS

依存性注入フレームワークの外では、依存性注入(コンストラクター注入またはセッター注入による)はほぼゼロサムゲームです。オブジェクトAとその依存性Bの間の結合を減らしますが、Aのインスタンスを必要とするすべてのオブジェクトは、オブジェクトBも作成します。

AとBの間の結合を少し減らしましたが、Aのカプセル化を減らし、AとAの依存関係にも結合することによって、Aのインスタンスを構築する必要があるクラスとの間の結合を増やしました。

したがって、依存関係の注入(フレームワークなし)は、役立つのと同じくらい有害です。

追加コストは、多くの場合、簡単に正当化できます。ただし、クライアントコードがオブジェクト自体よりも依存関係を構築する方法について知っている場合、依存関係の注入は実際に結合を減らします。たとえば、スキャナーは、入力を解析するための入力ストリームを取得または構築する方法や、クライアントコードが入力を解析するために必要なソースについて詳しく知らないため、入力ストリームのコンストラクターインジェクションは明白なソリューションです。

テストは、モックの依存関係を使用できるようにするためのもう1つの正当化です。つまり、依存関係の注入を許可するテスト専用のコンストラクターを追加する必要があります。代わりに、常に依存関係の注入を要求するようにコンストラクターを変更した場合、突然、依存関係の依存関係の依存関係について知る必要があります。直接依存関係があり、作業を行うことはできません。

それは役立つかもしれませんが、必ず各依存関係について自問する必要があります。テストのメリットはコストに見合うものですか。テスト中にこの依存関係をモックしたいのですか?

依存関係注入フレームワークが追加され、依存関係の構築がクライアントコードではなくフレームワークに委任されると、コスト/利益分析が大きく変化します。

依存性注入フレームワークでは、トレードオフは少し異なります。依存関係を挿入することで失うものは、依存している実装を簡単に把握し、依存している依存関係を判断する責任を自動化された解決プロセスに移すことです(たとえば、@ Inject'ed Foo 、@ Provides Foo、および注入された依存関係が利用可能なもの、または各リソースに使用するプロバイダーを規定する高レベルの構成ファイル、またはこの2つのハイブリッド(たとえば、必要に応じて、構成ファイルを使用してオーバーライドできる依存関係の自動解決プロセスです。

コンストラクターインジェクションの場合と同様に、そうすることの利点は、やはり、そうすることのコストと非常に似たものになると思います。依存するデータを提供している人物を知る必要はなく、複数の可能性がある場合プロバイダーは、プロバイダーをチェックインするための優先順序を知る必要はありません。データを必要とするすべての場所が、すべての潜在的なプロバイダーなどをチェックすることを確認してください。これらはすべて、依存関係注入によって高レベルで処理されるためです。プラットホーム。

私は個人的にDIフレームワークについて多くの経験はありませんが、必要なデータまたはサービスの正しいプロバイダーを見つけるという頭痛が頭痛よりもコストが高い場合、DIフレームワークはコストよりも多くの利益をもたらすという印象があります。何かが失敗した場合、どのコードが悪いデータを提供したかをすぐにローカルで知ることができず、後でコードでエラーが発生しました。

場合によっては、DIフレームワークがシーンに登場したときに依存関係を曖昧にする他のパターン(サービスロケーターなど)が既に採用されており(おそらくその価値も証明されています)、DIフレームワークが採用されました。ボイラープレートコードを減らしたり、実際に使用されているプロバイダーを特定する必要が生じたときに依存関係のプロバイダーを不明瞭にしたりする可能性を少なくします。

30
  1. データベースエンティティを作成する場合は、代わりにコントローラーに注入するファクトリクラスが必要です。

  2. intやlongなどのプリミティブオブジェクトを作成する必要がある場合。また、日付やGUIDなどのほとんどの標準ライブラリオブジェクトを「手動」で作成する必要があります。

  3. 構成文字列を挿入する場合は、いくつかの構成オブジェクトを挿入する方がよいでしょう(一般的に、単純な型を意味のあるオブジェクトにラップすることをお勧めします:int temperatureInCelsiusDegrees-> CelciusDeegree temperature)

そして、依存関係注入の代替としてService locatorを使用しないでください。これはアンチパターンです。詳細: http://blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx

11
0lukasz0

プロジェクトを保守可能およびテスト可能にすることで何も得ることができない場合。

真剣に、私は一般的にIoCとDIが大好きです、そして私はそのパターンを98%使用することは間違いないと言うでしょう。マルチユーザー環境では特に重要です。コードは、ロジックを実装から分離するため、さまざまなチームメンバーやさまざまなプロジェクトで何度も再利用できます。ロギングはこれの主な例です。クラスに挿入されたILogインターフェースは、別のプロジェクトが同じロギングフレームワークを使用する保証がないので(単に全部で!).

ただし、該当するパターンでない場合があります。たとえば、オーバーライド不可能なイニシャライザー(WebMethods、私はあなたを見ているが、ProgramクラスのMain()メソッドは別の例です)によって静的コンテキストで実装される関数エントリポイントは、初期化時に依存関係を注入することはできません時間。また、プロトタイプや使い捨ての調査用のコードも悪い候補だと私は言っています。 DIの利点は、ほぼ中長期的な利点(テスト容易性と保守容易性)です。1週間以内にコードの大部分を破棄することが確実であれば、分離しても何も得られないでしょう。依存関係。コードを機能させるために、通常は依存関係のテストと分離に費やす時間を費やすだけです。

全体として、100%の時間で適用できるものはないため、方法論やパターンに実用的なアプローチをとることが賢明です。

注意すべきことの1つは、自動テストに関するコメントです。これの私の定義は自動化されたfunctionalテストです。たとえば、Webコンテキストの場合のスクリプト化されたSeleniumテストなどです。これらは一般に完全にブラックボックスのテストであり、コードの内部動作を知る必要はありません。単体テストまたは統合テストを参照している場合、DIパターンは、ほとんどの場合、そのようなホワイトボックステストに大きく依存しているプロジェクトに適用できると思います。たとえば、 DBが存在する必要なくDBにアクセスするメソッド。

8
Ed James

これは完全な答えではなく、単なる別のポイントです。

一度起動して長時間実行するアプリケーション(Webアプリなど)がある場合は、DIが良いでしょう。

起動回数が多く、実行時間が短いアプリケーション(モバイルアプリなど)がある場合、コンテナーはおそらく必要ありません。

2
Koray Tugay

基本的なOOP原則を使用してみてください:一般的な機能を抽出するためにinheritanceを使用してください)カプセル化(非表示)もの、プライベート/内部/保護されたメンバー/タイプを使用して外部から保護する必要があります。強力なテストフレームワークを使用して、テストのみのコードを挿入します(例: https://www.typemock.com/ または https://www.telerik.com/products/mocking.aspx

次に、DIを使用してコードを書き直して、通常DIで表示されるコードと比較します。

  1. あなたはより多くのインターフェース(より多くのタイプ)を持っています
  2. パブリックメソッドのシグネチャの複製を作成し、それを2倍にする必要があります(一部のパラメーターを1度だけ変更することはできません。2度行う必要があります。基本的に、すべてのリファクタリングとナビゲーションの可能性はより複雑になります)。
  3. いくつかのコンパイルエラーをランタイムエラーに移動しました(DIを使用すると、コーディング中に依存関係を無視できますが、テスト中に確実に公開されるとは限りません)。
  4. カプセル化を開きました。保護されたメンバー、内部型などが公開されました
  5. コードの総量が増加しました

私が見たものから、ほとんどの場合、コード品質はDIによって低下しています。

ただし、クラス宣言で「パブリック」アクセス修飾子、および/またはメンバーのパブリック/プライベート修飾子のみを使用している場合、および/または高価なテストツールを購入する選択肢がなく、同時にユニットテストが必要な場合統合テストに置き換えるか、注入しようとしているクラスのインターフェースがすでにある場合は、DIを選択することをお勧めします。

pSおそらく私はこの投稿から多くのマイナスを得るでしょう、現代の開発者のほとんどはinternalキーワードを使用する方法と理由を理解していないため、コンポーネントのカップリングを減らす方法、そして最後にそれを減らす理由)最後に、コード化して比較してみてください

1
Eugene Gorbovoy

他の回答は技術的な側面に焦点を当てていますが、実用的な側面を追加したいと思います。

何年にもわたって、Dependency Injectionの導入を成功させるには、満たす必要のある実際的な要件がいくつかあるという結論に達しました。

  1. それを紹介する理由があるはずです。

    これは当たり前のことのように聞こえますが、コードがデータベースからのみ取得し、ロジックなしでそれを返す場合、DIコンテナーを追加すると、物事はより複雑になり、実際のメリットはありません。ここでは、統合テストがより重要になります。

  2. チームは、訓練を受けて乗船する必要があります。

    チームの大多数が参加していて、DIがコントロールコンテナーの反転を追加することは、物事を行うためのさらに別の方法になり、コードベースをさらに複雑にすることを理解しない限り。

    DIがチームの新しいメンバーによって導入された場合、それを理解して気に入っており、DIが優れていることを示したいだけであり、チームが積極的に関与していない場合、実際にDIの品質が低下するという本当のリスクがあります。コード。

  3. テストする必要があります

    デカップリングは一般的に良いことですが、DIは依存関係の解決をコンパイル時から実行時へと移行できます。うまくテストしないと、これは実際には非常に危険です。実行時の解決の失敗は、追跡と解決にコストがかかる場合があります。
    (テストから明らかですが、多くのチームはDIで必要とされる範囲でテストしていません。)

1
tymtam

Dependency Injectionの代わりにService Locatorを使用しています 。 Service Locatorは理解とデバッグが簡単で、特にDIフレームワークを使用していない場合は、オブジェクトの構築がより簡単になります。サービスロケーターは、外部静的依存関係を管理するための適切なパターンです。データアクセス層のすべてのオブジェクトに渡す必要があります。

レガシーコードをリファクタリングする場合、依存性注入よりもサービスロケータにリファクタリングする方が簡単なことがよくあります。あなたがするすべては、インスタンス化をサービスルックアップで置き換えて、ユニットテストでサービスを偽造することです。

ただし、Service Locatorにはいくつかの欠点があります。依存関係はコンストラクタやセッターではなく、クラスの実装に隠されているため、クラスのdepandanciesを知ることはより困難です。また、同じサービスの異なる実装に依存する2つのオブジェクトを作成することは困難または不可能です。

[〜#〜] tldr [〜#〜]:クラスに静的な依存関係があるか、レガシーコードをリファクタリングしている場合、Service Locatorは間違いなくDI。

0
Garrett Hall