コンストラクター注入
予定
クラスとその必要な依存関係の間のハードリンクを解除します。
説明
パターンの本質は、特定のクラスに必要なすべての依存関係が、 インターフェイスまたは抽象クラスの形式で提示されるコンストラクターパラメーターとしてパターンに渡されることです。
必要な依存関係が開発中のクラスで常に利用できることをどのように保証できますか?
これは、呼び出し元のすべてのクラスがコンストラクターパラメーターとして依存関係を渡す場合に保証されます。
依存関係を必要とするクラスには、必要な依存関係のインスタンスをコンストラクターへの引数として受け取るパブリックアクセス修飾子を持つコンストラクターが必要です。
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つ以上の依存関係を必要とし、適切なローカルデフォルトがない場合に、最も一般的なシナリオを実装します。
コンストラクターを介して実装を使用するための最良のヒントとプラクティスを検討してください。
- 可能であれば、クラスを単一のコンストラクターに制限する必要があります。
- オーバーロードされたコンストラクターはあいまいさを引き起こします:どのコンストラクターが依存性注入を使用する必要がありますか?
- コンストラクタに他のロジックを追加しないでください
- コンストラクターはその存在を保証するため、クラスのどこにも依存しない場合、nullをチェックする必要はありません。
長所 | 欠点 |
展開保証 | 一部のフレームワークでは、コンストラクターを介した実装を使用することは困難です。 |
実装のしやすさ | 依存関係グラフ全体の即時初期化が必要(*) |
クラスとそのクライアントとの間の明確な契約を保証します(上位クラスからの依存関係がどこから来るのかを考えることなく、現在のクラスについて考える方が簡単です) | - |
クラスの複雑さが明らかになる | - |
コンストラクターを使用して依存関係を渡す場合の潜在的な問題は、コンストラクターパラメーターの過度の増加です。 ここでもっと読むことができます。
多数のコンストラクタパラメータのもう1つの理由は、強調表示されている抽象化が多すぎることです。 この状況は、まったく切り離す必要がないものからでも切り離し始めたことを示している可能性があります。単にデータを保存するオブジェクト、または動作が安定しており、外部環境に依存せず、明らかにクラス内に隠されるべきオブジェクトのインターフェイスを作成し始めました突き出すのではなく
使用例
コンストラクター注入は、依存性注入の基本的なパターンであり、ほとんど考えていない場合でも、ほとんどのプログラマーによって広く使用されています。 ほとんどの「標準」デザインパターン(GoFパターン)の主な目標の1つは疎結合デザインを取得することです。そのため、それらのほとんどが何らかの形で依存性注入を使用することは驚くことではありません。
そのため、 デコレーターはコンストラクターを介した依存性注入を使用します。 ストラテジーはコンストラクターを介して渡されるか、目的のメソッドに「実装」されます。 コマンドはパラメーターとして渡すことも、コンストラクターを介して周囲のコンテキストを取ることもできます。 多くの場合、 抽象ファクトリはコンストラクターを介して渡され、定義により、インターフェイスまたは抽象クラスを介して実装されます。 Stateパターンは、必要なコンテキストを依存関係などとして受け取ります。
BCLでのコンストラクター注入の使用を示す2つの例では、 System.IO.StreamReaderクラスとSystem.IO.StreamWriterクラスを使用します。
どちらも、コンストラクターでSystem.IO.Streamクラスのインスタンスを取得します。
public StreamWriter(Stream stream); public StreamReader(Stream stream);
Streamクラスは、 StreamWriterとStreamReaderがタスクを実行する抽象化として機能する抽象クラスです。 Streamクラスの実装をコンストラクターに渡すことができ、コンストラクターはそれを使用します。 ただし、 nullをStreamとしてコンストラクターに渡そうとすると、 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をキャッチできます )。 次の保護ブロックは、依存関係が一度だけ設定されるようにする責任があります。
また、プロパティの読み取り後に依存関係がブロックされることに気付くかもしれません。 これは、クライアントが中毒が同じままであると考える一方で、中毒が後で予告なく変更される状況から顧客を保護するために行われます。
プロパティの埋め込みをいつ適用するか
プロパティインジェクションは、開発中のクラスに適切なローカルデフォルトがある場合にのみ適用する必要がありますが、同時に、依存関係タイプの別の実装を使用する機会を呼び出し元に残したい場合があります。 プロパティの注入は、依存関係がオプションの場合に最適に使用されます 。 プロパティに値を割り当てることを忘れがちであり、コンパイラはこれに反応しないため、プロパティはオプションであると見なされる必要があります。
設計時に特定のクラスにこのデフォルト実装を設定するのは魅力的かもしれません。 ただし、そのような初期のデフォルトが別のアセンブリで実装されている場合、この方法で使用すると、必然的に不変の参照が作成され、 弱いバインディングの利点の多くが無効になります。
警告
- 必要な依存関係にプロパティインジェクションを使用します。 これは、このパターンを使用する際の最も一般的な間違いの1つです。 クラスに何らかの依存関係が必要な場合は、コンストラクターに渡して、オブジェクトの作成直後に有効な状態になるようにする必要があります。
- ローカルデフォルトの代わりに外部デフォルトを使用します。 デフォルトの依存関係の実装を使用する危険の1つは、サービスが認識してはならないアセンブリにある特定の依存関係の使用です( Foreign Default )。 そのようなサービスが多数ある場合、理解と保守を複雑にする追加の物理接続が多数得られます。 デフォルトの実装は同じアセンブリ内にある必要があります( Local Default )。
- 複雑さ 必要な依存関係にプロパティインジェクションを使用する場合の問題は、クラスの複雑さが大幅に増加することです。 それぞれがnullの可能性がある3つのフィールドを持つクラスは、オブジェクトの状態の8つの異なる組み合わせにつながります。 各オープンメソッドの本体の状態をチェックしようとすると、複雑さが不必要にジャンプします。
- コンテナへの添付。 ほとんどの場合、 最小限の場所でコンテナを使用する必要があります。 コンストラクターインジェクションを全体として使用すると、クラスを特定のコンテナーにバインドしないため、これを実現できます。 ただし、 プロパティインジェクションを使用すると状況が変わります。 ほとんどのコンテナには、プロパティを介して依存関係を管理するための特殊な属性のセットが含まれています( StructureMapの SetterAttribute 、 Unityの Dependency 、 Castle Windsorの DoNotWireなど)。 このような緊密な接続では、「気分を変えて」別のコンテナに切り替えたり、使用を完全に拒否したりすることはできません。
- 書き込み専用プロパティ。 常に依存関係を返すプロパティを公開したいわけではありません。 この場合、書き込み用のプロパティ( set-only property )を設定する必要があります。これは、 .NETプラットフォームで一般に受け入れられている設計原則に反するか、プロパティの代わりにメソッドを使用します( Setter Method Injection )。
public class SomeClass { private ISomeInterface _dependency; public void SetDependency(ISomeInterface dependency) { _dependency = dependency; } }
代替案
オプションの依存関係を含むクラスがある場合 、2つのコンストラクターで古いアプローチを使用できます。
public class SomeClass { private ISomeInterface _dependency; public SomeClass() : this(new DefaultSomeInterface()) { } public SomeClass(ISomeInterface dependency) { _dependency = dependency; } }
おわりに
プロパティインジェクションは 、 オプションの依存関係に最適です 。 これらはデフォルト実装の戦略に非常に適していますが、とにかく、 Constructor Injectionの使用を推奨し、必要な場合にのみ他のオプションを検討します。