DIコンテナについて学ぶ
PrismというMVVMフレームワークではDIコンテナが使われているのですが、「よくわからないけれどフレームワークを使う上でいろいろうまくやってくれてる」ぐらいの理解しかできていませんでした。
DIコンテナはMVVMだけではなく汎用的に使えるもののはずなので、もう少し理解を深めて意図的に使えるようになりたいと思います。
この記事ではUnityというDIコンテナフレームワークをターゲットとします。
Unityのインストール
Nugetから「Unity」で検索すると見つかります。 作者が「Unity Container Project」というやつですね。
DIのメリットについて
Unityに限らずですがDIコンテナのメリットの一つとして、あるオブジェクトのインスタンスを生成するときに、そのオブジェクトが必要とする他のオブジェクトのインスタンスを生成する手間がなくなる、という点があります。
例えば、クラスAがあって、クラスAの中でクラスB、クラスC、クラスDのインスタンスを使うというケースを考えてみましょう。
クラスAの中でクラスB、クラスC、クラスDのインスタンスを生成するのは密結合でテスト容易性や開発の効率性の点でよろしくありません。
クラスAはクラスB、C、Dが完成しないと手をつけられませんし、テスト時にクラスAのみの動作をテストすることもできません。
class Program { static void Main(string[] args) { Console.WriteLine("Hello World!"); var a = new A(); a.Do(); } } class A { public void Do() { var b = new B(); var c = new C(); var d = new D(); b.Do(); c.Do(); d.Do(); } } class B { public void Do() { } } class C { public void Do() { } } class D { public void Do() { } }
このクラスAがクラスB、C、Dに影響うけまくり(依存しまくり)の状況を解決するために、DI(Dependency Injection)パターンを使います。
クラスAの中で使うクラスB、C、DのインスタンスをクラスAの外側で作って、クラスAに渡します。
とはいえ、これではまだ何も状況は良くなっていません。 まだクラスAはクラスB、C、Dが完成するまで何もできません。テストも。
class Program { static void Main(string[] args) { Console.WriteLine("Hello World!"); //Aの外でインスタンス化 var b = new B(); var c = new C(); var d = new D(); //Aのコンスタクタ経由でB、C、Dのインスタンスを渡す var a = new A(b, c, d); a.Do(); } } class A { private B _b; private C _c; private D _d; public A(B b, C c, D d) { this._b = b; this._c = c; this._d = d; } public void Do() { this._b.Do(); this._c.Do(); this._d.Do(); } } class B { public void Do() { } } class C { public void Do() { } } class D { public void Do() { } }
そこで、クラスB、C、Dが完成していなくても完成するまで代わりのもの(Mock)を使えるようにすればいいじゃない、ということで、各オブジェクトをインターフェース経由で受け取るようにします。
クラスA、B、Cのそれぞれにインターフェースを用意し、それぞれインターフェースを実装するようにします。
このようにすることでクラスAはクラスB、C、Dが完成していなくてもクラスAの開発ができるようにとりあえずのインスタンスを使うことができますし、テストもクラスA単体でできるようになります。
class Program { static void Main(string[] args) { Console.WriteLine("Hello World!"); //Aの外でインスタンス化 var b = new B(); var c = new C(); var d = new D(); //Aのコンスタクタ経由でB、C、Dのインスタンスを渡す var a = new A(b, c, d); a.Do(); //インターフェースを実装していさえすればOK var bModoki = new BModoki(); var cModoki = new CModoki(); var dModoki = new DModoki(); var a2 = new A(bModoki, cModoki, dModoki); a2.Do(); } } //クラスB、C、D用のインターフェース interface IB { public void Do(); } interface IC { public void Do(); } interface ID { public void Do(); } class A { private IB _b; private IC _c; private ID _d; public A(IB b, IC c, ID d) { this._b = b; this._c = c; this._d = d; } public void Do() { this._b.Do(); this._c.Do(); this._d.Do(); } } class B : IB { public void Do() { } } class C : IC { public void Do() { } } class D : ID { public void Do() { } } class BModoki : IB { public void Do() { } } class CModoki : IC { public void Do() { } } class DModoki : ID { public void Do() { } }
DIの復習になってしまいましたが、まあDIパターンの場合、あるクラスで使う別のクラスのインスタンスを用意して、それを渡してやる必要があるわけです。
上の例でいえば、クラスAをインスタンス化する箇所すべてで行う必要があります。
クラスAをnew
するときはこれだけ書かなきゃいけません。大変。
//Aの外でインスタンス化 var b = new B(); var c = new C(); var d = new D(); //Aのコンスタクタ経由でB、C、Dのインスタンスを渡す var a = new A(b, c, d); a.Do();
DIコンテナを使うとこうなります。
static void Main(string[] args) { Console.WriteLine("Hello World!"); var container = new UnityContainer(); //型引数にはインターフェースとその具象クラスをペアで指定 //そのインターフェースの引数には、ペアとなる具象クラスのインスタンスを使う、 //という指定。 container.RegisterType<IB, B>(); container.RegisterType<IC, C>(); container.RegisterType<ID, D>(); //クラスB、C、Dのインスタンスを生成し、 //クラスAのコンストラクタに渡して、 //クラスAのインスタンスを生成 var a = container.Resolve<A>(); a.Do(); //クラスAのインスタンス生成は1行でOK var aa = container.Resolve<A>(); var aaa = container.Resolve<A>(); var aaaa = container.Resolve<A>(); }
例えば、クラスBはまだ開発中だからとりあえず代わりのオブジェクトを使うようにしておこう、といった場合、RegisterType
メソッドのところで以下のようにすればクラスBのインスタンスではなくクラスBModokiのインスタンスが使われるようになります。
static void Main(string[] args) { Console.WriteLine("Hello World!"); var container = new UnityContainer(); //型引数にはインターフェースとその具象クラスをペアで指定 //そのインターフェースの引数には、ペアとなる具象クラスのインスタンスを使う、 //という指定。 container.RegisterType<IB, BModoki>();//BModoki container.RegisterType<IC, C>(); container.RegisterType<ID, D>(); //クラスBModoki、C、Dのインスタンスを生成し、 //クラスAのコンストラクタに渡して、 //クラスAのインスタンスを生成 var a = container.Resolve<A>(); a.Do(); //クラスAのインスタンス生成は1行でOK var aa = container.Resolve<A>(); var aaa = container.Resolve<A>(); var aaaa = container.Resolve<A>(); }
便利ですね。 これは一番単純な使い方で、Unityはとても柔軟な利用ができるようです。
コンテナオブジェクトの置き場所は?
よくわかっていないので自信は全然ないのですが、やはり、プロジェクト、ソリューションの中のどこからでも使える位置に置いておく必要があるのかな、と思います。
そして、一度生成したらその一つだけが使われるようにSingletonにしたりとか、そういう感じにしておく必要があるのかな、と。
学んだこと
インターフェースについて理解が進んだ。 「インターフェースに対してプログラミングする」という言葉の意味が理解できた。気がする。