48
40

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Unity】シンプルなDIコンテナを自作してみた

Last updated at Posted at 2023-10-23

こんにちは!
株式会社OGIXのクライアントエンジニアのK.Iです。(OGIXについてはページ最下部で紹介しています!)

今回はUnityで扱うDIコンテナについて、内部のDI(依存性注入)の構造まで深く紹介している記事が少なかったので、ご紹介します。

まずDIとは

DIDependency Injectionの略で、別名 依存性注入と呼びます。
プログラミングにおけるデザインパターンの一つで、オブジェクトを成立させるために必要となるオブジェクトの参照を実行時に注入(Inject)する手法のことです。

DIという概念自体はとてもシンプルで、
オブジェクトの生成時に注入を行うコンストラクタインジェクション、
生成後にメソッド経由で注入を行うメソッドインジェクションやセッターインジェクションなど
DIという用語を知らなくても、普段から行っているこれらの処理のことを示します。
Unityの GetComponent や SerializeField もDIの1つです。

DIコンテナとは

DIコンテナはDI(依存性注入)を簡易的に行うための仕組みです。

Unityで有名なDIコンテナライブラリとして、以下のものがあります。

  • Extenject(Zenject)
  • VContainer

これらのライブラリでは、[Inject]属性をフィールドやメソッドに付与することで、DIコンテナが自動的に依存性の解決と注入を行います。

特にVContainerはコード数も少なく、DIが高速化される仕組みが備わっています。

こちらの機能を参考にDIコンテナを実装していきます。

DIコンテナに必要な機能

基本機能としては、登録(Register)と依存注入(Inject)が提供できれば機能を実現できます。

登録(Register)

今回は以下の登録方法を提供できるように実装していきます。

  • T型からインスタンスの生成と登録ができる
  • 既に生成されたインスタンスの登録ができる
  • 同じ型の登録を行う場合に、既に登録されているインスタンスを保持し続けるか、新しいインスタンスに差し替えるかを選択することができる

実装したコードがこちらです。

using System;
using System.Collections.Generic;

public class DIContainer
{
    public enum Lifetime
    {
        Singleton,  // 常に同じインスタンスを返す
        Transient   // 新しいインスタンスを生成して返す
    }

    private class Value
    {
        public Lifetime Lifetime { get; set; }
        public object Instance { get; set; }
        public Value(Lifetime lifetime, object instance)
        {
            Lifetime = lifetime;
            Instance = instance;
        }
    }

    private readonly Dictionary<Type, Value> _container = new Dictionary<Type, Value>();

    private readonly static DIContainer _instance = new DIContainer();
    public static DIContainer Instance => _instance;

    // T型からインスタンスの生成と登録を行う
    public void Register<T>(Lifetime lifetime) where T : class, new()
    {
        var type = typeof(T);
        if (_container.TryGetValue(type, out Value value))
        {
            if (value.Lifetime == Lifetime.Singleton)
                return;

            var ins = new T();
            value.Lifetime = lifetime;
            value.Instance = ins;
        }
        else
        {
            var ins = new T();
            _container.Add(type, new Value(lifetime, ins));
        }
    }

    // 既に生成されたインスタンスの登録を行う
    public void Register<T>(T ins, Lifetime lifetime)
    {
        var type = typeof(T);
        if (_container.TryGetValue(type, out Value value))
        {
            if (value.Lifetime == Lifetime.Singleton)
                return;

            value.Lifetime = lifetime;
            value.Instance = ins;
        }
        else
        {
            _container.Add(type, new Value(lifetime, ins));
        }
    }
}

依存性注入(Inject)

今回は、フィールドインジェクション(Field Injection) に焦点を当てています。フィールドインジェクションが機能すれば、他の注入方法を使わなくても問題が生じにくいと考えています。また、フィールドインジェクションは一番宣言が簡潔であり、手軽に利用できる注入方法です。ただし、注意すべき点として、コード上で明示的な代入が行われないため、初めて見る人にとっては少し違和感を感じるかもしれません。

まず、InjectAttributeを作成します。これによりフィールドに [Inject] 属性を付与できるようになります。

using System;

[AttributeUsage(AttributeTargets.Field)]
public class InjectAttribute : Attribute
{
}

次に、DIContainerクラスのInjectメソッドを実装します。このメソッドはobject型の引数を受け取り、指定されたオブジェクトの全フィールドに対して[Inject]属性が付与されている場合、DIコンテナからの依存性の解決と注入を行います。

public class DIContainer
{    
    public void Inject(object target)
    {
        // targetオブジェクトの全フィールドを取得
        var fields = target
            .GetType()
            .GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);

        foreach (var field in fields)
        {
            // [Inject]属性が付与されたフィールドのみ対象とする
            var injectAttribute = field.GetCustomAttributes(typeof(InjectAttribute), true);
            if (injectAttribute.Length <= 0)
                continue;

            if (!_container.TryGetValue(field.FieldType, out Value value))
                continue;
            
            // フィールドに依存注入
            field.SetValue(target, value.Instance);
        }
    }
}

完成したコード

using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;

public class DIContainer
{
    public enum Lifetime
    {
        Singleton,  // 常に同じインスタンスを返す
        Transient   // 新しいインスタンスを生成して返す
    }

    private class Value
    {
        public Lifetime Lifetime { get; set; }
        public object Instance { get; set; }
        public Value(Lifetime lifetime, object instance)
        {
            Lifetime = lifetime;
            Instance = instance;
        }
    }

    private readonly Dictionary<Type, Value> _container = new Dictionary<Type, Value>();

    private readonly static DIContainer _instance = new DIContainer();
    public static DIContainer Instance => _instance;

    // T型からインスタンスの生成と登録を行う
    public void Register<T>(Lifetime lifetime) where T : class, new()
    {
        var type = typeof(T);
        if (_container.TryGetValue(type, out Value value))
        {
            if (value.Lifetime == Lifetime.Singleton)
                return;

            var ins = new T();
            value.Lifetime = lifetime;
            value.Instance = ins;
        }
        else
        {
            var ins = new T();
            _container.Add(type, new Value(lifetime, ins));
        }
    }

    // 既に生成されたインスタンスの登録を行う
    public void Register<T>(T ins, Lifetime lifetime)
    {
        var type = typeof(T);
        if (_container.TryGetValue(type, out Value value))
        {
            if (value.Lifetime == Lifetime.Singleton)
                return;

            value.Lifetime = lifetime;
            value.Instance = ins;
        }
        else
        {
            _container.Add(type, new Value(lifetime, ins));
        }
    }

    public void Unregister<T>() where T : class
    {
        _container.Remove(typeof(T));
    }

    public void UnregisterAll()
    {
        _container.Clear();
    }

    public T Resolve<T>() where T : class
    {
        if (_container.TryGetValue(typeof(T), out Value value))
        {
            return (T)value.Instance;
        }
        return default;
    }

    // 依存性注入
    public void Inject(object target)
    {
        // targetオブジェクトの全フィールドを取得
        var fields = target
            .GetType()
            .GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);

        foreach (var field in fields)
        {
            // [Inject]属性が付与されたフィールドのみ対象とする
            var injectAttribute = field.GetCustomAttributes(typeof(InjectAttribute), true);
            if (injectAttribute.Length <= 0)
                continue;

            if (!_container.TryGetValue(field.FieldType, out Value value))
                continue;
            
            // フィールドに依存注入
            field.SetValue(target, value.Instance);
        }
    }
}

今回紹介しなかったメソッドで、ResolveとUnregisterを追加しています。

  • Unregister は登録したインスタンスの解除
  • Resolve は登録したインスタンスの取得(※メソッドの命名はZnjectを参考にしています)

使い方サンプル

using UnityEngine;

public class TestRunner : MonoBehaviour
{
    private void Start()
    {
        // 登録
        DIContainer.Instance.Register<HogeHogeAAA>(DIContainer.Lifetime.Singleton);
        DIContainer.Instance.Register<HogeHogeBBB>(DIContainer.Lifetime.Singleton);

        var ccc = new HogeHogeCCC();

        ccc.Log("1 >>>");

        Debug.Log("依存注入する");
        DIContainer.Instance.Inject(ccc);

        ccc.Log("2 >>>");
    }
}
public class HogeHogeAAA
{
}
public class HogeHogeBBB
{
}
public class HogeHogeCCC
{
    [Inject] // 依存注入の対象として宣言する
    private HogeHogeAAA _hogeHogeAAA = null;

    // 宣言しない
    private HogeHogeBBB _hogeHogeBBB = null;

    public void Log(string str)
    {
        if (_hogeHogeAAA == null)
        {
            Debug.Log($"{str} _hogeHogeAAA; は nullです。");
        }
        else
        {
            Debug.Log($"{str} _hogeHogeAAA は DIに成功しました。");
        }

        if (_hogeHogeBBB == null)
        {
            Debug.Log($"{str} _hogeHogeBBB は nullです。");
        }
        else
        {
            Debug.Log($"{str} _hogeHogeBBB は DIに成功しました。");
        }
    }
}

実行後のログ結果

Untitled.png

[Inject]属性を付与することで、DIされていることが確認できます。

まとめ

いろいろ紹介しましたが、VContainerを使うのがおすすめです。しかし、ライブラリとして提供されているため、多くの機能が提供されており、初めて使う方には取っつきにくいと感じることもあるかもしれません。しかし、今回のシンプルな実装では本当に必要な機能だけに焦点を当てることができます。各工程の処理の流れも追いやすくなり、結果としてDIコンテナの仕組みや概念を理解しやすいと思いますので、参考にしてみてください!

一緒に働く仲間を募集しています!

株式会社OGIXでは一緒に働いてくれる仲間を募集しています!
エンタメ制作集団としてゲームのみならず、未来を見据えたエンタメコンテンツの開発を行っています。
事業拡大に伴い、エンジニアさんを大募集しています。
興味のある方は下記リンクから弊社のことをぜひ知っていただき応募してもらえると嬉しいです。
▼会社について
https://www.wantedly.com/companies/company_6473754/about
▼代表インタビュー
https://www.wantedly.com/companies/company_6473754/post_articles/443064
▼東京オフィスの応募はこちら
https://www.wantedly.com/projects/1468324
▼新潟オフィスの応募はこちら
https://www.wantedly.com/projects/1468155

48
40
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
48
40

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?