web-dev-qa-db-ja.com

依存性注入の批判と欠点

依存性注入(DI)は、よく知られた流行のパターンです。ほとんどのエンジニアは、次のような利点を知っています。

  • 単体テストでの分離を可能/簡単にする
  • クラスの依存関係を明示的に定義する
  • 優れた設計の促進( 単一責任の原則 (SRPなど))
  • 実装の切り替えをすばやく有効にする(たとえば、DbLoggerではなくConsoleLogger

DIは優れた有用なパターンであるという業界全体のコンセンサスがあると思います。現時点ではあまり批判はありません。コミュニティで言及されている欠点は通常、軽微です。それらのいくつか:

  • クラス数の増加
  • 不要なインターフェースの作成

現在、私たちは同僚と建築設計について話し合っています。彼はかなり保守的ですが、オープンマインドです。 ITの多くの人々は最新のトレンドをコピーし、利点を繰り返し、一般にあまり考えないでください-深く分析しないでください。

私が聞きたいのは:

  • 実装が1つしかない場合、依存性注入を使用する必要がありますか?
  • 言語/フレームワーク以外の新しいオブジェクトの作成を禁止する必要がありますか?
  • 特定のクラスをユニットテストする予定がない場合、単一の実装を注入することは悪い考えです(実装が1つしかないため、「空の」インターフェイスを作成したくないとしましょう)。
130
Landeeyo

最初に、フレームワークの概念から設計アプローチを分離したいと思います。最も単純で最も基本的なレベルでの依存性注入は次のとおりです。

親オブジェクトは、子オブジェクトに必要なすべての依存関係を提供します。

それでおしまい。インターフェース、フレームワーク、注入のスタイルなどを必要とするものは何もないことに注意してください。公平にするために、私はこのパターンについて20年前に最初に学びました。 新品ではありません。

依存関係注入のコンテキストでは、親と子という用語について2人以上が混乱しているため:

  • parentは、使用する子オブジェクトをインスタンス化して構成するオブジェクトです
  • childは、受動的にインスタンス化されるように設計されたコンポーネントです。つまり親によって提供される依存関係を使用するように設計されており、独自の依存関係をインスタンス化しません。

依存性注入は、オブジェクトcompositionのパターンです。

なぜインターフェース?

インターフェースは契約です。これらは、2つのオブジェクトを緊密に結合する方法を制限するために存在します。すべての依存関係にインターフェースが必要なわけではありませんが、モジュール式コードの作成に役立ちます。

単体テストの概念を追加すると、特定のインターフェイスに2つの概念的な実装がある可能性があります。アプリケーションで使用する実際のオブジェクトと、オブジェクトに依存するコードのテストに使用するモックまたはスタブされたオブジェクトです。それだけでも、インターフェイスを十分に正当化できます。

なぜフレームワーク?

子オブジェクトへの依存関係を本質的に初期化して提供することは、子オブジェクトの数が多い場合に困難になることがあります。フレームワークには次の利点があります。

  • コンポーネントへの依存関係の自動配線
  • ある種の設定でコンポーネントを構成する
  • ボイラープレートコードを自動化して、複数の場所で記述されていることを確認する必要がないようにします。

また、次の欠点もあります。

  • 親オブジェクトは「コンテナ」であり、コードには何もありません
  • テストコードで直接依存関係を提供できない場合、テストがより複雑になります。
  • リフレクションや他の多くのトリックを使用してすべての依存関係を解決するため、初期化が遅くなる可能性があります
  • 特にコンテナーがインターフェースとインターフェースを実装する実際のコンポーネントの間にプロキシーを挿入する場合、ランタイムのデバッグはより困難になる可能性があります(Springに組み込まれているアスペクト指向プログラミングが思い浮かびます)。コンテナーはブラックボックスであり、デバッグプロセスを容易にするという概念で常にビルドされるわけではありません。

そうは言っても、トレードオフがあります。可動部品が多くなく、DIフレームワークを使用する理由がほとんどない小規模なプロジェクトの場合。ただし、特定のコンポーネントがすでに作成されている、より複雑なプロジェクトの場合は、フレームワークを正当化できます。

[インターネット上のランダムな記事]はどうですか?

それはどうですか?多くの場合、人々は熱心になり、たくさんの制限を加え、あなたが「真の方法」で物事をしていなければあなたを非難することができます。真の方法は1つではありません。記事から有用なものを抽出できるかどうかを確認し、同意しないものは無視してください。

要するに、自分で考えて、いろいろ試してみてください。

「古いヘッド」での作業

できるだけ多くのことを学んでください。70年代に取り組んでいる多くの開発者に見られるのは、独断的ではないことを学んだということです。多くのこと。彼らには、何十年もの間使用してきた正しい方法を生み出す方法があります。

私はこれらのいくつかを扱う特権を持っていて、彼らは非常に理にかなっているいくつかの残酷に正直なフィードバックを提供することができます。そして、価値を見いだすと、彼らはそれらのツールをレパートリーに追加します。

168
Berin Loritsch

依存性注入は、ほとんどのパターンと同様に、問題の解決策です。そもそも問題があるかどうかを尋ねることから始めましょう。そうでない場合、パターンを使用すると、コードがworseになります。

依存関係を削減または排除できるかどうかを最初に検討してください。他のすべての条件は同じであるため、システム内の各コンポーネントの依存関係をできるだけ少なくします。そして、依存関係がなくなった場合、注入するかどうかという問題は根本的に解決します!

外部サービスからデータをダウンロードして解析し、複雑な分析を実行して結果をファイルに書き込むモジュールを考えてみましょう。

ここで、外部サービスへの依存関係がハードコーディングされている場合、このモジュールの内部処理を単体テストすることは本当に困難になります。したがって、外部サービスとファイルシステムをインターフェースの依存関係として挿入することを決定できます。これにより、代わりにモックを挿入できるようになり、内部ロジックのユニットテストが可能になります。

しかし、はるかに優れた解決策は、分析を入力/出力から分離することです。分析が副作用なしでモジュールに抽出されると、テストがはるかに容易になります。モッキングはコードの匂いであることに注意してください-常に回避できるわけではありませんが、一般的には、モッキングに依存せずにテストできる方が優れています。したがって、依存関係を排除することで、DIが軽減するはずの問題を回避できます。このような設計は、SRPにも非常に準拠しています。

DIは必ずしもSRPや懸念の分離、高い凝集性/低い結合などのような他の優れた設計原則を促進するわけではないことを強調したいと思います。逆の効果もあるかもしれません。内部で別のクラスBを使用するクラスAについて考えます。 BはAによってのみ使用されるため、完全にカプセル化され、実装の詳細と見なすことができます。これを変更してAのコンストラクターにBを注入すると、この実装の詳細が公開され、この依存関係とBを初期化する方法、Bの寿命などの知識がシステム内の他の場所に個別に存在する必要があります。 Aから、リークの懸念がある全体的に悪いアーキテクチャがあります。

一方、DIが本当に役立つ場合もあります。たとえば、ロガーのような副作用のあるグローバルサービスの場合です。

問題は、パターンとアーキテクチャーがツールではなく、それ自体で目標になるときです。 「DIを使用する必要がありますか?」馬の前にカートを置くようなものです。 「問題はありますか?」そして「この問題の最善の解決策は何ですか?」

質問の一部は次のように要約されます。「パターンの要求を満たすために、余分なインターフェースを作成する必要がありますか?」あなたはおそらくこれに対する答えをすでに知っています- 絶対違う!そうでなければあなたに言っている誰もがあなたに何かを売り込もうとしています-おそらく高価なコンサルティング時間。インターフェイスは、抽象化を表す場合にのみ値を持ちます。単一のクラスの表面を模倣するだけのインターフェースは「ヘッダーインターフェース」と呼ばれ、これは既知のアンチパターンです。

92
JacquesB

私の経験では、依存性注入にはいくつかの欠点があります。

まず、DIを使用しても、宣伝されているほど自動テストは簡素化されません。インターフェイスのモック実装でクラスをユニットテストすると、そのクラスがインターフェイスとどのように相互作用するかを検証できます。つまり、テスト対象のクラスがインターフェイスによって提供されるコントラクトをどのように使用するかを単体テストできます。ただし、これにより、テスト中のクラスからインターフェイスへの入力が期待どおりであることをはるかに保証できます。テスト中のクラスがインターフェイスからの出力に期待どおりに応答するという保証はやや不十分です。これは、ほぼ例外なくモック出力であり、それ自体がバグや単純化などの影響を受けやすいためです。つまり、インターフェイスの実際の実装でクラスが期待どおりに動作することを検証できません。

第2に、DIを使用すると、コード内を移動することがはるかに困難になります。関数への入力として使用されるクラスの定義に移動しようとする場合、インターフェイスは、マイナーな煩わしさ(たとえば、単一の実装がある場合)からメジャーなタイムシンク(たとえば、IDisposableのような過度に一般的なインターフェイスが使用される場合)まで何でもかまいません。使用されている実際の実装を見つけようとするとき。これにより、「このロギングステートメントが出力された直後に発生するコードのnull参照例外を修正する必要がある」などの簡単な練習を1日の長い労力に変えることができます。

第三に、DIとフレームワークの使用は両刃の剣です。一般的な操作に必要な定型コードの量を大幅に削減できます。ただし、これには、これらの一般的な操作が実際にどのように相互に関連付けられているかを理解するために、特定のDIフレームワークの詳細な知識が必要になるという犠牲が伴います。依存関係がフレームワークに読み込まれる方法を理解し、新しい依存関係をフレームワークに追加して挿入するには、大量の背景資料を読み、フレームワークに関するいくつかの基本的なチュートリアルに従う必要があります。これにより、いくつかの単純なタスクをかなり時間がかかるタスクに変えることができます。

37
Eric

私は、「。NETでの依存性注入」からのマークシーマンのアドバイスに従いました-推測しました。

DIは、「揮発性の依存関係」がある場合に使用する必要があります。変更される可能性が十分にあります。

したがって、将来的に複数の実装が存在する可能性がある場合、または実装が変更される可能性がある場合は、DIを使用してください。それ以外の場合は、newで問題ありません。

15
Rob

DIに関する私の最大の不満はすでにいくつかの回答で言及されていますが、ここで少し詳しく説明します。 DI(今日はほとんどがコンテナなどで行われているため)は本当に、コードの可読性を本当に損ないます。そして、おそらくコードの可読性が、今日のほとんどのプログラミング革新の背後にある理由です。誰かが言ったように-コードを書くのは簡単です。コードを読むのは難しいです。しかし、ある種の小さな追記型使い捨てユーティリティを作成しているのでない限り、これも非常に重要です。

この点でDIの問題は、不透明であることです。コンテナはブラックボックスです。オブジェクトは単にどこかから出現し、あなたには分からない-誰がいつそれらを構築したのか?コンストラクタに何が渡されましたか?このインスタンスを誰と共有していますか?知るか...

そして、主にインターフェイスを操作する場合、IDEのすべての「定義に移動」機能は完全に機能します。プログラムを実行せずにステップ実行するだけでプログラムのフローを追跡するのは非常に困難です。この特定の場所で使用されたインターフェースの実装を確認するためにスルーします。また、場合によっては、ステップスルーを妨げる技術的なハードルがあります。可能であれば、DIコンテナーのねじれた腸を通過する場合、全体事件はすぐに欲求不満の運動になります。

DIを使用したコードの一部を効率的に使用するには、そのコードに精通していて、すでにknowがどこにあるかを理解している必要があります。

7
Vilx-

実装の切り替えをすばやく有効にする(たとえば、ConsoleLoggerの代わりにDbLogger)

一般的にDIは確かに良いものですが、すべてに盲目的に使用しないことをお勧めします。たとえば、ロガーを注入することはありません。 DIの利点の1つは、依存関係を明示的かつ明確にすることです。ほぼすべてのクラスの依存関係としてILoggerを一覧表示する意味はありません。必要な柔軟性を提供するのはロガーの責任です。私のロガーはすべて静的な最終メンバーです。静的でないものが必要な場合は、ロガーを挿入することを検討します。

クラス数の増加

これは、DI自体ではなく、特定のDIフレームワークまたはモックフレームワークの欠点です。ほとんどの場所で、私のクラスは具象クラスに依存しています。つまり、必要なボイラープレートはありません。 Guice(Java DIフレームワーク)はデフォルトでクラスをそれ自体にバインドし、テストでのバインドをオーバーライドするだけで済みます(または代わりに手動でワイヤリングします)。

不要なインターフェースの作成

必要なときにのみインターフェイスを作成します(これはかなりまれです)。これは、クラスのすべての出現箇所をインターフェイスで置き換える必要があることを意味しますが、IDEでこれを行うことができます。

実装が1つしかない場合、依存性注入を使用する必要がありますか?

はい、しかし 追加された定型文は避けてください

言語/フレームワーク以外の新しいオブジェクトの作成を禁止する必要がありますか?

いいえ。多くの値(不変)クラスとデータ(可変)クラスがあり、インスタンスは作成されて渡されるだけであり、別のオブジェクト(または他のオブジェクトにのみ保存されることはないため)を挿入する意味がありません。そのようなオブジェクト)。

それらについては、代わりにファクトリを注入する必要があるかもしれませんが、ほとんどの場合それは意味をなしません(例えば、@Value class NamedUrl {private final String name; private final URL url;};ここにファクトリーは本当に必要なく、注入するものもありません)。

特定のクラスをユニットテストする予定がない場合、単一の実装を注入することは悪い考えです(実装が1つしかないため、「空の」インターフェイスを作成したくないとしましょう)。

私見それはそれがコード膨張を引き起こさない限り問題ありません。依存関係を挿入しますが、インターフェースを作成しないでください(そして、面倒な設定XMLはありません!)後で簡単に行うことができるためです。

実際、私の現在のプロジェクトには、4つのクラス(数百のうち)があります。これらは、データオブジェクトを含め、非常に多くの場所で使用される単純なクラスであるため、 DIから除外 にすることにしました。


ほとんどのDIフレームワークのもう1つの欠点は、実行時のオーバーヘッドです。これはコンパイル時に移動できます(Javaの場合、 Dagger があり、他の言語についてはわかりません)。

さらに別の欠点は、魔法がどこでも発生することであり、これは調整できます(たとえば、Guiceを使用するときにプロキシの作成を無効にしました)。

3
maaartinus