DIコンテナを自作してみたかった
Unityで動くDIコンテナを自作してみたかったので、自作しました。
リポジトリを公開していますが、もっと有名なライブラリが存在するし、自分が欲しいと思った機能しか実装していないので、使うメリットはないと思います。 また何か起きても責任を負いません。
導入方法
UPMから導入することを想定しています。 Package Managerからadd package from git URLで下記のリンクを入力するとimportできます。
https://github.com/satoshishi/NeContainer.git?path=unity-project/Assets/NeCo
使い方
hadashiA様のVContainerの書き方を参考にさせて頂いている部分が多いです。
Builderの生成
staticクラスから、builderを生成します。
using NeCo; INeCoBuilder builder = _.Create();
Builderへの登録
builderに依存解決したいクラスを登録します。
// ClassAをSingletonで登録(Containerからインスタンス生成後、共有) builder.RegistrationAsSingleton<ClassA>(); // ClassAをid付きで登録 builder.RegistrationAsSingleton<ClassA>("A"); // ClassAをTransientで登録(Resolveの度にインタスンス生成) builder.RegistrationAsTransient<ClassA>(); // ClassAの生成済みインスタンスを登録(登録したインスタンスを共有) builder.RegistrationAsConstant(instance);
Resolverの生成
Build()からResolverを生成します。
INeCoResolver resolver = builder.Build();
インスタンスの参照
Resolve
ClassA instance = resolver.Resolve<ClassA>(); // ClassA 且つ IdがAのインスタンスを参照 ClassA instance = resolver.Resolve<ClassA>("A");
注入先の指定
[inject]を対象の構文に付けることで、Resolve時に依存注入が実行されます。
using NeCo; // コンストラクタインジェクション public class ClassB { public ClassA a; [Inject] public ClassB(ClassA a) { this.a = a; } } // メソッドインジェクション public class ClassB { public ClassA a; [Inject] private Injection(ClassA a) { this.a = a; } } // プロパティインジェクション public class ClassB { [Inject] public ClassA A { get; private set; } // idを指定してinjection(プロパティインジェクション限定) [InjectFromId("A")] public ClassA A { get; private set; } }
entry pointの指定
builderへのRegistration時にentry pointに指定することで、Build()の時点で依存解決を実行させることができます。
// 第一引数をtrueで、entry pointに指定される builder.RegistrationAsSingleton<ClassA>(true); // この時点でClassAのインスタンスが生成と依存注入が実行される。 builder.Build();
ServiceLocator的な使い方
INeCoResolverを指定することでServiceLocator的な使い方ができます。
public class ClassB { public ClassA a; [Inject] private Injection(INeCoResolver locator) { this.a = locator.Resolve<SampleA>(); } }
Scene上のMonoBehaviourを継承したクラスの登録
Scene上のMonoBehaviourを継承したクラスを登録する際は、特定のメソッドから登録する必要があります。
builder.RegistrationMonoBehaviour_AsConstant(componentA);
Prefabの登録
Prefabを登録する際は、特定のメソッドから登録する必要があります。 登録したPrefabはResolve時にInstantiateされます。
// Resolve時にparentのchildとしてInstantiateする(Instantiate後のResolveは生成済みのインスタンスを返す) builder.RegistrationPrefab_AsSingleton(componentA, new PrefabRegistrationOptions() { IsThisEntryPoint = true, Parent = null, DontDestoryOnLoad = true, Id = "Sample05" }); // Resolve時にparentのchildとしてInstantiateする(Resolveの度にInstantiateを実行) builder.RegistrationPrefab_AsTransient(componentA);
Funcの登録
VContainerに憧れて、Funcを登録できるようにしました。
builder.RegisterFunc_AsSingleton<float, Effect>(resolver => { return damage => { if(damage <= 50f) { // ResolverからDIしながらInstantiate(これも憧れで) return resolver.Instantiate<Effect>(this.normalEffect, null); } return resolver.Instantiate<Effect>(this.criticalEffect, null); }; });
破棄する
BuilderとResolverはDisposeから破棄できます。
builder.Dispose(); resolver.Dispose();
Resolver生成までを補助する機能
Builderへの登録からRevolverを生成するところまで補助する機能を用意しました。
GameObjectにアタッチされているコンポーネントを登録する
登録したいScene上のコンポーネントをアタッチし、スクリプトからRegistrationAndBuild()を実行するとResolverが取得できます。
[SerializeField] private MonoBehaviourRegistrationHelper helper; void Start() { // builderの生成、登録、resolverの生成までを実行する INeCoResolver resolver = helper.RegistrationAndBuild(); }
Prefabを登録する
スクリプトがアタッチされたprefabをアタッチし、スクリプトからRegistrationAndBuild()を実行するとResolverが取得できます。
[SerializeField] private PrefabRegistrationHelper helper; void Start() { // builderの生成、登録、resolverの生成までを実行する INeCoResolver resolver = helper.RegistrationAndBuild(); }
Scene上のHelper群から登録する
RegistrationAndBuild()を実行すると、Scene上にアタッチされているHelperクラス群を参照、登録してResolverを返します。
[SerializeField] private SceneRegistrationHelper helper; void Start() { // builderの生成、登録、resolverの生成までを実行する INeCoResolver resolver = helper.RegistrationAndBuild(); }
Helperクラスを自作する
RegistrationHelperGameObjectというクラスを継承することで、Helperクラスを自作することができます。
public class MyHelper : RegistrationHelperGameObject { public override INeCoBuilder Registration(INeCoBuilder container = null) { // containerへの登録処理を記述 return container; } }
ScriptableObjectから登録する
ScriptableObjectにRegistrationHelperGameObjectを継承したコンポーネントをアタッチすることで、Helper群からResolverを生成することができます。 PrefabRegistrationHelperや自作HelperクラスなどをアタッチしたPrefabを作成し、それをScriptableObjectに登録する。みたいな使い方を想定しています。
[SerializeField] private RegistrationHelperScriptableObject helper; void Start() { // builderの生成、登録、resolverの生成までを実行する INeCoResolver resolver = helper.RegistrationAndBuild(); }
その他
RegistrationHelperGameObjectを継承した自作Helperスクリプトを生成して、そのスクリプトをアタッチしたPrefabを生成するエディタ拡張も作成しましたが、Unity2021のどこかのバージョンでInitializeOnLoadの挙動が変わったようで、現在機能していません。
詰まった所など
当初、dynamic(ExpressionTree)を使って実装しようと思っていたのですが、IL2CPPで動作しないこと(link.xmlを作成すれば一部動作するようですが)を知らず、ILコード周りの知識もなかったので、結局リフレクションで作りました。なので性能は良くないと思います。またNeContainerは依存注入を再帰的に処理しているのですが、依存関係に循環参照があると無限ループで死んでしまうため、対策をする必要がありました。強引な方法ですが、FormatterServices.GetUninitializedObject
で不完全なインスタンスを参照先同士に渡し、各インスタンスが行き渡った後にリフレクションからInvokeして完全体にする方法で対応しました(ただし注入するインタスンスがSingletonでない場合はnullが注入されてしまいます)。