sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

DI パターンを体験するために IoC Unity を使ってみる

f:id:shikaku_sh:20210823173353p:plain:w300

IoC Unity とは、ゲームエンジンの Unity ではないです。inversion of control (IoC) コンテナの Unity です。

DI コンテナを C# で体験してみるなら、デファクトスタンダートに近い存在である Unity コンテナを触ってみることにします。

以下のコードは、参考リンクのサンプルコードを個人的に試してみたものになります。

テスト1

class Sample2
{
    public interface IMasterManager
    {
        DataSet Read();
    }

    public class CustomerManager : IMasterManager
    {
        public DataSet Read() => new DataSet("Customer");
    }

    public class SupplierManager : IMasterManager
    {
        public DataSet Read() => new DataSet("Supplier");
    }

    public class ProductManager : IMasterManager
    {
        public DataSet Read() => new DataSet("Product");
    }

    public void Test()
    {
        var container = new UnityContainer();

        container.RegisterInstance<IMasterManager>("Customer", new CustomerManager());
        container.RegisterInstance<IMasterManager>("Supplier", new SupplierManager());
        container.RegisterInstance<IMasterManager>(new ProductManager());

        var manager1 = container.Resolve<IMasterManager>("Customer");
        var manager2 = container.Resolve<IMasterManager>("Supplier");
        var manager3 = container.Resolve<IMasterManager>();

        // ERROR ここからは呼び出せない
        // var manager4 = container.Resolve<IVehicle>("Bike");

        Console.WriteLine(manager1.Read().DataSetName);
        Console.WriteLine(manager2.Read().DataSetName);
        Console.WriteLine(manager3.Read().DataSetName);

        // 1, 2 だけ
        var managers = container.ResolveAll<IMasterManager>();

        foreach (var manager in managers)
        {
            Console.WriteLine(manager.Read().DataSetName);
        }
    }
}

出力結果:

Customer
Supplier
Product
Customer
Supplier

Unity コンテナにオブジェクトを格納して、インターフェース経由でオブジェクトを取得します。 キーになっているものはテキストなので、迷うところはないと思います。

スト2

class Sample
{
    public interface IAnimal
    {
        void Cry();
    }

    public class Cat : IAnimal
    {
        public void Cry() => Console.WriteLine("ニャ~");
    }

    public class Dog : IAnimal
    {
        public void Cry() => Console.WriteLine("バウ!");
    }

    public class Person
    {
        [Dependency("Dog")]
        public IAnimal Pet { get; set; }

        public void Call()
        {
            Pet.Cry();
        }
    }

    public void Test()
    {
        UnityContainer container = new UnityContainer();

        container.RegisterInstance<IAnimal>(nameof(Dog), new Dog());
        container.RegisterInstance<IAnimal>(nameof(Cat), new Cat());

        var person = new Person();

        person = container.BuildUp(person);

        person.Call();
    }
}

出力:

バウ!

Dependency 属性で Person は、Dog を取得します。これもあまり迷うところはないけど、サービスロケータ感がちょっと臭う感じですので、利用の際は注意がちょっとだけ必要かも。

とはいえ、文字列で紐づいているだけなので、コンテナ自身と具体的に結びついているわけではないのがポイント。BuildUp メソッドを呼び出したコンテナが構築する流れです。なので、かなりデメリットを脱臭できていると思います。

基本はコンストラクターからインターフェースを受け取る形式から始めてみて、手に負えないときはこのような属性を利用したセッターを検討するとよいと思います。(Martin Fowler の意見を尊重するなら)

テスト3

class Sample
{
    public class AnimalExtension : UnityContainerExtension
    {
        protected override void Initialize()
        {
            Container.RegisterType(typeof(IAnimal), typeof(Dog), "Dog", TypeLifetime.Transient);
            Container.RegisterType(typeof(IAnimal), typeof(Cat), "Cat", TypeLifetime.Transient);
        }
    }

    public void Test()
    {
        var container = new UnityContainer();

        container.AddNewExtension<AnimalExtension>();

        var person = container.Resolve<Person>();

        person.Call();
    }
}

出力結果:

バウ!

拡張を利用したコンテナ作成のサンプルです。TypeLifetime でインスタンスのライフタイムを決めることができる点がおもしろいです。

テスト4

class Sample
{
    public interface IVehicle
    {
        int Run();
    }

    public class Car : IVehicle
    {
        private int _Miles = 0;
        public int Run() => ++_Miles;
    }

    public class Bike : IVehicle
    {
        private int _Miles = 0;
        public int Run() => ++_Miles;
    }

    public class Driver
    {
        private IVehicle _Vehicle;

        [InjectionConstructor]
        public Driver(IVehicle vehicle)
        {
            _Vehicle = vehicle;
        }

        public void Run() => Console.WriteLine($"Run {_Vehicle.GetType().Name} - {_Vehicle.Run()} mile.");
    }

    public class Driver2
    {
        private IVehicle _Vehicle1;
        private IVehicle _Vehicle2;

        [InjectionConstructor]
        public Driver2(IVehicle vehicle1, IVehicle vehicle2)
        {
            _Vehicle1 = vehicle1;
            _Vehicle2 = vehicle2;
        }

        public void Run()
        {
            Console.WriteLine($"Run2 {_Vehicle1.GetType().Name} - {_Vehicle1.Run()} mile.");
            Console.WriteLine($"Run2 {_Vehicle2.GetType().Name} - {_Vehicle2.Run()} mile.");
        }
    }

    public void Test()
    {
        var container = new UnityContainer();

        container.RegisterType<IVehicle, Car>(nameof(Car));
        container.RegisterType<IVehicle, Bike>(nameof(Bike));

        container.RegisterType<Driver>(nameof(Car) + nameof(Driver), new InjectionConstructor(container.Resolve<IVehicle>(nameof(Car))));
        container.RegisterType<Driver>(nameof(Bike) + nameof(Driver), new InjectionConstructor(container.Resolve<IVehicle>(nameof(Bike))));

        container.RegisterType<Driver2>(
            nameof(Driver2), 
            new InjectionConstructor(
                container.Resolve<IVehicle>(nameof(Car)), 
                container.Resolve<IVehicle>(nameof(Bike))
            )
        );

        var car = container.Resolve<IVehicle>(nameof(Car));

        var carDriver = container.Resolve<Driver>(nameof(Bike) + nameof(Driver));
        var driver2 = container.Resolve<Driver2>(nameof(Driver2));

        car.Run();
        carDriver.Run();
        driver2.Run();
        driver2.Run();
    }

}

出力結果:

Run Bike - 1 mile.
Run2 Car - 1 mile.
Run2 Bike - 1 mile.
Run2 Car - 2 mile.
Run2 Bike - 2 mile.

素直にコンストラクターから DI を受け取る形式です。なので、一番の基本形になるのかなと思います。

サンプルコードとしては、一番長くなったので最後にまわしたけど。

サンプル

GitHub にテストコードをあげています。

参考