前書き
軽いDIコンテナを作る話です。そのため初学者向けの内容となります
Attributeでこんなことできますってサンプルコードにもなっていると思うので、Attribute拡張に触れたことない人は参考にしてみてください
経緯
挑戦と挫折
DIContainerを導入しようと思ったことがありました
候補として
・Zenject
・VContainer
などがありますね
しかし断念しました。
理由としては
・前提としてアーキテクチャに組み込む必要性がある
・今まで携わったプロジェクトでDIContainerの保守性が最悪だった
・前任者のインターフェイスの切り分け方が酷かった
・設計者の思想を理解しないと使えない場合が多い
・寿命の紐づけがわかりずらい
・疎結合になる関係上、コードで追いづらくなってしまう
・注入するオブジェクトの定義が面倒くさい
ざっくりまとめると、
難しくて気軽に使えないよねー
でも使いたい!
どんな時に?
・UIからデータ参照するときの流れをシンプルにしたい
・APIの呼び出しを簡略化したい
・シングルトンを減らしたい
↑この辺はInterface定義して中身をダミーに切り替えてテストしたりとかもできますよね
それ以外にもUnity側の機能を
[Inject] public UnitySoundManager soundManager;
みたいな感じで気軽に取ってこれるようになりたい!
ゆえに使いたい!
そして...
既存のDIContainerの問題点
・使い勝手がシンプルじゃない
・組み込む際にアーキテクチャも一緒に考えないといけない
・部分的に使うことはできるが、そのための定義とか面倒くさい
これらを解決するためには....
そうだ、自作しよう
設計
めっちゃざっくりしたクラス図
コードに起こしてみた
interfaceとAttribute
using System;
namespace TyInjector
{
/// 属性の定義
[System.AttributeUsage(System.AttributeTargets.Field) ]
public class TyInjectAttribute : System.Attribute
{
public TyInjectAttribute()
{
}
}
/// インターフェイス
public interface IInjectable : IDisposable
{
void Inject();
}
}
MonoBehaviourで使えるように基底クラスを定義
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);
}
}
}
コンテナ本体
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);
}
}
}
サンプルコード
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みたいな仕組みにしていきたいですね