DIコンテナを自作してみたかった

Unityで動くDIコンテナを自作してみたかったので、自作しました。

リポジトリを公開していますが、もっと有名なライブラリが存在するし、自分が欲しいと思った機能しか実装していないので、使うメリットはないと思います。 また何か起きても責任を負いません。

github.com

導入方法

UPMから導入することを想定しています。 Package Managerからadd package from git URLで下記のリンクを入力するとimportできます。

https://github.com/satoshishi/NeContainer.git?path=unity-project/Assets/NeCo

使い方

hadashiA様のVContainerの書き方を参考にさせて頂いている部分が多いです。

github.com

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()から依存解決したインスタンスTが参照することができます。

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が注入されてしまいます)。

learn.microsoft.com