依存関係を理解する

翻訳者から



私たちは実装者です。 空想ではなく、紹介しなければなりません!

(リナ・ゼレナヤ、映画「住所のない少女」)



この記事を翻訳するきっかけは2つあります。1)Springフレームワークをよりよく理解したいという願望、2)ロシア語のトピックに関する少数の情報源。



OOPの基礎は「依存性注入」です。 「実装」プロセス全体の説明が満足できるものである場合、「依存」の概念の説明は通常角括弧から除外されます。 私の意見では、これは重大な省略です。







空想するのではなく、実装するために、最初に私たちが導入していることを理解する必要があります。 また、Jakob Jenkovによる簡潔な記事「Understanding Dependencies」がこれに役立ちます。 Javaで書く人だけでなく、他の言語で書いてアプリケーション設計の品質を監視する人にも役立ちます。



UPD:依存関係に関する別のJakob Jenkov記事を翻訳しました。 HabréでDependency Injectionの記事の翻訳を読んでください。これは同じ名前の一連の記事を開き、ある意味でこの記事を続けています。 このシリーズの記事では、依存関係、依存関係注入(DI)、DIコンテナーなどの概念について説明します。





依存関係を理解する





依存関係、弱い接続などについて言及していないOOPに関する良い本を読むことはできません。これには十分な理由があります。 APIとアプリケーションのオブジェクト指向設計では、依存関係を理解することが重要です。 しかし、私の意見では、このテーマは多くの本よりもはるかに深く探求することができます。 これがテキストの目的です。 あなたが経験豊富なオブジェクト指向開発者であれば、ここに書かれていることの多くをすでに知っていることができます。 また、多くの開発者がテキストから何かを引き出すことができると信じています。



中毒とは何ですか?



クラスAがクラスまたはインターフェースBを使用する場合、AはBに依存します。AはBなしでジョブを実行できず、AをBを再利用せずに再利用できません。 「中毒」



互いに使用する2つのクラスは、関連と呼ばれます。 クラス間の接続性は、弱い、強い、またはその間にある可能性があります。 接続の程度は、バイナリまたは離散ではなく、連続的です。 強い接続性は強い依存関係につながり、弱い接続性は弱い依存関係につながり、状況によっては依存関係がなくなることさえあります。



依存関係または関係が指示されます。 AがBに依存しているからといって、BがAに依存しているわけではありません。



中毒が悪いのはなぜですか?



依存関係は、再利用を減らすという点で悪いです。 再利用を減らすことは多くの理由で悪いです。 通常、再利用は開発速度、コード品質、コードの可読性などにプラスの効果をもたらします。



依存関係がどのように害を及ぼすかは、例によって最もよく示されています。XMLファイルからカレンダーイベントを読み取ることができるCalendarReaderクラスがあるとします。 CalendarReaderの実装は次のとおりです。



public class CalendarReader { public List readCalendarEvents(File calendarEventFile){ //open InputStream from File and read calendar events. } }
      
      





readCalendarEventsメソッドは、File型のオブジェクトをパラメーターとして受け取ります。 したがって、このメソッドはFileクラスに依存します。 Fileクラスへの依存は、CalendarReaderがファイルシステム内のローカルファイルからのみカレンダーイベントを読み取ることができることを意味します。 ネットワーク接続、データベース、またはクラスパス上のリソースからカレンダーイベントを読み取ることはできません。 CalendarReaderは、Fileクラスおよびローカルファイルシステムと密接に関連していると言えます。



以下のコードのように、あまり関連性のない実装は、File型のパラメーターをInputStream型のパラメーターに置き換えることです。



 public class CalendarReader { public List readCalendarEvents(InputStream calendarEventFile){ //read calendar events from InputStream } }
      
      





ご存知かもしれませんが、InputStreamは、Fileタイプのオブジェクト、ネットワークソケット、URLConnectionクラス、Classオブジェクト(Class.getResourceAsStream(文字列名))、JDBCを介したデータベースの列などから取得できます。 CalendarReaderは、ローカルファイルシステムに関連付けられなくなりました。 多くのソースからカレンダーイベントファイルを読み取ることができます。



InputStreamを使用するreadCalendarEvents()メソッドのバージョンにより、CalendarReaderクラスの再利用性が向上しました。 ローカルファイルシステムへの密結合は削除されました。 代わりに、InputStreamクラスへの依存関係に置き換えられました。 InputStreamへの依存はFileクラスへの依存よりも柔軟性がありますが、CalendarReaderを100%再利用できることを意味するものではありません。 たとえば、彼はまだNIOチャネルからデータを読み取ることができません。



依存関係の種類



依存関係は単なる「中毒」ではありません。 依存関係にはいくつかのタイプがあります。 それらはそれぞれ、コードの柔軟性を高めます。 依存関係の種類:





クラスの依存関係はクラスの依存関係です。 たとえば、次のコードボックスのメソッドは、パラメーターとして文字列を受け取ります。 したがって、メソッドはStringクラスに依存します。



 public byte[] readFileContents(String fileName){ //open the file and return the contents as a byte array. }
      
      





インターフェイスの依存関係は、インターフェイスの依存関係です。 たとえば、以下のコード挿入のメソッドは、パラメーターとしてCharSequenceを受け取ります。 CharSequenceは、標準のJavaインターフェイス(java.langパッケージ内)です。 CharBuffer、String、StringBuffer、およびStringBuilderクラスはCharSequenceインターフェイスを実装しているため、これらのクラスのインスタンスのみをこのメソッドのパラメーターとして使用できます。



 public byte[] readFileContents(CharSequence fileName){ //open the file and return the contents as a byte array. }
      
      





メソッドまたはフィールドの依存関係は、オブジェクトの特定のメソッドまたはフィールドへの依存関係です。 必要なタイプのメソッドまたはフィールドがある限り、オブジェクトのクラスまたは実装するインターフェースは関係ありません。 次の例は、メソッドの依存関係を示しています。 readFileContentsメソッドは、パラメーター(fileNameContainer)として渡されるオブジェクトのクラスの「getFileName」というメソッドに依存します。 依存関係はメソッド宣言からは見えないことに注意してください!



 public byte[] readFileContents(Object fileNameContainer){ Method method = fileNameContainer .getClass() .getMethod("getFileName", null); String fileName = method.invoke(fileNameContainer, null); //open the file and return the contents as a byte array. }
      
      





メソッドまたは変数の依存関係は、リフレクションを使用するAPIに固有です。 たとえば、Butterfly Persistenceはリフレクションを使用して、クラスのゲッターとセッターを検出します。 ゲッターとセッターがないと、Butterfly Persistenceはデータベースとの間でクラスオブジェクトを読み書きできません。 したがって、Butterfly Persistenceはゲッターとセッターに依存しています。 Hibernate(同様のORM API)は、ゲッターとセッターの両方を使用でき、フィールドを直接、およびリフレクションを介して使用できます。 したがって、Hibernateはメソッドまたはフィールドのいずれかに依存しています。



メソッドまたは(「関数」)の依存関係は、引数として渡す必要がある関数ポインターまたはメソッドポインターをサポートする言語でも確認できます。 たとえば、C#のデリゲート。



追加の依存関係特性



依存関係には、タイプ以外の重要な特性があります。 依存関係は、コンパイル時間、実行時、表示、非表示、直接、間接、コンテキストなどの依存関係です。 これらの追加機能については、次のセクションで説明します。



インターフェイス実装の依存関係



クラスAがインターフェイスIに依存する場合、AはIの特定の実装に依存しません。しかし、AはIの実装に依存します。Aは、Iの実装なしではその仕事を行えません。したがって、クラスがインターフェイスに依存する場合、このクラスもインターフェイスの実装に依存します。



インターフェイスにメソッドが多いほど、開発者が要求されない場合に開発者が独自の実装を提供する可能性が低くなります。 したがって、インターフェイスに存在するメソッドが多いほど、開発者がこのインターフェイスの標準実装に「スタック」する可能性が高くなります。 言い換えれば、インターフェイスがより複雑で面倒になるほど、デフォルトの実装に密接に関連付けられます。



インターフェイス実装の依存関係により、盲目的にインターフェイスに機能を追加する必要はありません。 機能をコンポーネントの独自の別個のインターフェースでカプセル化できる場合は、そうする必要があります。



以下は、これが何を意味するかの例です。 サンプルコードは、階層ツリー構造のツリーノードを示しています。



 public interface ITreeNode { public void addChild(ITreeNode node); public List<ITreeNode> getChildren(); public ITreeNode getParent(); }
      
      





特定のノードの子孫の数を計算できるようにしたいと想像してください。 最初に、countDescendents()メソッドをITreeNodeインターフェイスに追加したいと思うかもしれません。 ただし、その場合、ITreeNodeインターフェイスを実装する場合は、countDescendents()メソッドも実装する必要があります。



代わりに、DescendentCounterクラスを実装して、ITreeNodeインスタンスを表示し、そのインスタンスのすべての子孫を読み取ることができます。 DescendentCounterは、ITreeNodeインターフェイスの他の実装で再利用できます。 ITreeNodeインターフェースを実装する必要がある場合でも、countDescendents()メソッドの実装の問題からユーザーを救っただけです!



コンパイル時間とランタイムの依存関係



コンパイル時に解決できる依存関係は、コンパイル時依存関係と呼ばれます。 実行の開始前に解決できない依存関係は、実行時間の依存関係です。 コンパイル時の依存関係は、実行時の依存関係よりも簡単に認識できますが、実行時の依存関係はより柔軟です。 たとえば、Butterfly Persistenceは、実行時にクラスのゲッターとセッターを見つけ、それらをデータベーステーブルに自動的にマッピングします。 これは、クラスをデータベーステーブルにマップする非常に簡単な方法です。 ただし、これを行うには、Butterfly Persistenceは適切な名前のゲッターとセッターに依存します。



可視および非表示の依存関係



表示可能な依存関係は、開発者がクラスインターフェイスから確認できる依存関係です。 クラスインターフェイスで依存関係を検出できない場合、これらは非表示の依存関係です。



前述の例では、readFileContents()メソッドのStringおよびCharSequence依存関係は可視の依存関係です。 これらは、クラスインターフェイスの一部であるメソッド宣言で表示されます。 Objectをパラメーターとして受け取るreadFileContents()メソッドの依存関係は表示されません。 インターフェイスから、readFileContents()メソッドがfileNameContainer.toString()を呼び出してファイル名を取得すること、または実際にはgetFileName()メソッドを呼び出すことを確認できません。



非表示の依存関係のもう1つの例は、静的シングルトンまたはメソッド内の静的メソッドへの依存関係です。 インターフェイスがクラスが静的メソッドまたは静的シングルトンに依存することを確認できません。



ご想像のとおり、隠された依存関係は悪です。 開発者にとっては検出が困難です。 それらは、コードを調べることによってのみ識別できます。



これは、非表示の依存関係を使用しないでくださいということとは異なります。 非表示の依存関係は、多くの場合、適切なデフォルトを提供した結果です。 この例では、これは問題にならない場合があります。



 public class MyComponent{ protected MyDependency dependency = null; public MyComponent(){ this.dependency = new MyDefaultImpl(); } public MyComponent(MyDependency dependency){ this.dependency = dependency; } }
      
      





コンストラクターで確認できるように、MyComponentはMyDefaultImplに非表示の依存関係があります。 しかし、MyDefaultImplには危険な副作用がないため、この場合、隠された依存関係は危険ではありません。



直接および間接の依存関係



依存関係は、直接または間接のいずれかです。 クラスAがクラスBを使用する場合、クラスAはクラスBに直接依存します。AがBに依存し、BがCに依存する場合、AはCに間接依存します。BなしでAを使用できず、Bを使用できない場合Cなしでは、CなしでAも使用できません。



間接的な依存関係は、連結(チェーン)または推移的(ブルースA.テートとジャスティンゲーランドによるより良い、より速い、より軽いJava)とも呼ばれます。



不当に広範な依存関係



コンポーネントが動作するために必要以上の情報に依存する場合があります。 たとえば、Webアプリケーションのログインコンポーネントを想像してください。 このコンポーネントに必要なのはユーザー名とパスワードのみで、ユーザーオブジェクトが見つかった場合はそれを返します。 インターフェイスは次のようになります。



 public class LoginManager{ public User login(HttpServletRequest request){ String user = request.getParameter("user"); String password = request.getParameter("password"); //read user and return it. } }
      
      





コンポーネント呼び出しは次のようになります。



 LoginManager loginManager = new LoginManager(); User user = loginManager.login(request);
      
      





シンプルに見えますよね? また、ログインメソッドでさらに多くのパラメータが必要な場合でも、呼び出しコードを変更する必要はありません。



しかし、現時点では、ログインメソッドには、HttpServletRequestインターフェイスに対して「不当に広範な依存関係」と呼ばれるものがあります。 この方法は、動作に必要な以上のものに依存しています。 LoginManagerはユーザーを見つけるためにユーザー名とパスワードのみを必要としますが、ログインメソッドのパラメーターとしてHttpServletRequestを受け取ります。 HttpServletRequestには、LoginManagerが必要とする以上の情報が含まれています。



HttpServletRequestインターフェイスの依存関係により、2つの問題が発生します。



  1. LoginManagerは、HttpServletRequestオブジェクトなしでは再利用できません。 これにより、LoginManagerユニットテストがより困難になる可能性があります。 HttpServletRequestオブジェクトをロックする必要がありますが、これには多くの作業が必要です。
  2. LoginManagerでは、ユーザー名とパスワードのパラメーター名を「login」と「password」にする必要があります。 これもオプションの依存関係です。


LoginManagerログインメソッドのはるかに優れたインターフェイスは次のとおりです。



 public User login(String user, String password){ //read user and return it. }
      
      





しかし、呼び出し元のコードがどうなるか見てみましょう。



 LoginManager loginManager = new LoginManager(); User user = loginManager.login( request.getParameter("user"), request.getParameter("password"));
      
      





より複雑になりました。 これが、開発者が不当に広範な依存関係を作成する理由です。 呼び出しコードを簡素化するため。



ローカルおよびコンテキストの依存関係



アプリケーションを開発する場合、通常、アプリケーションをコンポーネントに分割します。 これらのコンポーネントの一部は、他のアプリケーションでも使用できる汎用コンポーネントです。 他のコンポーネントはアプリケーション固有であり、アプリケーションの外部で使用されることはありません。



汎用コンポーネントの場合、コンポーネント(またはAPI)に属するクラスはすべて「ローカル」です。 アプリケーションの残りの部分はコンテキストです。 汎用コンポーネントがアプリケーション固有のクラスに依存する場合、これは「コンテキスト依存関係」と呼ばれます。 コンテキストの依存関係は、アプリケーション外で汎用コンポーネントを使用することが不可能になるため、不良です。 悪いOO開発者だけがコンテキスト依存を作成すると考えるのは魅力的ですが、そうではありません。 開発者がアプリケーションの作成を単純化しようとすると、通常、コンテキスト依存が発生します。 ここでの良い例は、メッセージキューまたはWebアプリケーションに接続されたアプリケーションなどの要求処理アプリケーションです。



XMLとして要求を受信するアプリケーションが要求を処理し、応答でXMLを受信するとします。 XML要求処理には、いくつかの個別のコンポーネントが含まれます。 これらの各コンポーネントには異なる情報が必要であり、一部の情報は以前のコンポーネントによってすでに処理されています。 XMLファイルと関連するすべての処理を、すべてのコンポーネントに送信される何らかの種類の要求オブジェクト内で処理シーケンスでアセンブルすることは非常に魅力的です。 処理コンポーネントは、この要求オブジェクトから情報を読み取り、処理シーケンスのさらに先にあるコンポーネントの情報を自身から追加できます。 このリクエストオブジェクトをパラメータとして使用すると、リクエストを処理する各コンポーネントはこのリクエストに依存します。 要求オブジェクトはアプリケーション固有であるため、コンテキストは各要求処理コンポーネントに依存します。



標準とカスタムのクラス/インターフェースの依存関係



多くの場合、コンポーネントは標準Java(またはC#)パッケージのクラスまたはインターフェースに依存する方が適切です。 これらのクラスまたはインターフェイスは常に誰でもアクセスできるため、これらの依存関係を簡単に満たすことができます。 また、これらのクラスは変更される可能性が低く、アプリケーションのコンパイルが失敗する原因になります。



ただし、状況によっては、標準ライブラリに依存することは最良の方法ではありません。 たとえば、メソッドを設定するには4行が必要です。 したがって、メソッドはパラメーターとして4行を受け入れます。 たとえば、これは、データベースに接続するためのドライバー名、データベースURL、ユーザー名、およびパスワードです。 これらのすべての行が常に一緒に使用される場合、これらの4行をクラスにグループ化し、4行の代わりにそのインスタンスを渡すと、このメソッドのユーザーにとって明確になる可能性があります。



まとめ



依存関係のいくつかの異なるタイプと特性を見ました。 一般的に、インターフェイスの依存関係はクラスの依存関係よりも望ましいです。 状況によっては、クラスの依存関係がインターフェイスの依存関係よりも望ましい場合があります。 メソッドとフィールドの依存関係は非常に便利ですが、通常は非表示の依存関係であり、非表示の依存関係によりコンポーネントのユーザーがそれらを見つけて要件を満たすことは困難です。



インターフェイス実装の依存関係は、思っているよりも一般的です。 多くのアプリケーションやAPIでそれらを見てきました。 インターフェイスを小さく保ち、可能な限り制限するようにしてください。 少なくとも、コンポーネントのユーザーが実装するインターフェース。 追加の機能(カウントなど)を、問題のインターフェイスのインスタンスをパラメーターとして使用する外部コンポーネントに移動します。



個人的には、ランタイムの依存関係よりもコンパイル時の依存関係の方が好きですが、場合によってはランタイムの依存関係がよりエレガントになります。 たとえば、Mr。 Persisterは、getterおよびsetterの実行時依存関係を使用します。これにより、pojoは永続的なインターフェースを実装できなくなります。 この方法での実行時の依存性は、

コンパイル時の依存関係。



非表示の依存関係は危険な場合がありますが、実行時の依存関係は非表示の依存関係でもあるため、常に選択できるとは限りません。



コンポーネントが別のコンポーネントに直接依存していなくても、間接的に依存している可能性があることに注意してください。 制限は緩和されますが、間接的な依存関係も依存関係です。



不当に広い依存症を避けるようにしてください。 多くのパラメータをクラスにグループ化すると、不当に広い依存関係が発生することに注意してください。 これは、コードを単純にするために行われる一般的なリファクタリングですが、ご覧のとおり、不当に広い依存関係につながる可能性があります。



異なるコンテキストで使用されることになっているコンポーネントには、コンテキストの依存関係がありません。 つまり、コンポーネントは、最初に開発されたコンテキストおよび統合されたコンテキストにおいて、他のコンポーネントに依存してはなりません。



このテキストは依存関係のみを説明しています。 彼は彼らに何をすべきか教えてくれません。 このトレーニングサイトの他のテキストは、このトピックに没頭します( translに注意してください :私は著者の個人サイトを意味します )。



はじめに

依存性注入シリーズに戻る



All Articles