依存関係反転の原則を適用しない場合


43

現在、SOLIDを把握しようとしています。したがって、依存関係反転の原則は、任意の2つのクラスが直接ではなく、インターフェースを介して通信することを意味します。例:class Aメソッドがあり、typeのオブジェクトへのポインターを予期するclass B場合、このメソッドは実際にtypeのオブジェクトを予期する必要がありますabstract base class of B。これは、オープン/クローズにも役立ちます。

私が正しく理解していれば、私の質問は、これをすべてのクラスの相互作用に適用するのが良い習慣である、レイヤーの観点から考えてみるべきですか?

私が懐疑的である理由は、この原則に従うためにいくらかの代価を払っているからです。たとえば、featureを実装する必要がありますZ。分析後Z、機能はABおよびで構成されていると結論付けCます。私が作成ファサードクラスはZ、それは、インタフェースを介して、クラスを使用していますABC。私は実装をコーディングを開始し、いくつかの点で私は、そのタスクを実現するZ実際の機能で構成されABそしてD。次に、Cインターフェイス、Cクラスプロトタイプを破棄し、Dインターフェイスとクラスを個別に記述する必要があります。インターフェイスがなければ、クラスのみを置き換える必要があります。

つまり、何かを変更するには、1。呼び出し元2.インターフェイス3.宣言4.実装を変更する必要があります。Python直接結合実装では、実装のみを変更する必要があります。


13
依存関係の反転は単なる手法であるため、必要な場合にのみ適用する必要があります...適用できる範囲に制限はないため、どこにでも適用すると、ゴミになります:他のすべての状況と同様に固有のテクニック。
フランクヒルマン

簡単に言うと、一部のソフトウェア設計原則の適用は、要件が変化したときに容赦なくリファクタリングできることに依存しています。これらのうち、インターフェイス部分は設計の契約上の不変条件を最もよく捉えると考えられていますが、ソースコード(実装)はより頻繁な変更に耐えることが期待されています。
ルワン

@rwongインターフェイスは、契約不変条件をサポートする言語を使用する場合にのみ、契約不変条件をキャプチャします。共通言語(Java、C#)では、インターフェースは単にAPIシグネチャのセットです。余分なインターフェイスを追加しても、デザインが劣化するだけです。
フランクヒルマン

私はあなたがそれを間違って理解したと言うでしょう。DIPは、「高レベル」コンポーネントから「低レベル」コンポーネントへのコンパイル時の依存関係を回避し、低レベルに異なる実装を使用する他のコンテキストで高レベルコンポーネントを再利用できるようにすることです。レベルのコンポーネント。これは、低レベルのコンポーネントによって実装される高レベルで抽象型を作成することにより行われます。したがって、高レベルコンポーネントと低レベルコンポーネントの両方がこの抽象化に依存します。最終的に、高レベルコンポーネントと低レベルコンポーネントはインターフェイスを介して通信しますが、これはDIPの本質ではありません
ロジェリオ

回答:


87

多くの漫画やその他のメディアでは、善と悪の勢力は、キャラクターの肩に座っている天使と悪魔によって描かれていることがよくあります。ここでの話では、善悪の代わりに片方の肩にソリッドがあり、もう片方にYAGNI(あなたは必要ないでしょう!)があります。

SOLIDの原則を最大限に活用することは、巨大で複雑な、超構成可能なエンタープライズシステムに最適です。より小規模な、またはより具体的なシステムの場合、すべてを途方もなく柔軟にすることは適切ではありません。物事を抽象化するために費やす時間が有益であることが証明されないからです。

具体的なクラスの代わりにインターフェイスを渡すと、たとえば、ファイルからの読み取りをネットワークストリームに簡単に交換できることを意味する場合があります。しかし、ソフトウェアプロジェクトの偉大な量のために、柔軟性のようなものはただではない、これまで必要とされようとして、そしてあなたにもちょうど、具体的なファイルのクラスを渡し、その日それを呼び出すと、あなたの脳細胞を惜しまかもしれません。

ソフトウェア開発の技術の一部は、時間が経つにつれて変化する可能性のあるものとそうでないものをよく理解していることです。変更される可能性のあるものについては、インターフェイスおよびその他のSOLIDの概念を使用してください。そうでないものについては、YAGNIを使用して、具体的な型を渡し、ファクトリクラスを忘れ、すべてのランタイムフックアップと構成などを忘れ、SOLID抽象化の多くを忘れます。私の経験では、YAGNIのアプローチはそうでない場合よりもはるかに頻繁に正しいことが証明されています。


19
SOLIDを初めて紹介したのは、約15年前に私たちが構築していた新しいシステムについてでした。私たちは皆、クールエイドマンを飲みました。誰か YAGNIのように聞こえる何かに言及した場合、私たちは「Pfffft ... plebeian」のようでした。私は、このシステムが次の10年間で進化するのを見るという名誉(ホラー?)を持っていました。それは、私たちの創始者でさえも、誰も理解できない扱いにくい混乱になりました。建築家はソリッドが大好きです。実際に生計を立てている人はヤグニが大好きです。どちらも完璧ではありませんが、YAGNIは完璧に近いので、何をしているのかわからない場合はデフォルトにする必要があります。:-)
カルフール

11
@NWardええ、私たちはプロジェクトでそれをしました。それにナットをかけた。現在、私たちのテストは、部分的にモックがかかっているため、読み取りや保守が不可能です。その上、依存関係の注入のため、何かを理解しようとするときにコードをナビゲートするのは後部の苦痛です。SOLIDは特効薬ではありません。YAGNIは特効薬ではありません。自動テストは特効薬ではありません。をしているのかを考え、それがあなたの仕事や他の人の助けになるのか、それとも邪魔するのかを決定するという骨の折れる仕事からあなたを救うことはできません
jpmc26

22
ここで多くの反固い感情。SOLIDとYAGNIは、スペクトルの両端ではありません。それらは、チャート上のXおよびY座標のようなものです。優れたシステムには、余分なコード(YAGNI)はほとんどなく、堅実な原則に従っています。
スティーブン

31
Meh、(a)SOLID = enterpriseyであることに同意しません。(b)SOLIDの要点は、私たちが必要とされるものの予測が非常に貧弱になる傾向があるということです。ここで@Stephenに同意する必要があります。YAGNIは、今日明確に記述されていない将来の要件を予測しようとすべきではないと述べています。SOLIDは、デザインが時間とともに進化し、特定の簡単な手法を適用してそれを促進することを期待する必要があると述べています。それらは相互に排他的ではありません。どちらも、変化する要件に適応するための手法です。実際の問題は、不明確な、または非常に遠い要件に合わせて設計しようとすると発生します。
アーロンノート

8
「ファイルからの読み取り値をネットワークストリームに簡単に交換できます」-これは、DIの過度に単純化された説明が人々を迷わせる良い例です。人々は(実際には)「この方法ではを使用するFileので、代わりにIFile、仕事を終える」と考えることがあります。その後、ネットワークストリームを簡単に置き換えることはできません。インターフェイスを過剰に要求しIFile、メソッドには使用しない操作もあり、ソケットには適用されないため、ソケットは実装できませんIFile。DIが特効薬ではないことの1つは、適切な抽象化(インターフェイス)を発明することです:
スティーブジェソップ

11

素人の言葉で:

DIPの適用は簡単で楽しいものです。最初の試行で設計を正しく行わなかっただけでは、DIPを完全に放棄する十分な理由にはなりません。

  • 通常、IDEはそのようなリファクタリングを支援します。一部の実装では、既に実装されているクラスからインターフェイスを抽出することさえできます。
  • 初めてデザインを正しくすることはほとんど不可能です
  • 通常のワークフローでは、開発の最初の段階でインターフェイスを変更して再考する必要があります
  • 開発が進化するにつれて成熟し、インターフェイスを変更する理由が少なくなります
  • 高度な段階では、インターフェース(設計)は成熟し、ほとんど変わりません。
  • その瞬間から、あなたはアプリがスケールアップするために開かれているので、あなたは利益を享受し始めます。

一方、インターフェイスとOODを使用したプログラミングは、時には時代遅れのプログラミング技術に喜びを取り戻すことができます。

複雑さを増すと言う人もいますが、オポサイトは真実だと思います。小さなプロジェクトでも。テスト/モック作成が簡単になります。caseステートメントまたはネストされてifsいる場合、コードが少なくなります。循環的な複雑さを軽減し、新鮮な方法で考えさせます。プログラミングを実世界の設計と製造により似たものにします。


5
OPでどの言語またはIDEが使用されているのかわかりませんが、VS 2013では、インターフェイスに対して作業し、インターフェイスを抽出して実装し、TDDを使用する場合は非常に簡単です。これらの原則を使用して開発するための追加の開発オーバーヘッドはありません。
スティーブンバイエル

質問がDIPに関するものである場合、なぜこの回答がDIについて語っているのですか?DIPは1990年代の概念ですが、DIは2004年のものです。これらは非常に異なっています。
ロジェリオ

1
(私の以前のコメントは別の答えを意味するものでした。無視してください。)「インターフェースへのプログラミング」はDIPよりもはるかに一般的ですが、すべてのクラスに個別のインターフェースを実装することではありません。また、テスト/モックツールに重大な制限がある場合にのみ、「テスト/モック」を簡単にします。
ロジェリオ

@Rogério通常、DIを使用する場合、すべてのクラスが個別のインターフェイスを実装するわけではありません。複数のクラスによって実装される1つのインターフェイスが一般的です。
Tulainsコルドバ

@Rogério私は答えを訂正しました。DIについて言及するたびにDIPを意味しました。
Tulainsコルドバ

9

意味のある場合は、依存関係の反転を使用します。

極端な反例の1つは、多くの言語に含まれる「文字列」クラスです。これは、基本的な文字の配列である基本的な概念を表します。このコアクラスを変更できると仮定すると、内部状態を他のものと交換する必要がないため、ここでDIを使用する意味はありません。

モジュール内で使用されるオブジェクトのグループが他のモジュールに公開されていないか、どこでも再利用されていない場合、おそらくDIを使用するだけの価値はありません。

私の意見では、DIが自動的に使用されるべき場所は2つあります。

  1. 拡張用に設計されたモジュール内。モジュールの全体的な目的がそれを拡張して動作を変更することである場合、DIを最初から焼き付けることは完全に理にかなっています。

  2. コードの再利用を目的としてリファクタリングしているモジュール内。おそらく、何かをするためにクラスをコーディングし、後でリファクタリングを行うと、そのコードを他の場所で活用できるため、そうする必要があることに気づくでしょう。これは、DIおよびその他の拡張性の変更の優れた候補です。

ここでのキーは、複雑さが増すため必要な場所で使用し、技術要件(ポイント1)または定量的コードレビュー(ポイント2)で必要性を測定することです

DIは素晴らしいツールですが、他の*ツールと同様に、使いすぎたり誤用されたりする可能性があります。

*上記のルールの例外:往復のこぎりは、あらゆる仕事に最適なツールです。問題が解決しない場合は、それを削除します。永久に。


3
「あなたの問題」が壁の穴だとしたら?のこぎりはそれを除去しません。悪化するでしょう。;)
メイソンウィーラー

3
@MasonWheelerの強力で楽しい使用法で、「壁の穴」が有用な資産である「戸口」に変わる可能性があります:

1
のこぎりで穴のパッチを作ることはできませんか?
ジェフ

非ユーザー拡張String型を持つことにはいくつかの利点がありますが、型に適切な仮想操作セットがある場合、代替表現が役立つ場合が多くあります(たとえば、サブストリングをの指定部分にコピーしshort[]、部分文字列はASCIIのみを含むか、または含む可能性があります。ASCIIのみを含むと考えられる部分文字列をbyte[]などの指定部分にコピーしてみてください)。
-supercat

1
質問がDIPに関するものである場合、なぜこの回答がDIについて語っているのですか?DIPは1990年代の概念ですが、DIは2004年のものです。これらは非常に異なっています。
ロジェリオ

5

元の質問には、DIPの要点が欠けているように思えます。

私が懐疑的である理由は、この原則に従うためにいくらかの代価を払っているからです。たとえば、機能Zを実装する必要があります。分析後、機能Zは機能A、B、およびCで構成されていると結論付けます。インターフェイスを介してクラスA、B、およびCを使用するfascadeクラスZを作成します。実装と、ある時点でタスクZが実際に機能A、B、Dで構成されていることに気付きます。Cインターフェイス、Cクラスプロトタイプを破棄し、個別のDインターフェイスとクラスを記述する必要があります。インターフェイスがなければ、クラスだけを置き換える必要があります。

DIPを真に活用するには、最初にクラスZを作成し、クラスA、B、およびC(まだ開発されていない)の機能を呼び出すようにします。これにより、クラスA、B、CのAPIが得られます。次に、クラスA、B、Cを作成して詳細を入力します。クラスZが必要とするものに完全に基づいて、クラスZを作成するときに必要な抽象化を効果的に作成する必要があります。クラスA、B、またはCを作成する前に、クラスZをテストすることもできます。

DIPでは、「高レベルのモジュールは低レベルのモジュールに依存するべきではありません。両方とも抽象化に依存する必要がある」ということを覚えておいてください。

クラスZに必要なものと、クラスZが必要なものを取得する方法を決定したら、詳細を入力できます。もちろん、クラスZに変更を加える必要がある場合もありますが、99%の場合はそうではありません。

Zが記述される前にA、B、Cが必要であることがわかったため、クラスDが存在することはありません。要件の変更はまったく別の話です。


5

短い答えは「ほとんどない」ですが、実際には、DIPが意味をなさない場所がいくつかあります。

  1. オブジェクトを作成することが仕事である工場または建築業者。これらは基本的に、IoCを完全に包含するシステムの「リーフノード」です。ある時点で、何かが実際にオブジェクトを作成する必要があり、それを行うために他の何かに依存することはできません。多くの言語では、IoCコンテナがあなたのためにそれをすることができますが、時々あなたは昔ながらの方法でそれをする必要があります。

  2. データ構造とアルゴリズムの実装。一般に、これらの場合、最適化する際の顕著な特性(実行時間や漸近的な複雑さなど)は、使用されている特定のデータ型に依存します。ハッシュテーブルを実装している場合、リンクリストではなく、ストレージ用の配列を操作していること、そして配列を適切に割り当てる方法を知っているのはテーブル自体だけであることを本当に知る必要があります。また、可変配列を渡して、その内容をいじることによって呼び出し元にハッシュテーブルを分割させたくありません。

  3. ドメインモデルクラス。これらはビジネスロジックを実装し、(ほとんどの場合)1つのビジネス用のソフトウェアのみを開発しているため、(ほとんどの場合)1つの実装を持つことだけが理にかなっています。けれども、いくつかのドメインモデルクラスは、他のドメインモデルクラスを使用して構築される可能性があります、これは一般的に、ケースバイケースであることを行っています。ドメインモデルオブジェクトには、便利にモックできる機能が含まれていないため、DIPにはテスト容易性や保守性の利点はありません。

  4. 外部APIとして提供され、他のオブジェクトを作成する必要があるオブジェクト。その実装の詳細を公開したくない場合。これは、「ライブラリの設計はアプリケーションの設計とは異なります」という一般的なカテゴリに分類されます。ライブラリまたはフレームワークは、DIを内部で自由に使用できますが、最終的には実際の作業を行う必要があります。そうでない場合、あまり有用なライブラリではありません。ネットワークライブラリを開発しているとしましょう。コンシューマーがソケットの独自の実装を提供できるようにすることは本当に望ましくありませ。内部でソケットの抽象化を使用することもできますが、呼び出し元に公開するAPIは独自のソケットを作成します。

  5. ユニットテスト、およびテストダブル。偽物とスタブは、1つのことを実行し、単純に実行することになっています。依存性注入を行うかどうかを心配するほど複雑な偽物がある場合は、おそらくあまりにも複雑です(おそらく、あまりにも複雑すぎるインターフェイスを実装しているためです)。

もっとあるかもしれません。これらはやや頻繁に見られるものです。


「動的言語で作業しているときはいつでも」はどうですか?
ケビン

番号?私はJavaScriptで多くの仕事をしていますが、それでも同じようにうまく適用されます。ただし、SOLIDの「O」と「I」は少しぼやけることがあります。
アーロンノート

ええと...私はPythonのファーストクラスの型とアヒルの型付けを組み合わせることで、必要性がかなり低くなっていることがわかりました。
ケビン

DIPは、型システムとはまったく関係ありません。そして、どのように「ファーストクラス型」はPythonに固有ですか?何かを分離してテストする場合、依存関係をテストダブルに置き換える必要があります。これらのテストダブルは、インターフェースの代替実装(静的型付け言語)にすることも、匿名オブジェクトまたは同じメソッド/関数を偶然に持つ代替型(アヒル型付け)にすることもできます。どちらの場合でも、実際にインスタンスを置き換える方法が必要です。
アーロンノート

1
@Kevin pythonは、動的型付けやルーズモックのいずれかを備えた最初の言語ではありませんでした。また、完全に無関係です。問題は、オブジェクトのタイプではなく、そのオブジェクトがどのように/どこで作成されるかです。オブジェクトが独自の依存関係を作成すると、パブリックAPIで言及されていないクラスのコンストラクターをスタブするなどの恐ろしいことを行うことで、実装の詳細を単体テストする必要があります。そして、テストを忘れ、振る舞いとオブジェクトの構築を単純に結びつけるだけです。ダックタイピングは、これらの問題のいずれも解決しません。
アーロンノート

2

DIPを非常に小さなレベルで適用している可能性がありますが、値を提供していません。

  • C / CImplまたはIC / Cペアがあり、そのインターフェイスの実装は1つだけである
  • インターフェイスと実装の署名が1対1に一致する(DRY原則に違反)
  • CとCImplを同時に頻繁に変更します。
  • Cはプロジェクトの内部にあり、プロジェクトの外部でライブラリとして共有されることはありません。
  • Visual StudioのEclipse / F12のF3に実際のクラスの代わりにインターフェイスが表示されることにイライラしている

これが表示されている場合は、ZがCを直接呼び出してインターフェイスをスキップする方が良いかもしれません。

また、依存性注入/動的プロキシフレームワーク(Spring、Java EE)によるメソッドデコレーションは、真のSOLID DIPとは異なります。これは、その技術スタックでのメソッドデコレーションの動作の実装の詳細に似ています。Java EEコミュニティは、これまでのようにFoo / FooImplのペアを必要としないことを改善と見なしています(参照)。対照的に、Pythonは関数装飾を第一級の言語機能としてサポートしています。

このブログ投稿も参照してください。


0

依存関係を常に反転させると、すべての依存関係が逆さまになります。つまり、依存関係の結びつきがある厄介なコードから始めた場合、それはまだ(本当に)持っているものであり、逆になっているだけです。ここで、実装を変更するたびにインターフェースも変更する必要があるという問題が発生します。

依存関係の逆転のポイントは、物事を混乱させる依存関係を選択的に逆転させることです。AからBからCに移動する必要があるものはまだそうします。CからAに移動していたものは、現在AからCに移動します。

結果は、サイクルのない依存グラフ、つまりDAGになります。このプロパティをチェックし、グラフを描くさまざまな ツールがあります。

詳細な説明については、この記事を参照してください

依存関係逆転の原則を正しく適用することの本質はこれです:

依存するコード/サービス/…をインターフェイスと実装に依存します。インターフェイスは、それを使用してコードの専門用語の依存関係を再構築し、実装はその基礎となる技術の観点から実装します。

実装はそのままです。しかし、インターフェースには現在、異なる機能があり(そして、異なる専門用語/言語を使用しています)、使用するコードができることを記述しています。そのパッケージに移動します。インターフェイスと実装を同じパッケージに配置しないことにより、依存関係(の方向)は、ユーザー→実装から実装→ユーザーに反転します。

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