MarkSimanによる伝説の本「.NETプラットフォームへの依存関係の注入」の第2版をリリースする準備を しています。
そのような膨大な本でさえ、そのようなトピックを完全にカバーすることはほとんど不可能です。しかし、C#の例を使用して、単純な言語での依存関係の挿入の本質を概説する、非常にアクセスしやすい記事の簡略化された翻訳を提供します。
この記事の目的は、依存関係の注入の概念を説明し、特定のプロジェクトでどのようにプログラムされるかを示すことです。ウィキペディアから:
依存関係の注入は、動作を依存関係の解決から分離する設計パターンです。したがって、相互に依存度の高いコンポーネントを切り離すことができます。
依存関係インジェクション(またはDI)を使用すると、他のクラスに実装とサービスを提供して消費することができます。コードは非常に緩く結合されたままです。この場合の要点は次のとおりです。実装の代わりに、他の実装を簡単に置き換えることができます。同時に、実装とコンシューマーは契約によってのみ接続されている可能性が高いため、最小限のコードを変更する必要があります 。
C#では、これは、サービスの実装がインターフェイスの要件を満たす必要があることを意味します。サービスのコンシューマを作成するときは、実装ではなくインターフェイスをターゲットにし 、 実装を提供または 実装する必要があります。自分でインスタンスを作成する必要がないようにします。このアプローチを使用すると、依存関係がどのように作成され、どこから取得されるかについて、クラスレベルで心配する必要はありません。この場合、重要なのは契約だけです。
例による依存関係の注入
DIが役立つ例を見てみましょう。まず、メッセージをログに記録するなど、いくつかのタスクを実行できるようにするインターフェイス(契約)を作成しましょう。
public interface ILogger {
void LogMessage(string message);
}
注意:このインターフェースは、メッセージがどのように記録され、どこに記録されるかについてはどこにも記述していません。ここでは、インテントは単に文字列をリポジトリに書き込むために渡されます。次に、このインターフェイスを使用するエンティティを作成しましょう。ディスク上の特定のディレクトリを追跡し、ディレクトリに変更が加えられるとすぐに、対応するメッセージをログに記録するクラスを作成するとします。
public class DirectoryWatcher {
private ILogger _logger;
private FileSystemWatcher _watcher;
public DirectoryWatcher(ILogger logger) {
_logger = logger;
_watcher = new FileSystemWatcher(@ "C:Temp");
_watcher.Changed += new FileSystemEventHandler(Directory_Changed);
}
void Directory_Changed(object sender, FileSystemEventArgs e) {
_logger.LogMessage(e.FullPath + " was changed");
}
}
この場合、を実装する必要なコンストラクターが提供されていることに注意することが最も重要
ILogger
です。ただし、繰り返しになりますが、ログがどこに移動するか、またはログがどのように作成されるかは関係ありません。インターフェイスを念頭に置いてプログラミングするだけで、他のことは考えられません。
したがって、私たちのインスタンスを作成するに
DirectoryWatcher
は、既製の実装も必要
ILogger
です。先に進んで、メッセージをテキストファイルに記録するインスタンスを作成しましょう。
public class TextFileLogger: ILogger {
public void LogMessage(string message) {
using(FileStream stream = new FileStream("log.txt", FileMode.Append)) {
StreamWriter writer = new StreamWriter(stream);
writer.WriteLine(message);
writer.Flush();
}
}
}
Windowsイベントログにメッセージを書き込む別のイベントを作成しましょう。
public class EventFileLogger: ILogger {
private string _sourceName;
public EventFileLogger(string sourceName) {
_sourceName = sourceName;
}
public void LogMessage(string message) {
if (!EventLog.SourceExists(_sourceName)) {
EventLog.CreateEventSource(_sourceName, "Application");
}
EventLog.WriteEntry(_sourceName, message);
}
}
現在、非常に異なる方法でメッセージをログに記録する2つの別個の実装がありますが、どちらも実行します
ILogger
。つまり、インスタンスが必要な場所でどちらも使用できます
ILogger
。次に、インスタンス
DirectoryWatcher
を作成して、ロガーの1つを使用するように指示できます 。
ILogger logger = new TextFileLogger();
DirectoryWatcher watcher = new DirectoryWatcher(logger);
または、最初の行の右側を変更するだけで、別の実装を使用できます。
ILogger logger = new EventFileLogger();
DirectoryWatcher watcher = new DirectoryWatcher(logger);
これはすべて、DirectoryWatcherの実装を変更せずに行われ、これが最も重要なことです。消費者が自分でインスタンスを作成する必要がないように、ロガーの実装を消費者に注入しています。示されている例は簡単ですが、複数の依存関係があり、それらを使用する消費者が何倍もある大規模なプロジェクトでこのような手法を使用するとどうなるか想像してみてください。そして、突然、メッセージをログに記録する方法を変更する要求があります(たとえば、メッセージは監査目的でSQLサーバーにログに記録される必要があります)。何らかの形式で依存関係の挿入を使用しない場合は、コードを注意深く確認し、ロガーが実際に作成されて使用される場所に変更を加える必要があります。大規模なプロジェクトでは、このような作業は面倒でエラーが発生しやすくなります。DIを使用すると、依存関係を1か所で変更するだけで、アプリケーションの残りの部分が実際に変更を吸収し、すぐに新しいロギング方法の使用を開始します。
本質的に、これは依存度の高い古典的なソフトウェアの問題を解決し、DIを使用すると、非常に柔軟で変更が容易な疎結合コードを作成できます。
依存性注射容器
簡単にダウンロードして使用できる多くのDIインジェクションフレームワークは、さらに一歩進んで、依存関係インジェクション用のコンテナを使用します。本質的に、これはタイプマッピングを格納し、指定されたタイプの登録済み実装を返すクラスです。簡単な例では、コンテナにインスタンスを照会することができ
ILogger
、インスタンス
TextFileLogger
、またはコンテナを初期化したインスタンスが返さ れます。
この場合、通常はアプリケーション起動イベントが発生する1つの場所にすべての型マッピングを登録できるという利点があります。これにより、システムにどのような依存関係があるかをすばやく明確に確認できます。さらに、多くのプロフェッショナルフレームワークでは、新しいリクエストごとに新しいインスタンスを作成するか、複数の呼び出しで1つのインスタンスを再利用することにより、そのようなオブジェクトの存続期間を構成できます。
コンテナは通常、プロジェクトのどこからでも「リゾルバ」(インスタンスを要求できるエンティティの種類)にアクセスできるように作成されます。
最後に、専門的なフレームワークは通常、サブ依存の現象を サポートします。-この場合、依存関係自体には、コンテナにも知られている他のタイプへの1つ以上の依存関係があります。この場合、リゾルバーはこれらの依存関係も満たすことができ、タイプマッピングに対応する正しく作成された依存関係の完全なチェーンを返します。
非常に単純な依存関係インジェクションコンテナを自分で作成して、すべてがどのように機能するかを確認しましょう。このような実装はネストされた依存関係をサポートしていませんが、インターフェイスを実装にマップし、後で実装自体を要求することができます。
public class SimpleDIContainer {
Dictionary < Type, object > _map;
public SimpleDIContainer() {
_map = new Dictionary < Type, object > ();
}
/// <summary>
/// , .
/// </summary>
/// <typeparam name="TIn">The interface type</typeparam>
/// <typeparam name="TOut">The implementation type</typeparam>
/// <param name="args">Optional arguments for the creation of the implementation type.</param>
public void Map <TIn, TOut> (params object[] args) {
if (!_map.ContainsKey(typeof(TIn))) {
object instance = Activator.CreateInstance(typeof(TOut), args);
_map[typeof(TIn)] = instance;
}
}
/// <summary>
/// , T
/// </summary>
/// <typeparam name="T">The interface type</typeparam>
public T GetService<T> () where T: class {
if (_map.ContainsKey(typeof(T))) return _map[typeof(T)] as T;
else throw new ApplicationException("The type " + typeof(T).FullName + " is not registered in the container");
}
}
次に、コンテナを作成し、タイプを表示して、サービスを要求する小さなプログラムを作成できます。繰り返しますが、単純でコンパクトな例ですが、はるかに大きなアプリケーションでどのように見えるか想像してみてください。
public class SimpleDIContainer {
Dictionary <Type, object> _map;
public SimpleDIContainer() {
_map = new Dictionary < Type, object > ();
}
/// <summary>
/// , .
/// </summary>
/// <typeparam name="TIn">The interface type</typeparam>
/// <typeparam name="TOut">The implementation type</typeparam>
/// <param name="args">Optional arguments for the creation of the implementation type.</param>
public void Map <TIn, TOut> (params object[] args) {
if (!_map.ContainsKey(typeof(TIn))) {
object instance = Activator.CreateInstance(typeof(TOut), args);
_map[typeof(TIn)] = instance;
}
}
/// <summary>
/// , T
/// </summary>
/// <typeparam name="T">The interface type</typeparam>
public T GetService <T> () where T: class {
if (_map.ContainsKey(typeof(T))) return _map[typeof(T)] as T;
else throw new ApplicationException("The type " + typeof(T).FullName + " is not registered in the container");
}
}
プロジェクトに新しい依存関係を追加するときは、このパターンに固執することをお勧めします。プロジェクトのサイズが大きくなると、緩く結合されたコンポーネントの管理がいかに簡単であるかがわかります。かなりの柔軟性が得られ、プロジェクト自体は最終的には保守、変更、および新しい条件への適応がはるかに簡単になります。