このトピックは、高品質で柔軟性のあるテスト可能なコードを作成するための必須項目の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つ以上の依存関係を必要とし、適切なローカルデフォルトがない場合に、最も一般的なシナリオを実装します。
コンストラクターを介して実装を使用するための最良のヒントとプラクティスを検討してください。
- 可能であれば、クラスを単一のコンストラクターに制限する必要があります。
- オーバーロードされたコンストラクターはあいまいさを引き起こします:どのコンストラクターが依存性注入を使用する必要がありますか?
- コンストラクタに他のロジックを追加しないでください
- コンストラクターはその存在を保証するため、クラスのどこにも依存しない場合、nullをチェックする必要はありません。
長所 | 欠点 |
展開保証 | 一部のフレームワークでは、コンストラクターを介した実装を使用することは困難です。 |
実装のしやすさ | 依存関係グラフ全体の即時初期化が必要(*) |
クラスとそのクライアントとの間の明確な契約を保証します(上位クラスからの依存関係がどこから来るのかを考えることなく、現在のクラスについて考える方が簡単です) | - |
クラスの複雑さが明らかになる | - |
(*)コンストラクター実装の明らかな欠点は、
依存関係グラフ全体を
すぐに初期化する必要があることです(多くの場合、アプリケーションの起動時に既に)。 それにもかかわらず、この欠点はシステムの効率を低下させるように見えますが、実際にはほとんど問題になりません。 複雑なオブジェクトグラフであっても、オブジェクトのインスタンス化は、
.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クラスは、
StreamWriterと
StreamReaderがタスクを実行する抽象化として機能する抽象クラスです。
Streamクラスの実装をコンストラクターに渡すことができ、コンストラクターはそれを使用します。 ただし、
nullを
Streamとしてコンストラクターに渡そうとすると、
ArgumentNullExceptionsが生成されます。
おわりに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 {
DefaultDependencyの作成は、プロパティが最初に要求されるまで遅らせることができます。 この場合、初期化の遅延が発生します。 ローカルのデフォルトは、
セッターを介して
public修飾子を使用して割り当てられるため、すべての保護ブロックが実行されることに注意してください。 最初の保護ブロックは、確立される依存関係が
nullでないことを保証し
ます (使用中に
NREをキャッチでき
ます )。 次の保護ブロックは、依存関係が一度だけ設定されるようにする責任があります。
また、プロパティの読み取り後に依存関係が
ブロックされることに気付くかもしれません。 これは、クライアントが中毒が同じままであると考える一方で、中毒が後で予告なく変更される状況から顧客を保護するために行われます。
プロパティの埋め込みをいつ適用するか
プロパティインジェクションは、開発中のクラスに適切な
ローカルデフォルトがある場合にのみ適用する必要があり
ますが、同時に、依存関係タイプの別の実装を使用する機会を呼び出し元に残したい場合があります。 プロパティの注入は、依存関係が
オプションの場合に最適に使用され
ます 。 プロパティに値を割り当てることを忘れがちであり、コンパイラはこれに反応しないため、プロパティは
オプションであると見なされる必要があります。
設計時に特定のクラスにこのデフォルト実装を設定するのは魅力的かもしれません。 ただし、そのような初期のデフォルトが別の
アセンブリで実装されている場合、この方法で使用すると、必然的に
不変の参照が作成され、
弱いバインディングの利点の多くが無効になります。
警告
代替案
オプションの依存関係を含むクラスがある
場合 、2つのコンストラクターで古いアプローチを使用できます。
public class SomeClass { private ISomeInterface _dependency; public SomeClass() : this(new DefaultSomeInterface()) { } public SomeClass(ISomeInterface dependency) { _dependency = dependency; } }
おわりに
プロパティインジェクションは 、
オプションの依存関係に最適
です 。 これらはデフォルト実装の戦略に非常に適していますが、とにかく、
Constructor Injectionの使用を推奨し、必要な場合にのみ他のオプションを検討します。