DI(Dependency Injection) の解説
- DI とは何か
- DI を利用するメリット・デメリット
- Autofacを利用した例
- Autofacのさらなる機能
- その他
DI とは何か
Wikipediaでは以下のように説明されている。
コンポーネント間の依存関係をプログラムのソースコードから排除し、外部の設定ファイルなどで注入できるようにするソフトウェアパターンである。
言い換えると
あるコンポーネントAが利用している別のコンポーネントBを外部から注入することにより、コンポーネントAがコンポーネントBの実装との依存関係を排除するデザインパターン。
となる。
例
まずは DIを使わない場合
///<summary>メインプログラム</summary>
class Program{
public void Main(){
var client = new FooClient();
client.Execute();
}
}
///<summary>FooComponentを利用する機能</summary>
class FooClient{
public FooClient(){
}
public void Execute(){
var component = new FooComponent();
var result = component.Execute();
}
}
///<summary>機能Fooを提供するコンポーネント</summary>
class FooComponent{
public bool Execute(){
// ...なにか処理を行う
return true;
}
}
FooClient が FooComponentのインスタンスを new 演算子によって生成しているが、この場合FooClientは少なくとも「自身が利用するコンポーネントの生成方法」の知識を持ってしまっている。
本来、FooClient は FooComponentが公開しているインターフェースについての知識さえあれば機能Fooを利用できるはずだが、直接実装クラスの生成方法の知識を持ってしまっているため他の実装を利用できない。
FooComponentのカスタマイズ版を作って利用する場合、FooClientを書き換えるか、
///<summary>FooComponentを利用する機能</summary>
class FooClient{
public FooClient(){
}
public void Execute(){
// var component = new FooComponent(); から書き換え
var component = new CustomizeFooComponent();
var result = component.Execute();
}
}
///<summary>機能Fooを提供するコンポーネントのカスタマイズ版</summary>
class CustomFooComponent : FooComponent{
public bool Execute(){
// ...カスタマイズした処理を行う
return true;
}
}
定義済みシンボルで切り替えるか
///<summary>FooComponentを利用する機能</summary>
class FooClient{
public FooClient(){
}
public void Execute(){
// 以下、サンプルのため多数のカスタマイズ版を抱えることを極端に記述
#if CUSTOMER_A
var component = new FooComponent();
#elif CUSTOMER_B
var component = new CustomizeFooComponent();
#elif CUSTOMER_C
var component = new AnotherCustomizeFooComponent();
#elif CUSTOMER_D
var component = new MoreAnotherCustomizeFooComponent();
#endif
var result = component.Execute();
}
}
などが必要になる。
FooClientを書き換える方法は本来書き換えなくていい部分を書き換え無ければならず、定義済みシンボルで切り替える場合はプロジェクト構成ファイルの書き換えが必要になるし何よりコードが読みづらいし、バージョン管理上もマージの際の衝突のリスクが増える。
そこで、FooClientからFooComponentへの依存を排除してコードや構成ファイルの書き換えを減らす。
///<summary>メインプログラム</summary>
class Program{
public void Main(){
var client = new FooClient(new FooComponent());
client.Execute();
}
}
///<summary>FooComponentを利用する機能</summary>
class FooClient{
FooComponent component;
public FooClient(component){
this.component = component;
}
public void Execute(){
var result = component.Execute();
}
}
こうすると、FooClientは FooComponentが何者かについて考えなくても良くなるし、FooComponentの生成についての煩わしさはFooClientから除外できる。
(Main()はFooComponentの生成方法を知っている必要がある)
さらに、利用コンポーネントの生成をメインプログラムから排除するとこうなる
///<summary>メインプログラム</summary>
class Program{
public void Main(){
// 現実にはコンテナの構成は別クラスに移動し、設定ファイルで構成するとかする。
var componentManager = new ComponentManager();
componentManager.Register<FooComponent>(typeof(FooComponent));
componentManager.Register<FooClient>(typeof(FooClient));
var client = componentManager.GetComponent<FooClient>();
client.Execute();
}
}
///<summary>FooComponentを利用する機能</summary>
class FooClient{
FooComponent component;
public FooClient(component){
this.component = component;
}
public void Execute(){
var result = component.Execute();
}
}
///<summary>DIコンテナを利用する例</summary>
class ComponentManager{
private Container _container = new Container();
public void RegisterComponent<TComponent>(Type componentType){
_container.Register<TComponent>(componentType);
}
public TComponent GetComponent<TComponent>(){
return _container.Get<TComponent>();
}
}
これで、メインプログラムはクライアントや利用コンポーネントの生成についての煩わしさから解放される。
また、クライアントが別のコンポーネントを同時に利用するように変更された場合でもメインプログラムの中でクライアント自体の生成について書き換える必要はなくなる。
(その代わりDIコンテナの構成方法についての知識が必要になる。)
つまり、ある機能を利用するという事に関して機能のクライアントが余計な事を考えなくても済むようになるので、機能のクライアントを実装するプログラマは実装に集中することができる。
DI を利用するメリット・デメリット
メリット
- コンポーネントを利用する機能を実装する際に考慮する事を減らせる
- コードがすっきりする。
- コンポーネントのカスタマイズ版を利用しやすくなる(副次的)
デメリット
- 学習コストが上がる。(DIコンテナの構成方法など)
Autofacを利用した例
- コンポーネントクラスを記述する
- クライアントクラスを記述する
- DIコンテナを構成する
- DIコンテナから必要なオブジェクトを取り出す
- 全体のコード
コンポーネントクラスを記述する
クライアントにはインターフェースだけを公開する場合。
public interface IFooComponent
{
bool Execute();
}
public class FooComponent : IFooComponent
{
public bool Execute()
{
Console.WriteLine("foo component execute");
return true;
}
}
特に制限はない。
インターフェースだけを公開するのはクライアントが必要な知識を減らし、実装を自由にするため。
クライアントクラスを記述する
IFooComponentを利用するクライアントクラスを記述する。
public class FooClient
{
private readonly IFooComponent component;
public FooClient(IFooComponent component)
{
this.component = component;
}
public void Execute()
{
var result = component.Execute();
Console.WriteLine("foo client execute {0}", result);
}
}
コンポーネントと同じく、特に制限はない。
DIコンテナを構成する
コンテナに利用するコンポーネントを登録してコンテナを構成する。
var builder = new ContainerBuilder();
builder.RegisterType<FooClient>();
builder.RegisterType<FooComponent>().As<IFooComponent>();
var container =builder.Build();
クライアント自体の依存関係もメインプログラムから排除したいので、ここではクライアント自体もコンテナに登録する。
DIコンテナから必要なオブジェクトを取り出す
var client = container.Resolve<FooClient>();
client.Execute();
このように、各オブジェクトの生成に関する知識は全く不要になり、コードはすっきりする。
全体のコード
class Program
{
static void Main(string[] args)
{
var container = BuildContainer();
var client = container.Resolve<FooClient>();
client.Execute();
// debug実行時にコンソールが閉じてしまうのを防ぐ。
Console.ReadLine();
}
static IContainer BuildContainer()
{
var builder = new ContainerBuilder();
builder.RegisterType<FooClient>();
builder.RegisterType<FooComponent>().As<IFooComponent>();
return builder.Build();
}
}
public class FooClient
{
private readonly IFooComponent component;
public FooClient(IFooComponent component)
{
this.component = component;
}
public void Execute()
{
var result = component.Execute();
Console.WriteLine("foo client execute {0}", result);
}
}
public interface IFooComponent
{
bool Execute();
}
public class FooComponent : IFooComponent
{
public bool Execute()
{
Console.WriteLine("foo component execute");
return true;
}
}
Autofacのさらなる機能
- Module
- 特定条件のコンポーネントを一括して登録する
- 設定ファイル
Module
複数アセンブリを利用するアプリケーションなどで、DIコンテナの構成をすべて一か所で実施しようとすると構成処理が面倒になりがちなので、アセンブリ単位などでまとめてDIコンテナの構成を行うためModuleを利用する。
特定条件のコンポーネントを一括して登録する
特定アセンブリに含まれるすべてのコンポーネントを一括して登録する、などができる。
利用するコンポーネントの数が増えてくると便利。
設定ファイル
Moduleを利用したとしても、アプリケーションのエントリポイントでコンテナの構成を行う必要があるのは変わりなく、利用するコンポーネントを入れ替えたりするにはコードの書き換えが必要になってしまう。
これを回避するためにModuleや利用コンポーネントの記述を設定ファイルに記述することができる。
その他
- アプリケーションのカスタマイズ版を安全に構築しようとする場合は必須。(継承を使ってうまくコンポーネントを記述してもコンポーネントの生成自体はカスタマイズ版で書き換えなければならない)
- AOPのAspect注入にも利用される。
- DbConnectionやDbContextは構成時にオプション設定が必要な場合があるので、それらを生成するFactoryクラスやFactory delegateを使った方が良い。