依存性注入パターンを使用するのが適切でない場合


124

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


1
関連(おそらく重複していない) - stackoverflow.com/questions/2407540/...
Steve314

3
ゴールデンハンマーのアンチパターンに少し似ています。 en.wikipedia.org/wiki/Anti-pattern
Cuga

回答:


123

基本的に、依存性注入は、オブジェクトの性質に関する(通常は常にではありませんが)いくつかの仮定を行います。それらが間違っている場合、DIは最良の解決策ではない可能性があります。

  • まず、最も基本的に、DIはオブジェクト実装の密結合は常に悪いと仮定します。これは、依存関係反転の原則の本質です。「依存関係は、結石に対しては決して行われるべきではなく、抽象化に対してのみ」。

    これは、具体的な実装への変更に基づいて、変更する依存オブジェクトを閉じます。ConsoleWriterに依存するクラスは、出力を代わりにファイルに送信する必要がある場合に変更する必要がありますが、クラスがWrite()メソッドを公開するIWriterのみに依存している場合、現在使用されているConsoleWriterをFileWriterおよび従属クラスはその違いを知りません(リスホフ代替原理)。

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

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

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

  • 最後に、DIはその性質上、すべての依存関係とその実装に関する知識を集中化します。これにより、インジェクションを実行するアセンブリに必要な参照の数が増えますが、ほとんどの場合、実際の依存クラスのアセンブリに必要な参照の数は減りません。

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

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

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

    このすべてが、再び存在しないかもしれない場所の問題を解決するために。


1
SRP =単一の責任原則、疑問に思う他の人のために。
セオドアマードック

1
はい、LSPはリスコフ置換の原則です。Aに依存関係がある場合、他の変更なしでAから派生したBが依存関係を満たすことができるはずです。
キース

依存性注入は、クラスDからクラスBなどからクラスAを取得する必要がある場合に特に役立ちます(その場合のみ)。DIフレームワークは、貧しい人の注入よりもアセンブリの膨張を少なくすることができます。また、私は、コードを常に最適化することができるため、メンテナンスコストに、決してCPUコストを考える.. DIによって引き起こされるボトルネックがありませんでしたが、理由もなく、コストを持っていることをやって
GameDeveloper

別の仮定は、「依存関係が変わると、どこでも変わる」ということでしょうか?または、とにかくすべての消費クラスを見る必要があります
リチャードティングル

3
この回答では、依存関係の注入とハードコーディングのテスト容易性への影響に対処できません。また、明示的な依存関係の原則(参照deviq.com/explicit-dependencies-principleを
ssmith

30

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

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

そのため、依存性注入(フレームワークなし)は、有益であると同時にほぼ有害です。

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

モックの依存関係を使用できるようにするための別の理由は、テストです。これは、依存関係の挿入を許可するテストのみに使用される追加のコンストラクターを追加することを意味する必要があります:代わりにコンストラクターを変更して依存関係を常に挿入する必要がある場合は、突然、依存関係の依存関係を構築して直接的な依存関係があり、作業を完了できません。

これは役立つこともありますが、各依存関係について明確に自問する必要があります。テストの利点はコストに見合うものであり、テスト中にこの依存関係を真似したいのでしょうか?

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

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

コンストラクター注入の場合と同様に、そうすることの利点は、そうすることのコストに非常に似ていると思います。依存しているデータを誰が提供しているかを知る必要はありません。プロバイダーの場合、プロバイダーをチェックする優先順序を知る必要はありません。データを必要とするすべての場所がすべての潜在的なプロバイダーのチェックを行うことなどを確認してください。プラットフォーム。

個人的にはDIフレームワークの経験はあまりありませんが、必要なデータやサービスの適切なプロバイダーを見つけるという頭痛が頭痛よりも高い場合、コストよりもメリットが大きいと思います。何かが失敗したとき、どのコードが悪いデータを提供したかをすぐにローカルで知らず、それがコードのその後の失敗を引き起こした。

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


6
テストでの依存関係のモックについての簡単なコメントと「依存関係の依存関係の依存関係について知っておく必要があります」...それはまったく真実ではありません。モックが注入されている場合、具体的な実装の依存関係を知ったり、気にする必要はありません。テスト対象のクラスの直接的な依存関係についてのみ知る必要があり、それはモックによって満たされます。
エリックキング

6
あなたは誤解している、私はあなたがモックを注入するときの話ではなく、本当のコードの話だ。クラスAに依存関係Bがあり、その依存関係Cに依存関係Dがあります。DIがなければ、AはBを構成し、BはCを構成し、CはCを構成します。 Bを構築するには、最初にCを構築する必要があり、Cを構築するには、最初にDを構築する必要があります。したがって、クラスAは、Bを構築するために、依存関係の依存関係であるDについて知る必要があります。これは、過剰な結合につながります。
セオドアマードック

1
依存関係の注入を許可するだけの追加のコンストラクターをテストに使用するのにそれほどコストはかかりません。私が言ったことを修正しようとします。
セオドアマードック

3
依存性注入は、ゼロサムゲームでのみ依存性を移動します。依存関係を既存の場所(メインプロジェクト)に移動します。規則ベースのアプローチを使用して、たとえば名前空間などによって具体的なクラスを指定することで、実行時にのみ依存関係を解決することもできます。私は、これは単純な答えであると言っている
のSprague

2
@Sprague Dependency Injectionは、適用すべきでない場合に適用すると、やや負の合計ゲームで依存関係を移動します。この回答の目的は、依存性注入が本質的に良好ではないことを人々に思い出させることです。これは、適切な状況では信じられないほど有用な設計パターンですが、通常の状況では不当なコストです(少なくとも私が働いていたプログラミングドメインでは、一部のドメインでは他のドメインよりも頻繁に役立つことを理解しています)。DIは時々流行語になり、それ自体が目的になりますが、これはポイントを逃します。
セオドアマードック

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

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

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

また、依存関係注入の代替手段としてサービスロケーターを使用しないでください。これはアンチパターンです。詳細:http : //blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx


すべてのポイントは、完全に使用するのではなく、異なる形式の注入を使用することです。ただし、リンクの場合は+1。
Jan Hudec

3
Service Locatorはアンチパターンではなく、リンクしているブログの人はパターンの専門家ではありません。StackExchangeで有名になったことは、ソフトウェア設計に対する一種の不利益です。Martin Fowler氏(エンタープライズアプリケーションアーキテクチャの定番パターンの作者は)問題に関するより合理的な見解を持っている:martinfowler.com/articles/injection.html
コリン・

1
@Colin、それどころか、Service Locatorは間違いなくアンチパターンであり、ここでは簡単に言えばその理由です。
キラレッサ

@Kyralessa-あなたは、DIフレームワークが内部的にService Locatorパターンを使用してサービスを検索することを理解していますか?StackExchangeが一部の人々がダンプしたいという嫌悪感にもかかわらず、両方が適切であり、どちらもアンチパターンではない状況があります。
コリン

もちろん、私の車は内部でピストンとスパークプラグを使用していますが、車を運転したいだけの普通の人間にとっては良いインターフェイスではありません。
キラレッサ

8

プロジェクトの保守とテストを可能にすることで何も得られないとき。

真剣に、私は一般的にIoCとDIが大好きで、98%の時間でそのパターンを必ず使用すると言います。ロジックを実装から分離するため、異なるチームメンバーや異なるプロジェクトが何度もコードを再利用できるマルチユーザー環境では特に重要です。別のプロジェクトが同じロギングフレームワークを使用することを保証するものではないため、ロギングフレームワークは、ロギングフレームワークにプラグインするだけでなく、クラスに注入されたILogインターフェースはメンテナンスの数千倍も維持できます。 1つです!)。

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

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

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


ロギングの意味がわかりません。確かにすべてのロガーが同じインターフェースに準拠するわけではないので、このプロジェクトのロギング方法と特定のロガーの間で変換する独自のラッパーを作成する必要があります(ロガーを簡単に変更できるようにしたい場合)。それで、DIはあなたに何を与えますか?
リチャードティングル

@RichardTingleロギングラッパーをインターフェイスとして定義し、複数の抽象化をラップする単一のロギングクラスを使用する代わりに、外部ロギングサービスごとにそのインターフェイスの軽量実装を作成する必要があると思います。それはあなたが提案しているのと同じ概念ですが、異なるレベルで抽象化されています。
エドジェームズ

1

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

依存性注入の導入が成功するためには、長年にわたって、いくつかの実際的な要件を満たす必要があるという結論に達しました。

  1. それを導入する理由があるはずです。

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

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

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

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

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

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


1

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

一度起動するアプリケーションがあり、長時間実行される場合(Webアプリなど)、DIが良い場合があります。

何度も起動し、より短い時間(モバイルアプリのように)実行するアプリケーションがある場合、おそらくコンテナーは必要ありません。


私はどのように生きDIに関連するアプリケーションの時間が表示されない
マイケルFreidgeim

@MichaelFreidgeimコンテキストの初期化には時間がかかります。通常、DIコンテナはSpringなどの非常に重いです。1つのクラスだけでHello Worldアプリを作成し、Springで1つ作成して両方を10回起動すると、私の意味がわかります。
コライトゥゲイ

個々のDIコンテナの問題のように聞こえます。ネットの世界では、私はDIコンテナのための本質的な問題として、初期化時間について聞いたことがない
マイケルFreidgeim

1

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

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

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

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

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

psおそらく、この投稿には多くのマイナス点があります。現代の開発者のほとんどは、内部キーワードを使用する方法と理由、コンポーネントのカップリングを減らす方法、そして最後にそれを減らす理由を理解していないからですコーディングして比較しよう


0

依存性注入代替手段は、サービスロケーターの使用です。Service Locatorは、特にDIフレームワークを使用していない場合、理解、デバッグが容易であり、オブジェクトの構築をより簡単にします。サービスロケーターは、外部の静的な依存関係、たとえばデータアクセスレイヤーのすべてのオブジェクトに渡す必要があるデータベースなどを管理するための優れたパターンです。

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

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

TLDR:クラスに静的な依存関係がある場合、またはレガシーコードをリファクタリングしている場合、Service LocatorはDIよりも間違いなく優れています。


12
Service Locatorは、コード内の依存関係を隠します。使用するのは良いパターンではありません。
キラレッサ

静的な依存関係に関しては、インターフェイスを実装するファサードが平手打ちされているのが見たいです。これらは依存性を注入することができ、静的な手ren弾に落ちます。
エリックディートリッヒ

@Kyralessa私は同意します、サービスロケーターには多くの欠点があり、DIはほとんど常に望ましいです。ただし、すべてのプログラミング原則と同様に、ルールにはいくつかの例外があると思います。
ギャレットホール

サービスロケーションの主な使用方法は、「戦略ピッカー」クラスに使用する戦略を見つけるのに十分なロジックが与えられた戦略パターン内であり、それを戻すか、コールをパススルーします。この場合でも、戦略ピッカーは、同じロジックが与えられたIoCコンテナーによって提供されるファクトリーメソッドのファサードになります。それを破る理由は、最も隠されている場所ではなく、それが属する場所にロジックを配置するためです。
キース

Martin Fowlerの言葉を引用すると、「(DIとService Locator)の選択は、構成を使用から分離する原則よりも重要ではありません」。DIが常に優れているという考えは、ホグウォッシュです。サービスをグローバルに使用する場合、DIを使用する方が面倒で脆弱です。さらに、DIは、実行時まで実装がわからないプラグインアーキテクチャにはあまり適していません。常にDebug.WriteLine()などに追加の引数を渡すのがいかに厄介かを考えてください!
コリン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.