依存性注入はカプセル化を犠牲にする必要がありますか?


128

私が正しく理解している場合、依存性注入の一般的なメカニズムは、クラスのコンストラクターまたはクラスのパブリックプロパティ(メンバー)を介して注入することです。

これにより、注入される依存関係が明らかになり、カプセル化のOOP原則に違反します。

このトレードオフを特定することは正しいですか?この問題にどのように対処しますか?

以下の自分の質問に対する私の回答も参照してください。


7
これは非常に賢明な質問です。dho– dfa '06
06/17

5
この質問に答えるには、最初にカプセル化の意味についての議論が必要です。;)
ジェフスターナル

2
カプセル化はインターフェイスによって維持されます。オブジェクトの本質的な特性を公開し、依存関係などの詳細を隠します。これにより、より柔軟な構成を提供するために、クラスをある程度「開く」ことができます。
Lawrence Wagerfield、2011

回答:


62

この問題には、興味深いと思われる別の見方があります。

IoC /依存性注入を使用する場合、OOPの概念は使用しません。確かに、私たちはオブジェクト指向言語を「ホスト」として使用していますが、IoCの背後にある考え方は、オブジェクト指向ではなくコンポーネント指向のソフトウェアエンジニアリングに由来しています。

コンポーネントソフトウェアは、依存関係の管理に関するものです。一般的な使用例は、.NETのアセンブリメカニズムです。各アセンブリは、参照するアセンブリのリストを公開します。これにより、実行中のアプリケーションに必要な部分をまとめて(そして検証して)より簡単になります。

IoCを介してOOプログラムに同様の手法を適用することにより、プログラムの構成と保守を容易にすることを目指しています。依存関係の公開(コンストラクターのパラメーターなど)は、これの重要な部分です。コンポーネント/サービス指向の世界と同様に、カプセル化は実際には適用されません。詳細を漏らす「実装タイプ」はありません。

残念ながら、現在のところ私たちの言語は、きめの細かいオブジェクト指向の概念を、よりきめの細かいコンポーネント指向の概念から分離していません。そのため、これはあなたが心に留めておかなければならない違いです:)


18
カプセル化は、単なる凝った用語ではありません。それは本当の利点を備えた現実のものであり、プログラムを「コンポーネント指向」と「オブジェクト指向」のどちらであると考えても問題ではありません。カプセル化は、オブジェクト/コンポーネント/サービス/何でも予期しない方法で変更されないように保護するためのものであり、IoCはこの保護の一部を取り除きます。そのため、トレードオフがあります。
Ron Inbar 2015年

1
コンストラクタを通じて提供される引数は、オブジェクトが「変更」される可能性のある予想される方法の領域に含まます。それらは明示的に公開され、それらの周りの不変条件が適用されます。情報の非表示は、あなたが言及しているプラ​​イバシーの種類、@ RonInbarに適した用語であり、必ずしも有益であるとは限りません(パスタの絡み合いが難しくなります;-))。
Nicholas Blumhardt

2
OOPの重要な点は、パスタのもつれが個別のクラスに分けられ、変更したい特定のクラスの動作である場合にのみ、それをいじる必要があるということです(これがOOPが複雑さを軽減する方法です)。クラス(またはモジュール)は、その内部をカプセル化しながら、便利なパブリックインターフェイスを公開します(これが、OOPが再利用を促進する方法です)。インターフェースを介して依存関係を公開するクラスは、クライアントを複雑にし、結果として再利用性が低下します。また、本質的に壊れやすくなっています。
ニュートリノ2017年

1
どちらにしても、DIはOOPの最も有益な利点のいくつかを真剣に損なうように思われ、実際にそれを解決する方法で使用されていることに気付いたという状況にまだ遭遇していません既存の問題。
ニュートリノ

1
問題を解決する別の方法を次に示します。.NETアセンブリが「カプセル化」を選択し、依存する他のアセンブリを宣言していない場合を想像してください。それはクレイジーな状況で、ドキュメントを読んで、それをロードした後に何かがうまくいくことを望んでいるだけです。そのレベルで依存関係を宣言すると、自動化ツールでアプリの大規模な構成を処理できます。類推を見るには目を細める必要がありますが、同様の力がコンポーネントレベルで適用されます。トレードオフがあり、いつものようにYMMV :-)
Nicholas Blumhardt

29

これは良い質問です。ただし、オブジェクトが依存関係を満たすためには、ある時点で、最も純粋な形式のカプセル化に違反する必要があります。依存関係の一部のプロバイダーは、問題のオブジェクトにが必要であることを知っている必要がFooあり、プロバイダーはFooオブジェクトにを提供する方法を持っている必要があります。

古典的には、後者のケースは、あなたが言うように、コンストラクター引数またはセッターメソッドを通じて処理されます。ただし、これは必ずしも正しいとは限りません。たとえば、JavaのSpring DIフレームワークの最新バージョンでは、プライベートフィールドに注釈を付けることができます(例:を使用@Autowired)。依存関係は、リフレクションを介して設定されます。クラスのパブリックメソッド/コンストラクタ。これはあなたが探していた種類の解決策かもしれません。

そうは言っても、コンストラクターの注入がそれほど問題になるとは思いません。オブジェクトは、構築後は完全に有効である必要があると常に感じていました。つまり、役割を実行するために必要なもの(つまり、有効な状態)は、とにかくコンストラクタを通じて提供する必要があります。コラボレーターの動作を必要とするオブジェクトがある場合、コンストラクターがこの要件を公にアドバタイズし、クラスの新しいインスタンスが作成されたときに確実に満たされるようにすることは私には問題ないようです。

理想的には、オブジェクトを処理するときは、とにかくインターフェースを介してオブジェクトとやり取りします。これを行う(およびDIを介して依存関係を配線する)ほど、実際にコンストラクターを扱う必要が少なくなります。理想的な状況では、コードはクラスの具体的なインスタンスを処理したり作成したりすることはありません。そのためIFoo、コンストラクターがFooImplその仕事をする必要があることを示すことを心配することなく、実際にはFooImplの存在を意識することなく、DIを介して取得されます。この観点から、カプセル化は完璧です。

これは当然の意見ですが、私の心にDIは必ずしもカプセル化に違反しているわけではなく、実際、内部構造に関する必要なすべての知識を1か所に集中させることで、カプセル化を助けることができます。これはそれ自体が良いことであるだけでなく、この場所が自分のコードベースの外にあるので、クラスの依存関係を知る必要のないコードを書くことができます。


2
良い点。プライベートフィールドで@Autowiredを使用しないことをお勧めします。クラスをテストするのが難しくなります。モックまたはスタブをどのように注入しますか?
lumpynose 2009年

4
同意しません。DIはカプセル化に違反しており、これは回避できます。たとえば、ServiceLocatorを使用すると、クライアントクラスについて何も知る必要がないことは明らかです。Foo依存関係の実装について知る必要があるだけです。しかし、ほとんどの場合、最良のことは単に「新しい」演算子を使用することです。
–Rogério

5
@Rogerio-間違いなく、DIフレームワークは、あなたが説明するServiceLocatorとまったく同じように動作します。クライアントはFoo実装について何も特定しておらず、DIツールはクライアントについて何も特定していません。また、正確な実装クラスだけでなく、それが必要とするすべての依存関係の正確なクラスインスタンスも知る必要があるため、「新規」を使用するとカプセル化違反が大幅に悪化します。
アンジェイドイル

4
"new"を使用してヘルパークラスをインスタンス化すると、ヘルパークラスが公開されないことも多く、カプセル化が促進されます。DIの代替案は、ヘルパークラスをパブリックにして、パブリックコンストラクターまたはセッターをクライアントクラスに追加することです。どちらの変更も、元のヘルパークラスによって提供されたカプセル化を解除します。
ホジェリオ

1
「それは良い質問です。しかし、ある時点で、オブジェクトが依存関係を満たすために、最も純粋な形式のカプセル化に違反する必要があります。」返信の創設の前提は、単に正しくありません。@Rogérioは依存関係を内部的に更新することを述べているため、オブジェクトが内部的に自身の依存関係を飽和させる他のメソッドは、カプセル化に違反しません。
ニュートリノ2017年

17

これにより、注入される依存関係が明らかになり、カプセル化のOOP原則に違反します。

まあ、率直に言って、すべてがカプセル化に違反しています。:)それはよく扱われなければならない一種の優しい原則です。

それで、何がカプセル化に違反していますか?

継承は行います。

「継承はサブクラスをその親の実装の詳細に公開するため、しばしば「継承はカプセル化を壊す」と言われています。」(ギャングオブフォー1995:19)

アスペクト指向プログラミング はそうです。たとえば、onMethodCall()コールバックを登録すると、通常のメソッド評価にコードを挿入し、奇妙な副作用などを追加する絶好の機会が得られます。

C ++のフレンド宣言はそうです。

Rubyのクラス拡張はそうします。文字列クラスが完全に定義された後のどこかに文字列メソッドを再定義するだけです。

まあ、多くのものはそうします。

カプセル化は良い重要な原則です。しかし、それだけではありません。

switch (principle)
{
      case encapsulation:
           if (there_is_a_reason)
      break!
}

3
「これらは私の原則であり、あなたがそれらを好まないのであれば...まあ、私は他にもいます。」(グルーチョマルクス)
Ron Inbar 2015年

2
これは一種のポイントだと思います。これは、注入とカプセル化の依存関係です。したがって、依存性のある注射は、大きなメリットが得られる場所でのみ使用してください。DIの悪名を呼ぶのは、あらゆる場所のDIです
Richard Tingle

この答えが何を言おうとしているのかわからない... DIを実行するときにカプセル化に違反しても大丈夫か、とにかく違反されるため「常に大丈夫」か、または単にDI カプセル化に違反する理由かもしれないか?もう1つの注意事項として、最近では、依存関係を注入するためにパブリックコンストラクターやプロパティに依存する必要がなくなりました。代わりに、アノテーションが付けられたプライベートフィールドに注入できます。これは、より単純で(コードが少なく)、カプセル化を保持します。したがって、両方の原則を同時に利用できます。
ホジェリオ

継承は原則としてカプセル化に違反しませんが、親クラスが不適切に記述されている場合は違反する可能性があります。あなたが提起する他のポイントは、かなり縁のあるプログラミングパラダイムと、アーキテクチャやデザインとはほとんど関係のないいくつかの言語機能です。
ニュートリノ2017年

13

はい、DIはカプセル化(「情報隠蔽」とも呼ばれます)に違反しています。

しかし、実際の問題は、開発者がKISS(Keep It Short and Simple)およびYAGNI(You Ai n't Gonna Need It)の原則に違反する言い訳としてそれを使用するときに発生します。

個人的には、私はシンプルで効果的なソリューションを好みます。私は主に「新しい」演算子を使用して、必要なときにいつでもどこでもステートフルな依存関係をインスタンス化します。シンプルで、カプセル化されており、理解しやすく、テストも簡単です。では、なぜでしょうか。


先を考えることはひどいことではありませんが、特にそれを必要としないのであれば、Keep It Simple、Stupidに同意します。開発者がサイクルを浪費しているのを見てきました。開発者があまりにも未来を証明できないものを設計しすぎており、その直感に基づいており、既知の/疑われるビジネス要件でさえないからです。
Jacob McKay 2016年

5

優れた依存性注入コンテナー/システムは、コンストラクター注入を可能にします。依存オブジェクトはカプセル化され、公開する必要はまったくありません。さらに、DPシステムを使用することで、オブジェクトの構築方法の詳細を、コードがどれも「認識」することさえありません。この場合、カプセル化はさらに多くなります。コードのほぼすべてが、カプセル化されたオブジェクトの知識から保護されるだけでなく、オブジェクトの構築にも関与しないためです。

ここで、作成したオブジェクトが独自のカプセル化されたオブジェクトを作成する場合(おそらくコンストラクター内)と比較していると仮定します。DPについての私の理解は、この責任をオブジェクトから取り除き、他の誰かに与えたいということです。そのために、この場合のDPコンテナーである「他の誰か」は、カプセル化に「違反」する親密な知識を持っています。利点は、オブジェクト自体からその知識を引き出すことです。誰かが持っている必要があります。アプリケーションの残りの部分はしません。

私はそれをこのように考えます:ディペンデンシーインジェクションコンテナー/システムはカプセル化に違反していますが、コードは違反していません。実際、コードはかつてないほど「カプセル化」されています。


3
クライアントオブジェクトが依存関係を直接インスタンス化できる状況がある場合は、なぜそうしないのですか?これは間違いなく最も簡単なことであり、必ずしもテスト容易性を低下させるわけではありません。単純化とカプセル化の向上に加えて、これにより、ステートレスシングルトンの代わりにステートフルオブジェクトを簡単に作成できます。
–Rogério

1
@Rogérioの発言に加えて、効率も大幅に向上する可能性があります。世界の歴史の中でこれまでに作成されたすべてのクラスが、所有するオブジェクトの存続期間全体にわたってその依存関係のすべてをインスタンス化する必要があったわけではありません。DIを使用するオブジェクトは、自身の依存関係の最も基本的な制御、つまりその存続期間を失います。
ニュートリノ2017年

5

Jeff Sternalが質問へのコメントで指摘したように、答えはカプセル化の定義方法に完全に依存しています。

カプセル化の意味については、2つの主要な陣営があるようです。

  1. オブジェクトに関連するものはすべて、オブジェクトのメソッドです。だから、FileオブジェクトがへのメソッドがありSavePrintDisplayModifyText、など
  2. オブジェクトはそれ自体の小さな世界であり、外部の動作に依存しません。

これら2つの定義は、互いに直接矛盾しています。Fileオブジェクトがそれ自体を印刷できる場合、それはプリンターの動作に大きく依存します。一方、それが印刷できるもの(またはそのようなインターフェース)を知っているだけの場合IFilePrinterFileオブジェクトは印刷について何も知る必要がないため、オブジェクトを操作することでオブジェクトへの依存関係が少なくなります。

したがって、最初の定義を使用すると、依存性注入によりカプセル化が解除されます。しかし、率直に言って、私が最初の定義が好きかどうかはわかりません-それは明らかにスケーリングしません(そうした場合、MS Wordは1つの大きなクラスになります)。

一方、カプセル化の2番目の定義を使用している場合、依存性注入はほぼ必須です。


私は最初の定義についてあなたに同意します。また、間違いなくプログラミングの根本的な罪の1つであり、スケーリングできない理由の1つであるSoCにも違反しています。
マーカススタッド

4

カプセル化に違反しません。あなたはコラボレーターを提供していますが、クラスはそれがどのように使用されるかを決定します。Tellに従っている限り、問題はありません。私はコンストラクター注入が望ましいと思いますが、セッターは賢い限り問題ありません。つまり、クラスが表す不変条件を維持するためのロジックが含まれています。


1
それは...そうではないのですか?ロガーがあり、ロガーを必要とするクラスがある場合、そのクラスにロガーを渡してもカプセル化に違反しません。そして、それがすべての依存性注入です。
jrockway 2009年

3
カプセル化について誤解していると思います。たとえば、単純な日付クラスを取ります。内部的には、日、月、年のインスタンス変数がある場合があります。これらがロジックのない単純なセッターとして公開された場合、月を2に、日を31に設定するようなことを行うことができるため、カプセル化が破られます。 。また、後者のバージョンでは、ストレージを1970年1月1日からの日数に変更することもできます。また、日/月/年のメソッドを適切に書き直せば、インターフェースを使用する人はこれを意識する必要がありません。
Jason Watkins、

2
DIは確実にカプセル化/情報の隠蔽に違反します。プライベート内部依存関係をクラスのパブリックインターフェイスで公開されるものに変換すると、定義により、その依存関係のカプセル化が解除されます。
–Rogério

2
カプセル化がDIによって損なわれると思う具体的な例があります。私はDBから「fooデータ」を取得するFooProviderと、それをキャッシュしてプロバイダーの上でデータを計算するFooManagerを持っています。私はコードのコンシューマーに誤ってFooProviderのデータを要求しましたが、FooManagerのみを認識できるようにカプセル化してしまいました。これは基本的に私の元の質問のトリガーです。
urig

1
@Rogerio:コンストラクタはコンポジションルートでのみ使用されるため、コンストラクタはパブリックインターフェイスの一部ではないと主張します。したがって、依存関係はコンポジションルートによってのみ「認識」されます。構成ルートの単一の責任は、これらの依存関係を相互に関連付けることです。そのため、コンストラクター注入を使用しても、カプセル化は解除されません。
ジェイサリバン

4

これは賛成投票に似ていますが、私は大声で考えたいです-おそらく他の人も同じように物事を見るでしょう。

  • 従来のOOは、コンストラクターを使用して、クラスのコンシューマー用のパブリックな「初期化」コントラクトを定義します(すべての実装の詳細を非表示、別名はカプセル化)。このコントラクトにより、インスタンス化後、すぐに使用できるオブジェクト(つまり、ユーザーが記憶する(つまり、忘れる)追加の初期化手順がない)ことが保証されます。

  • (コンストラクター)DIは、このパブリックコンストラクターインターフェースを介して実装の詳細を明確にすることにより、カプセル化を確実に破壊します。ユーザーの初期化コントラクトの定義を担当するパブリックコンストラクターを検討している限り、カプセル化の恐ろしい違反が発生しています。

理論的な例:

クラスFooには4つのメソッドがあり、初期化に整数が必要なため、そのコンストラクターはFoo(int size)のようになり、Fooが機能するためには、インスタンス化時にサイズを指定する必要があることをクラスFooのユーザーにすぐにわかります。

このFooの特定の実装では、その作業を行うためにIWidgetも必要になる可能性があるとします。この依存関係のコンストラクター注入により、Foo(int size、IWidget widget)のようなコンストラクターが作成されます。

これについて私をいらいらさせているのは、初期化データと依存関係をブレンドするコンストラクターがあることです。1つの入力はクラス(size)のユーザーに関係し、もう1つの入力はユーザーを混乱させるだけの内部依存関係であり、実装です。詳細(ウィジェット)。

サイズパラメータは依存関係ではありません。これは、インスタンスごとの初期化値です。IoCは、外部依存関係(ウィジェットなど)に対しては危険ですが、内部状態の初期化に対しては危険です。

さらに悪いことに、このクラスの4つのメソッドのうち2つだけにウィジェットが必要な場合はどうでしょう。ウィジェットが使用されていない場合でも、インスタンス化のオーバーヘッドが発生する可能性があります。

これを妥協/調整する方法は?

1つのアプローチは、操作コントラクトを定義するためにインターフェースに排他的に切り替えることです。ユーザーによるコンストラクタの使用を廃止します。一貫性を保つために、すべてのオブジェクトはインターフェースのみを介してアクセスし、何らかの形式のリゾルバー(IOC / DIコンテナーなど)を介してのみインスタンス化する必要があります。コンテナのみがインスタンスを作成します。

これでウィジェットの依存関係が処理されますが、Fooインターフェースで個別の初期化メソッドを使用せずに「サイズ」を初期化するにはどうすればよいでしょうか。このソリューションを使用すると、インスタンスを取得するまでにFooのインスタンスが完全に初期化されるようにする機能が失われました。残念なのは、コンストラクター注入のアイデアと単純さが本当に好きだからです。

初期化が外部依存関係だけではない場合、このDIの世界で初期化を保証するにはどうすればよいですか?


更新:Unity 2.0がresolve()の間の依存関係のIoCの通常のメカニズムを使用しながら、コンストラクターパラメーター(状態初期化子など)の値の提供をサポートしていることに気づきました。おそらく他のコンテナもこれをサポートしていますか?これにより、1つのコンストラクターで状態の初期化とDIを混合するという技術的な問題が解決されますが、それでもカプセル化に違反しています!
shawnT 2010

うん。私もこの質問をしました。私も2つの良いもの(DIとカプセル化)が、一方を犠牲にしてもう一方が生まれると感じているからです。ところで、あなたの例では、4つのメソッドのうち2つだけがIWidgetを必要としていますが、それは他の2つが別のコンポーネントIMHOに属していることを示します。
urig

3

純粋なカプセル化は、決して達成できない理想です。すべての依存関係が隠されている場合、DIはまったく必要ありません。このように考えてください。たとえば、車のオブジェクトの速度の整数値など、オブジェクト内で内部化できるプライベートな値が本当にある場合、外部依存関係はなく、その依存関係を反転または注入する必要はありません。純粋にプライベート関数によって操作されるこれらの種類の内部状態値は、常にカプセル化したいものです。

しかし、特定の種類のエンジンオブジェクトを必要とする自動車を構築している場合は、外部依存関係があります。そのエンジン(たとえば、新しいGMOverHeadCamEngine()など)をcarオブジェクトのコンストラクター内で内部的にインスタンス化してカプセル化を保持しながら、具体的なクラスGMOverHeadCamEngineへのはるかに巧妙な結合を作成するか、または注入してCarオブジェクトが動作できるようにすることができます例えば、具体的な依存関係のないインターフェースIEngineに不可知的に(そしてはるかに堅牢に)。これを達成するためにIOCコンテナーまたは単純なDIのどちらを使用するかは重要ではありません。重要なのは、さまざまな種類のエンジンをそれらに結合せずに使用できるCarがあり、コードベースがより柔軟になり、副作用が起こりにくい。

DIはカプセル化の違反ではなく、事実上すべてのOOPプロジェクト内で当然のことながらカプセル化が必然的に失敗した場合にカップリングを最小限に抑える方法です。依存関係をインターフェースに外部から注入すると、カップリングの副作用が最小限に抑えられ、クラスは実装にとらわれないままになります。


3

それは、依存関係が実際に実装の詳細なのか、クライアントが何らかの方法で知りたい/知りたいと思うものなのかによって異なります。関連することの1つは、クラスが対象としている抽象化のレベルです。ここではいくつかの例を示します。

内部でキャッシュを使用して呼び出しを高速化するメソッドがある場合、キャッシュオブジェクトはシングルトンか何かであり、注入されるべきでありませ。キャッシュがまったく使用されていないという事実は、クラスのクライアントが気にする必要のない実装の詳細です。

クラスがデータのストリームを出力する必要がある場合、クラスが配列、ファイル、または他の誰かがデータを送信したい場所に結果を簡単に出力できるように、出力ストリームを注入することはおそらく意味があります。

灰色の領域で、モンテカルロシミュレーションを行うクラスがあるとします。ランダム性のソースが必要です。一方で、これが必要であるという事実は、クライアントが実際にランダム性がどこから来るのかを正確に気にしないという点で、実装の詳細です。一方、実際の乱数ジェネレータは、クライアントが制御したいランダム性の程度、速度などの間でトレードオフを行うため、クライアントはシーディングを制御して反復可能な動作を実現したい場合があるため、注入は意味があります。この場合、乱数ジェネレーターを指定せずにクラスを作成する方法を提供し、スレッドローカルのシングルトンをデフォルトとして使用することをお勧めします。より細かい制御が必要になった場合は、ランダム性のソースを注入できる別のコンストラクターを提供します。


2

私はシンプルに生きています。ドメインクラスにIOC / Dependecyインジェクションを適用しても、関係を記述した外部xmlファイルを作成してコードをメインにするのがはるかに困難になる以外は、改善はありません。EJB 1.0 / 2.0やstruts 1.1などの多くのテクノロジーは、XMLに入力するものを減らして元に戻し、注釈などとしてコードに挿入してみます。したがって、開発するすべてのクラスにIOCを適用すると、コードが意味をなさなくなります。

依存オブジェクトがコンパイル時に作成する準備ができていない場合、IOCには利点があります。これは、インフラストラクチャの抽象的なレベルのアーキテクチャコンポーネントのほとんどで発生し、さまざまなシナリオで機能する必要がある共通の基本フレームワークを確立しようとしました。これらの場所では、IOCの方が理にかなっています。それでも、これはコードをより単純化/保守可能にするものではありません。

他のすべてのテクノロジーと同様に、これにもPROとCONがあります。私の心配は、最新のテクノロジーを、コンテキストの使用状況に関係なくすべての場所に実装することです。


2

カプセル化は、クラスがオブジェクトを作成する責任(実装の詳細の知識が必要)とクラスを使用する(これらの詳細の知識を必要としない)場合にのみ機能します。その理由を説明しますが、最初に簡単な車の解析を行います。

1971年の古いコンビを運転していたとき、アクセルを押すことができ、それは(少し)速くなりました。私はその理由を知る必要はありませんでしたが、工場でKombiを作った人たちは正確に理由を知っていました。

しかし、コーディングに戻ります。 カプセル化は、「その実装を使用して何かから実装の詳細を隠す」ことです。クラスのユーザーが知らなくても実装の詳細が変わる可能性があるため、カプセル化は良いことです。

依存関係注入を使用する場合、コンストラクター注入を使用して、サービスタイプオブジェクトを作成します(状態をモデル化するエンティティ/値オブジェクトとは対照的)。サービスタイプオブジェクトのメンバー変数は、漏出してはならない実装の詳細を表します。たとえば、ソケットポート番号、データベース資格情報、暗号化を実行するために呼び出す別のクラス、キャッシュなど。

コンストラクタは、クラスを最初に作成されるときに関連しています。これは、DIコンテナ(またはファクトリ)がすべてのサービスオブジェクトを相互にワイヤリングする際の構築フェーズ中に発生します。DIコンテナーは、実装の詳細のみを認識します。Kombi工場のスタッフがスパークプラグについて知っているように、実装の詳細についてもすべて知っています。

実行時に、作成されたサービスオブジェクトは、実際の作業を行うためにaponと呼ばれます。現時点では、オブジェクトの呼び出し元は実装の詳細を何も知りません。

それは私が私のコンビをビーチに運転することです。

次に、カプセル化に戻ります。実装の詳細が変更された場合、実行時にその実装を使用するクラスを変更する必要はありません。カプセル化は壊れていません。

私は新しい車をビーチまで運転することもできます。カプセル化は壊れていません。

実装の詳細が変更された場合、DIコンテナー(またはファクトリー)を変更する必要があります。そもそも、実装の詳細をファクトリーから隠そうとしていませんでした。


どのようにあなたの工場をユニットテストしますか?つまり、クライアントは、稼働中の車を入手するためにファクトリーについて知る必要があります。つまり、システム内の他のオブジェクトごとにファクトリーが必要になります。
Rodrigo Ruiz

2

この問題にもう少し取り組んだ結果、依存性注入は(現時点では)カプセル化にある程度違反していると私は考えています。ただし、誤解しないでください。ほとんどの場合、依存関係の注入を使用することはトレードオフの価値があると思います。

DIがカプセル化に違反する理由は、作業中のコンポーネントが「外部」パーティー(顧客向けのライブラリを作成することを考える)に引き渡されるときに明らかになります。

私のコンポーネントがコンストラクター(またはパブリックプロパティ)を介してサブコンポーネントを注入する必要がある場合、保証はありません

「ユーザーがコンポーネントの内部データを無効または不整合な状態に設定することを防ぐ」。

同時にとは言えない

「コンポーネント(その他のソフトウェア)のユーザーは、コンポーネントの機能を知るだけでよく、コンポーネントの機能の詳細に依存することはできません

両方の引用はウィキペディアからです。

具体例を挙げましょう。WCFサービス(基本的にはリモートファサード)への通信を簡素化および非表示にするクライアント側DLLを提供する必要があります。3つの異なるWCFプロキシクラスに依存しているため、DIアプローチを採用すると、コンストラクターを介してそれらを公開する必要があります。これで、隠そうとしている通信層の内部を公開します。

一般的に私はすべてDIです。この特定の(極端な)例では、危険を感じます。


2

DIは非共有オブジェクトのカプセル化に違反しています-期間。共有オブジェクトは、作成されるオブジェクトの外側に存続期間があるため、作成されるオブジェクトに統合する必要があります。作成されるオブジェクトにプライベートなオブジェクトは、作成されたオブジェクトにCOMPOSEDする必要があります。作成されたオブジェクトが破棄されると、作成されたオブジェクトも一緒に使用されます。人体を例にとりましょう。何が構成され、何が集約されるか。DIを使用する場合、人体コンストラクターには100のオブジェクトが含まれます。たとえば、臓器の多くは(潜在的に)交換可能です。しかし、それらはまだ身体に組み込まれています。血液細胞は、体内で毎日作成され(そして破壊され)、外部からの影響(タンパク質以外)を必要としません。したがって、血液細胞は体内で内部的に作成されます-new BloodCell()。

DIの擁護者は、オブジェクトは決してnew演算子を使用してはならないと主張しています。その「純粋な」アプローチは、カプセル化に違反するだけでなく、オブジェクトを作成しているすべての人にとってのリスコフ置換原則にも違反します。


1

私もこの考えに苦労しました。最初は、DIコンテナー(Springなど)を使用してオブジェクトをインスタンス化するための「要件」は、フープを飛び越えているように感じられました。しかし、実際には、それは実際にはフープではありません-必要なオブジェクトを作成するためのもう1つの「公開された」方法にすぎません。確かに、「クラス外」の誰かが必要なものを知っているため、カプセル化は「壊れています」が、それを知っているのはシステムの他の部分ではなく、DIコンテナです。DIは1つのオブジェクトに別のオブジェクトが必要であることを "認識"しているため、特別なことは何も起こりません。

実際、それはさらに良くなります-ファクトリーとリポジトリーに集中することで、DIが関与していることを知る必要すらありません!それは私にふたをカプセル化に戻します。ふew!


1
インスタンス化のチェーン全体をDIが担当している限り、カプセル化が発生することに同意します。依存関係はまだ公開されており、悪用される可能性があるためです。しかし、チェーンのどこかで "上"にいる人がDIを使用せずにオブジェクトをインスタンス化する必要がある場合(多分、それらは "サードパーティ"である)、乱雑になります。彼らはあなたの依存関係にさらされており、それらを悪用したくなるかもしれません。または、彼らは彼らについてまったく知りたくないかもしれません。
urig、2009年

1

PS。依存性注入を提供することで、必ずしもカプセル化を解除する必要はありません。例:

obj.inject_dependency(  factory.get_instance_of_unknown_class(x)  );

クライアントコードはまだ実装の詳細を知りません。


あなたの例ではどのように何かが注入されていますか?(セッター関数の命名を除く)
foo

1

これは単純な考え方かもしれませんが、整数パラメーターを受け取るコンストラクターとパラメーターとしてサービスを受け取るコンストラクターの違いは何ですか?これは、新しいオブジェクトの外部で整数を定義し、それをオブジェクトに供給すると、カプセル化が壊れることを意味しますか?サービスが新しいオブジェクト内でのみ使用されている場合、それがカプセル化を壊す方法はわかりません。

また、なんらかの自動配線機能(たとえば、C#の場合はAutofac)を使用することで、コードが非常にクリーンになります。Autofacビルダーの拡張メソッドを作成することで、依存関係のリストが増えるにつれて長期にわたって維持しなければならなかったであろう、多くのDI構成コードを切り取ることができました。


それは価値対サービスについてではありません。それは制御の逆転についてです-クラスのコンストラクターがクラスをセットアップするかどうか、またはそのサービスがクラスのセットアップを引き継ぐかどうか。そのクラスの実装の詳細を知る必要があるため、メンテナンスと同期を行う別の場所を得ました。
foo

1

少なくともDIがカプセル化を著しく弱めることは自明だと思います。これに加えて、DIの他に考慮すべきいくつかの欠点があります。

  1. コードの再利用が難しくなります。明示的に依存関係を提供する必要なしにクライアントが使用できるモジュールは、クライアントがそのコンポーネントの依存関係を何らかの方法で発見し、何らかの方法でそれらを使用可能にする必要があるモジュールよりも明らかに使いやすいです。たとえば、ASPアプリケーションで使用するために最初に作成されたコンポーネントは、オブジェクトインスタンスにクライアントのhttpリクエストに関連するライフタイムを提供するDIコンテナによって提供される依存関係を期待する場合があります。これは、元のASPアプリケーションと同じ組み込みのDIコンテナーが付属していない別のクライアントで再現するのは簡単ではない場合があります。

  2. コードをより脆弱にする可能性があります。インターフェイス仕様によって提供される依存関係は予期しない方法で実装される可能性があり、静的に解決された具体的な依存関係では不可能なランタイムバグのクラス全体を引き起こします。

  3. それはあなたがそれがどのように動作したいかについてあなたがより少ない選択で終わるかもしれないという意味でコードをより柔軟にすることができます。すべてのクラスが、所有するインスタンスの存続期間全体にわたってすべての依存関係が存在する必要があるわけではありませんが、多くのDI実装では、他に選択肢はありません。

そのことを念頭に置くと、最も重要な質問は、「特定の依存関係を外部で指定する必要があるのでしょうか?」になると思います。実際には、テストをサポートするために依存関係を外部から提供する必要があることはめったにありません。

依存関係を実際に外部から提供する必要がある場合、通常、オブジェクト間の関係は内部の依存関係ではなくコラボレーションであることを示唆しています。この場合、適切な目標は、あるクラスを他のクラスの内部にカプセル化するのではなく、クラスをカプセル化することです。 。

私の経験では、DIの使用に関する主な問題は、DIが組み込まれたアプリケーションフレームワークから始めるか、コードベースにDIサポートを追加するかということです。何らかの理由で、正しいはずのDIサポートがあるので、人々はそれを推測しますすべてをインスタンス化する方法。彼らは「この依存関係を外部で指定する必要があるか」という質問をすることさえありません。さらに悪いことに、彼らはまたのためのDIサポートを使用して他の皆を強制しようとし始めるすべてをあまりにも。

この結果、コードベースが容赦なく進化し始め、コードベースに何かのインスタンスを作成するには、一連の鈍いDIコンテナー構成が必要になり、どのようにデバッグするかは、方法を特定するための余分なワークロードがあるためです。そして何がインスタンス化されたか。

質問に対する私の答えはこれです。DIを使用して、DIが解決する実際の問題を特定できます。これは、他の方法では簡単に解決できません。


0

極端に言えば、DIはカプセル化に違反する可能性があることに同意します。通常、DIは真にカプセル化されなかった依存関係を公開します。以下は、MiškoHeveryから借りた単純化された例です。シングルトンは病理学的嘘つきです:

CreditCardテストから始めて、単純な単体テストを作成します。

@Test
public void creditCard_Charge()
{
    CreditCard c = new CreditCard("1234 5678 9012 3456", 5, 2008);
    c.charge(100);
}

来月は100ドルの請求書が届きます。なぜ請求されたのですか?単体テストは本番データベースに影響を与えました。内部的には、CreditCardはを呼び出しますDatabase.getInstance()。CreditCardをリファクタリングDatabaseInterfaceしてコンストラクタでaを取得すると、依存関係があることが明らかになります。しかし、CreditCardクラスは外部から見える副作用を引き起こすため、依存関係がカプセル化されていないことは間違いないでしょう。リファクタリングせずにCreditCardをテストする場合は、依存関係を確認できます。

@Before
public void setUp()
{
    Database.setInstance(new MockDatabase());
}

@After
public void tearDown()
{
    Database.resetInstance();
}

データベースを依存関係として公開することでカプセル化が減少するかどうかを心配する価値はないと思います。これは優れた設計だからです。すべてのDIの決定がそれほど単純ではありません。ただし、他のどの回答も反例を示していません。


1
単体テストは通常​​、クラスの作成者によって作成されるため、技術的な観点から、テストケースの依存関係を詳しく説明しても問題ありません。PayPalなどのWeb APIを使用するように後でクレジットカードクラスが変更された場合、ユーザーがDIされた場合、ユーザーはすべてを変更する必要があります。単体テストは通常​​、テスト対象の詳細な知識で行われます(それが全体のポイントではありませんか?)、テストは典型的な例よりも例外的なものだと思います。
kizzx2

1
DIのポイントは、説明した変更を避けることです。Web APIからPayPalに切り替えた場合、MockPaymentServiceを使用し、CreditCardがPaymentServiceで構築されるため、ほとんどのテストを変更しません。CreditCardと実際のPaymentServiceの間の実際のやり取りを調べるテストはほんの一握りなので、将来の変更は非常に孤立しています。より深い依存関係グラフ(CreditCardに依存するクラスなど)の利点はさらに大きくなります。
クレイグP.モトリン

@クレイグp。Motlin CreditCardクラスの外部にあるものを変更することなく、オブジェクトをWebAPIからPayPalに変更するにはどうすればよいですか?
Ian Boyd

@Ian私は、CreditCardをリファクタリングして、DatabaseInterfacesの実装の変更からシールドするコンストラクタでDatabaseInterfaceを取得する必要があると述べました。多分それはさらに一般的である必要があり、WebAPIが別の実装になる可能性のあるStorageInterfaceを取る必要があります。PayPalはCreditCardの代替手段であるため、レベルが間違っています。PayPalとCreditCardはどちらもPaymentInterfaceを実装して、アプリケーションの他の部分をこの例の外部から保護できます。
Craig P. Motlin

@ kizzx2言い換えれば、クレジットカードがPayPalを使用する必要があると言うのはナンセンスです。彼らは現実の代替手段です。
クレイグP.モトリン

0

範囲の問題だと思います。カプセル化を定義するとき(方法を知らせない)、カプセル化された機能とは何かを定義する必要があります。

  1. そのままのクラス:カプセル化しているのは、クラスの唯一の責任です。どうやってそれを知っているか。たとえば、並べ替え。注文のためにコンパレーターを注入する場合、たとえば、クライアントはそれがカプセル化されたものの一部ではありません:クイックソート。

  2. 構成された機能:すぐに使用できる機能を提供する場合は、QuickSortクラスではなく、コンパレータで構成されたQuickSortクラスのインスタンスを提供します。その場合、作成と構成を担当するコードは、ユーザーコードから非表示にする必要があります。そしてそれがカプセル化です。

クラスをプログラミングする場合、つまり、単一の責任をクラスに実装する場合は、オプション1を使用します。

アプリケーションをプログラミングしているときは、有用な具体的な作業を行う何かを作成してから、オプション2を繰り返し使用します。

これは構成されたインスタンスの実装です。

<bean id="clientSorter" class="QuickSort">
   <property name="comparator">
      <bean class="ClientComparator"/>
   </property>
</bean>

これは、他のいくつかのクライアントコードでの使用方法です。

<bean id="clientService" class"...">
   <property name="sorter" ref="clientSorter"/>
</bean>

実装を変更しても(clientSorterBean定義を変更しても)クライアントの使用を中断しないため、カプセル化されています。たぶん、すべてをまとめて記述したxmlファイルを使用すると、すべての詳細が表示されます。しかし、私を信じて、クライアントコード(ClientServiceそのソーターについて何も知りません。


0

これEncapsulationは、いくぶん視点に依存していることに言及する価値があるでしょう。

public class A { 
    private B b;

    public A() {
        this.b = new B();
    }
}


public class A { 
    private B b;

    public A(B b) {
        this.b = b;
    }
}

Aクラスで作業している誰かの観点から見ると、2番目の例 Aでは、this.b

一方、DIなし

new A()

new A(new B())

このコードを見る人Aは、2番目の例のの性質について詳しく知っています。

DIを使用すると、少なくとも漏洩したすべての知識が1か所に集まります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.