shuhelohelo’s blog

Xamarin.Forms多めです.

DIコンテナについて学ぶ

PrismというMVVMフレームワークではDIコンテナが使われているのですが、「よくわからないけれどフレームワークを使う上でいろいろうまくやってくれてる」ぐらいの理解しかできていませんでした。

DIコンテナはMVVMだけではなく汎用的に使えるもののはずなので、もう少し理解を深めて意図的に使えるようになりたいと思います。

この記事ではUnityというDIコンテナフレームワークをターゲットとします。

Unityのインストール

Nugetから「Unity」で検索すると見つかります。 作者が「Unity Container Project」というやつですね。 f:id:shuhelohelo:20190501120404p:plain

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にしたりとか、そういう感じにしておく必要があるのかな、と。

学んだこと

インターフェースについて理解が進んだ。 「インターフェースに対してプログラミングする」という言葉の意味が理解できた。気がする。