【Unity・C#・Attribute拡張】自作DIコンテナーを作る話

# 前書き
軽いDIコンテナを作る話です。そのため初学者向けの内容となります

Attributeでこんなことできますってサンプルコードにもなっていると思うので、Attribute拡張に触れたことない人は参考にしてみてください

# 経緯
## 挑戦と挫折
DIContainerを導入しようと思ったことがありました

候補として
・Zenject
・VContainer
などがありますね

しかし断念しました。
理由としては
・前提としてアーキテクチャに組み込む必要性がある
・今まで携わったプロジェクトでDIContainerの保守性が最悪だった
　・前任者のインターフェイスの切り分け方が酷かった
  ・設計者の思想を理解しないと使えない場合が多い
・寿命の紐づけがわかりずらい
・疎結合になる関係上、コードで追いづらくなってしまう
・注入するオブジェクトの定義が面倒くさい

ざっくりまとめると、
難しくて気軽に使えないよねー
でも使いたい！
 
 ## どんな時に？
・UIからデータ参照するときの流れをシンプルにしたい
・APIの呼び出しを簡略化したい
・シングルトンを減らしたい
↑この辺はInterface定義して中身をダミーに切り替えてテストしたりとかもできますよね

それ以外にもUnity側の機能を
```
[Inject]　public UnitySoundManager soundManager;
```
みたいな感じで気軽に取ってこれるようになりたい！


ゆえに使いたい！

## そして...
既存のDIContainerの問題点
・使い勝手がシンプルじゃない
  ・組み込む際にアーキテクチャも一緒に考えないといけない
・部分的に使うことはできるが、そのための定義とか面倒くさい

これらを解決するためには....

**そうだ、自作しよう**


# 設計

めっちゃざっくりしたクラス図
```mermaid
classDiagram

class Attribute{
属性基底クラス
}

class InjectAttribute{
注入対象を選択するための属性
}

Attribute <|-- InjectAttribute : 継承


class InjectableMonoBehaviour{
注入可能なMonoBehaviour
}

class InjectableObject{
注入可能なObject
}

class IInjectable{
interface
}

class IDisposable

IDisposable <|-- IInjectable
IInjectable <|-- InjectableObject
IInjectable <|-- InjectableMonoBehaviour

class Container{
寿命管理や参照渡しを行う
void SetInstance(IInjectable injectable)
void Injection(Component target)
}

Container <-- InjectableObject : 生成時に自身のInstanceを渡す
Container <-- InjectableMonoBehaviour : 生成時に自身のInstanceを渡す
Container --> InjectAttribute : インスタンスを渡す


```


# コードに起こしてみた

## interfaceとAttribute
```C#
using System;

namespace TyInjector
{
    /// 属性の定義
    [System.AttributeUsage(System.AttributeTargets.Field)  ] 
    public class TyInjectAttribute : System.Attribute
    {
        public TyInjectAttribute()
        {
        }
    }

    /// インターフェイス
    public interface IInjectable : IDisposable
    {
        void Inject();
    }
}
```

## MonoBehaviourで使えるように基底クラスを定義

```C#
using UnityEngine;

namespace TyInjector
{
    /// Inject可能なMonoBehaviour
    public abstract class TyInjectableMonoBehaviour<T> : MonoBehaviour, IInjectable
    {
        /// Awake時にContainerにインスタンスを登録する
        public void Awake()
        {
            Inject();
        }
        
        /// Destroy時にContainerのインスタンスを削除する
        public void OnDestroy()
        {
            Dispose();
        }

        public void Inject()
        {
            TyContainer.Register<T>(this);
        }

        public void Dispose()
        {
            TyContainer.Unregister<T>(this);
        }
    }
}
```

## コンテナ本体
```C#
using System;
using System.Collections.Generic;

namespace TyInjector
{
    public class TyContainer : SingletonBehaviour<TyContainer>
    {
        /// コンテナに登録されたInstance群
        public Dictionary<Type, object> instances = new Dictionary<Type, object>();

        //注入処理
        public static void Inject<T>(T instance)
        {
            // 定義されているフィールドの取得、publicしか取れない
            var fields = instance.GetType().GetFields();
            foreach (var field in fields)
            {
                //inject属性がついている変数を探す
                var injectable = field.GetCustomAttributes(typeof(TyInjectAttribute), true);
                if (injectable.Length > 0)
                {
                    var fieldType = field.FieldType;
                    if (Instance.instances.ContainsKey(fieldType))
                    {
                        //オブジェクトを注入する
                        field.SetValue(instance, Instance.instances[fieldType]);
                    }
                }
            }
        }
        
        public void RegisterInternal<T>(object instance)
        {
            instances[typeof(T)] = instance;
        }
        
        public void UnregisterInternal<T>(object instance)
        {
            instances[typeof(T)] = instance;
        }
        
        public static void Register<T>(object instance)
        {
            Instance.RegisterInternal<T>(instance);
        }
        
        public static void Unregister<T>(object instance)
        {
            Instance.UnregisterInternal<T>(instance);
        }
    }
}
```


# サンプルコード
```C#
using TyInjector;
using UnityEngine;


public class InjectTest : TyInjectableMonoBehaviour<InjectTest>
{
    public int value = 100;
}

public class Sample : MonoBehaviour
{
    [TyInject] public InjectTest injectTest;

    private void Awake()
    {
        TyContainer.Inject(this);
    }
    
    public void Start()
    {
        Debug.Log(injectTest.value);
    }
}
```

# まとめ
interfaceいらなくね？
って思いながら作ってました

とりあえず最初に設計したもから忠実に作ってみました
現状のコードでは無駄が多く、インスタンスのチェックが薄いですね...

とはいえ、自分のやりたいことの要件の最低限はできている感じです
これをもう少し改良して、MonoBehaviourに特化したDIContainerみたいな仕組みにしていきたいですね