依存性注入パターン。 パート1

このトピックは、高品質で柔軟性のあるテスト可能なコードを作成するための必須項目の1つであるため、.Netでの依存関係の実装を取り上げましょう。 必要な基本的な依存関係注入パターン自体から始めます-コンストラクターとプロパティーを介した実装。 さあ、行こう!



コンストラクター注入



予定



クラスとその必要な依存関係の間のハードリンクを解除します。



説明



パターンの本質は、特定のクラスに必要なすべての依存関係が、 インターフェイスまたは抽象クラスの形式で提示されるコンストラクターパラメーターとしてパターンに渡されることです。



必要な依存関係が開発中のクラスで常に利用できることをどのように保証できますか?



これは呼び出し元のすべてのクラスがコンストラクターパラメーターとして依存関係を渡す場合に保証されます。



依存関係を必要とするクラスには、必要な依存関係のインスタンスをコンストラクターへの引数として受け取るパブリックアクセス修飾子を持つコンストラクターが必要です。



private readonly IFoo _foo; public Foo(IFoo foo) { if (foo == null) throw new ArgumentNullException(nameof(foo)); _foo = foo; }
      
      





依存関係は、 必須のコンストラクター引数です。 依存関係のインスタンスを提供しないクライアントのコードはコンパイルできません。 ただし、 インターフェイス抽象クラスの両方が参照型であるため、呼び出し元のコードは引数に特別なnull値を渡すことができ、これによりアプリケーションがコンパイルされます。 したがって、クラスのnullがチェックされ、そのような不適切な使用からクラスが保護されます 。 コンパイラーと保護ユニットの共同作業( nullのチェック)により、コンストラクター引数が正しいことが保証されるため(例外が発生しない場合( Exception ))、コンストラクターは、実際の実装の詳細を把握することなく、将来の使用のために依存関係を単純に保存できます。



依存関係の値を格納するフィールドを「 読み取り専用 」として宣言することをお勧めします。 そのため、コンストラクターの初期化ロジックが1回だけ実行されることを保証します。 フィールドは変更できません 。 これは、依存性注入を実装するために必要ではありませんが、この方法では、コードはクラスコード内の他の場所での偶発的なフィールド変更(値をnullに設定するなど)から保護されます。



コンストラクターを介した実装を使用するタイミングと方法



デフォルトでは、 コンストラクター注入を依存性注入とともに使用する必要があります。 クラスが1つ以上の依存関係を必要とし、適切なローカルデフォルトがない場合に、最も一般的なシナリオを実装します。



コンストラクターを介して実装を使用するための最良のヒントとプラクティスを検討してください。





長所 欠点
展開保証 一部のフレームワークでは、コンストラクターを介した実装を使用することは困難です。
実装のしやすさ 依存関係グラフ全体の即時初期化が必要(*)
クラスとそのクライアントとの間の明確な契約を保証します(上位クラスからの依存関係がどこから来るのかを考えることなく、現在のクラスについて考える方が簡単です) -
クラスの複雑さが明らかになる -
(*)コンストラクター実装の明らかな欠点は、 依存関係グラフ全体すぐに初期化する必要があることです(多くの場合、アプリケーションの起動時に既に)。 それにもかかわらず、この欠点はシステムの効率を低下させるように見えますが、実際にはほとんど問題になりません。 複雑なオブジェクトグラフであっても、オブジェクトのインスタンス化は、 .NETフレームワークが非常に迅速に実行するアクションです。 非常にまれなケースでは、この問題は非常に深刻です。 次に、この問題の解決に非常に適したDelayedと呼ばれるライフサイクルパラメータを使用します。



コンストラクターを使用して依存関係を渡す場合の潜在的な問題は、コンストラクターパラメーターの過度の増加です。 ここでもっと読むことができます。



多数のコンストラクタパラメータのもう1つの理由は、強調表示されている抽象化が多すぎることです。 この状況は、まったく切り離す必要がないものからでも切り離し始めたことを示している可能性があります。単にデータを保存するオブジェクト、または動作が安定しており、外部環境に依存せず、明らかにクラス内に隠されるべきオブジェクトのインターフェイスを作成し始めました突き出すのではなく



使用例



コンストラクター注入は、依存性注入の基本的なパターンであり、ほとんど考えていない場合でも、ほとんどのプログラマーによって広く使用されています。 ほとんどの「標準」デザインパターン(GoFパターン)の主な目標の1つは疎結合デザインを取得することです。そのため、それらのほとんどが何らかの形で依存性注入を使用することは驚くことではありません。



そのため、 デコレーターはコンストラクターを介した依存性注入を使用します。 ストラテジーはコンストラクターを介して渡されるか、目的のメソッドに「実装」されます。 コマンドはパラメーターとして渡すことも、コンストラクターを介して周囲のコンテキストを取ることもできます。 多くの場合、 抽象ファクトリはコンストラクターを介して渡され、定義により、インターフェイスまたは抽象クラスを介して実装されます。 Stateパターンは、必要なコンテキストを依存関係などとして受け取ります。



BCLでのコンストラクター注入の使用を示す2つの例では、 System.IO.StreamReaderクラスとSystem.IO.StreamWriterクラスを使用します。



どちらも、コンストラクターでSystem.IO.Streamクラスのインスタンスを取得します。



 public StreamWriter(Stream stream); public StreamReader(Stream stream);
      
      





Streamクラスは、 StreamWriterStreamReaderがタスクを実行する抽象化として機能する抽象クラスです。 Streamクラスの実装をコンストラクターに渡すことができ、コンストラクターはそれを使用します。 ただし、 nullStreamとしてコンストラクターに渡そうとすると、 ArgumentNullExceptionsが生成されます。



 //  var ms = new MemoryStream(); var bs = new BufferedStream(ms); //   var sortedArray = new SortedList<int, string>( new CustomComparer()); //  ResourceReader  Stream Stream ms = new MemoryStream(); var resourceReader = new ResourceReader(ms); // BinaryReader/BinaryWriter, StreamReader/StreamWriter //   Stream   var textReader = new StreamReader(ms); // Icon  Stream var icon = new System.Drawing.Icon(ms);
      
      





おわりに



DIコンテナを使用するかどうかに関係なく、 Constructor Injectionによる実装は、依存関係を管理する最初の方法である必要があります。 これを使用すると、クラス間の関係がより明確になるだけでなく、コンストラクターパラメーターの数が特定の制限を超えたときに設計上の問題を特定できます。 さらに、最新の依存性注入コンテナはすべてこのパターンをサポートしています。



プロパティインジェクション



予定



クラスとそのオプションの依存関係の間のハードリンクを解除します。



説明



適切なローカルデフォルトがある場合、クラスのオプションとして依存性注入を有効にするにはどうすればよいですか?



書き込み可能なプロパティを使用します。これにより、呼び出し側はデフォルトの動作を置き換える場合に値を設定できます。



依存関係を使用するクラスには、 public修飾子を持つ書き込み可能なプロパティが必要です。このプロパティのタイプは、依存関係のタイプと一致する必要があります。



 public class SomeClass { public ISomeInterface Dependency { get; set; } }
      
      





ここで、 SomeClassは ISomeInterfaceに依存しています 。 クライアントは、 Dependencyプロパティを介してISomeInterfaceインターフェイスの実装を渡すことができます。 コンストラクターの実装とは異なり、呼び出し元はSomeClassクラスのライフサイクルのいつでもこのプロパティの値を変更できるため、 Dependencyプロパティフィールドを「 読み取り専用 」としてマークすることできません。



依存クラスの他のメンバーは、注入された依存関係を使用して、たとえば次の機能を実行できます。



 public string DoSomething(string message) { return this.Dependency.DoStuff(message); }
      
      





ただし、 DependencyプロパティはISomeInterfaceインスタンスの戻りを保証しないため、このような実装は信頼できません。 たとえば、次のコードはDependencyプロパティの値がnullであるため、 NullReferenceExceptionをスローします



 var sc = new SomeClass(); sc.DoSomething("Hello world!");
      
      





この問題は、プロパティのインスタンスコンストラクターにデフォルトの依存関係を設定し、プロパティセッターメソッドにnullチェックを追加することで解決できます。



 public class SomeClass { private ISomeInterface _dependency; public SomeClass() { _dependency = new DefaultSomeInterface(); } public ISomeInterface Dependency { get => _dependency; set => _dependency = value ?? throw new ArgumentNullException(nameof(value)); } }
      
      





顧客がクラスのライフサイクル中に依存関係値を変更できる場合、問題が発生します。



クライアントがクラスのライフサイクル中に依存関係の値を変更しようとするとどうなりますか?



この結果は、クラスの一貫性のない、または予期しない動作になる可能性があるため、このようなイベントの変化から身を守ることをお勧めします。



 public class SomeClass { private ISomeInterface _dependency; public ISomeInterface Dependency { get => _dependency ?? (_dependency = new DefaultDependency()); set { //  1    if (_dependency != null) throw new InvalidOperationException(nameof(value)); _dependency = value ?? throw new ArgumentNullException(nameof(value)); } } }
      
      





DefaultDependencyの作成は、プロパティが最初に要求されるまで遅らせることができます。 この場合、初期化の遅延が発生します。 ローカルのデフォルトは、 セッターを介してpublic修飾子を使用して割り当てられるため、すべての保護ブロックが実行されることに注意してください。 最初の保護ブロックは、確立される依存関係がnullでないことを保証します (使用中にNREをキャッチできます )。 次の保護ブロックは、依存関係が一度だけ設定されるようにする責任があります。



また、プロパティの読み取り後に依存関係がブロックされることに気付くかもしれません。 これは、クライアントが中毒が同じままであると考える一方で、中毒が後で予告なく変更される状況から顧客を保護するために行われます。



プロパティの埋め込みをいつ適用するか



プロパティインジェクションは、開発中のクラスに適切なローカルデフォルトがある場合にのみ適用する必要がありますが、同時に、依存関係タイプの別の実装を使用する機会を呼び出し元に残したい場合があります。 プロパティの注入は、依存関係がオプションの場合に最適に使用されます 。 プロパティに値を割り当てることを忘れがちであり、コンパイラはこれに反応しないため、プロパティはオプションであると見なされる必要があります。



設計時に特定のクラスにこのデフォルト実装を設定するのは魅力的かもしれません。 ただし、そのような初期のデフォルトが別のアセンブリで実装されている場合、この方法で使用すると、必然的に不変の参照が作成され、 弱いバインディングの利点の多くが無効になります。



警告





代替案



オプションの依存関係を含むクラスがある場合 、2つのコンストラクターで古いアプローチを使用できます。



 public class SomeClass { private ISomeInterface _dependency; public SomeClass() : this(new DefaultSomeInterface()) { } public SomeClass(ISomeInterface dependency) { _dependency = dependency; } }
      
      





おわりに



プロパティインジェクションはオプションの依存関係に最適です 。 これらはデフォルト実装の戦略に非常に適していますが、とにかく、 Constructor Injectionの使用を推奨し、必要な場合にのみ他のオプションを検討します。



All Articles