【Unity】DIコンテナVContainerの使い方まとめ

UnityでDIコンテナVContainerの使う方法についてまとめました。

Unity2020.1.10
VContainer 1.4.0

はじめに

VContainerはUnity(ゲームエンジンの方)用のDIコンテナです。

github.com

機能が多すぎず少なすぎず、導入も簡単なのでとても使いやすいDIコンテナになっています。
DIコンテナが多機能すぎると重要な設計がブレやすかったり宗教戦争じみた議論が発生したり色々と大変なので、
程よい大きさのDIコンテナにはとても価値があると思います。

本記事ではこのVContainerの使い方についてまとめます。

とはいっても公式ドキュメントが非常にわかりやすいので、
英語に抵抗ない方はこちらを参照していただくのが良いかと思います。

vcontainer.hadashikick.jp

なお本記事ではDIの基礎知識については前提知識とし、記事内では説明しませんのでご了承ください。

インストール

インストールはmanifest.jsonに以下の一行を追加することで行います。

"jp.hadashikick.vcontainer": "https://github.com/hadashiA/VContainer.git?path=VContainer/Assets/VContainer#1.4.0"

末尾のバージョンに関してはgithubの最新リリースに適宜変更してください。

vcontainer.hadashikick.jp

シンプルな(Service Collectionのような)IoC Containerとして使ってみる

さてそれではまずVContainerをServiceCollectionのようなシンプルなIoCコンテナとして使ってみます。

準備として、以下のようにIFooServiceを実装したFooServiceIBarServiceを実装したBarServiceを用意します。
BarServiceはそのコンストラクタでIFooServiceを受け取ります。

public interface IFooService
{
}

public class FooService : IFooService
{
}

public interface IBarService
{
    IFooService GetFoo();
}

public class BarService : IBarService
{
    private readonly IFooService _fooService;
    
    public BarService(IFooService fooService)
    {
        _fooService = fooService;
    }

    public IFooService GetFoo()
    {
        return _fooService;
    }
}

これらの依存関係をVContainerで解決するには以下のように実装します。

using UnityEngine;
using VContainer;

public class Example : MonoBehaviour
{
    private IObjectResolver _resolver;
        
    private void Start()
    {
        // ContainerBuilderにサービスを登録していく
        var builder = new ContainerBuilder();
        builder.Register<IFooService, FooService>(Lifetime.Singleton);
        builder.Register<IBarService, BarService>(Lifetime.Singleton);
        
        // BuildしてIObjectResolverを得る
        var resolver = builder.Build();
        _resolver = resolver;
        
        // 依存関係の解決されたサービスを取得する
        var service = resolver.Resolve<IBarService>();
        Debug.Log(service.GetFoo()); // FooService
    }

    private void OnDestroy()
    {
        // すべてのIDisposableなサービスがDisposeされる
        _resolver.Dispose();
    }
}

ServiceCollectionとの対比としては、ContainerBuilderServiceCollection相当のもの、
IObjectResolverServiceProvider相当のものという位置づけになります。

VContainerの基本的な使い方

さてVContainerでは前節のような、ContainerBuilderを生成してObjectResolverをビルドしたり、
サービスが有効期間を抜けたらDisposeするような処理をライブラリ側でサポートしてくれます。

以下ではこの方法についてまとめます。

まずLifetimeScopeというVContainerのクラスを継承したクラスを定義します。
そしてConfigure()メソッドをオーバーライドして引数のbuilderに前節のようにサービスを追加していきます。

using VContainer;
using VContainer.Unity;

public class GameLifetimeScope : LifetimeScope // LifetimeScopeを継承
{
    // Confitureをoverride
    protected override void Configure(IContainerBuilder builder)
    {
        // builderにサービスを追加
        builder.Register<IFooService, FooService>(Lifetime.Singleton);
        builder.Register<IBarService, BarService>(Lifetime.Singleton);
    }
}

次にエントリポイントを作っていきます。
適当にBarServiceを使用するクラスを作っておきます。
また、VContainerのIInitializableインターフェースを実装しておきます。

using UnityEngine;
using VContainer.Unity;

public class EntryPoint : IInitializable
{
    private IBarService _barService;
    
    public EntryPoint(IBarService barService)
    {
        _barService = barService;
    }
    
    public void Initialize()
    {
        Debug.Log(_barService.GetFoo());
    }
}

最後にこのエントリポイントを先ほどのIContainerBuilderRegisterEntryPoint()メソッドで登録します。

builder.Register<IFooService, FooService>(Lifetime.Singleton);
builder.Register<IBarService, BarService>(Lifetime.Singleton);
// RegisterEntryPoint()で登録
builder.RegisterEntryPoint<EntryPoint>(Lifetime.Singleton);

このコンポーネントを適当なGameObjectにアタッチすると、
まずエントリポイントとして登録したEntryPointクラスがインスタンス化され、
次にそれに依存しているBarServiceとFooServiceがインスタンス化され依存性注入されます。

そしてEntryPointのInitialize()が呼ばれてエントリポイントが初期化されます。(FooServiceとログ出力されます)
またGameLifetimeScopeがDestroyされた際には生成された各インスタンスIDisposable.Dispose()が(実装されていれば)呼ばれます。

このようにIContainerBuilderに登録したエントリポイントを起点にして処理を進めていくのが基本的な使い方となります。

IInitializable以外のライフサイクルイベント

前節にはEntryPointにIInitializableを実装しましたが、ライフサイクルイベントは他にも存在します。
以下に、Unityのイベントとそれに相当するVContainerのインターフェースを表します。

Unity VContainer
PlayerLoop.Initializationの前 IInitializable
PlayerLoop.Initializationの後 IPostInitializable
MonoBehaviour.Start IStartable
MonoBehaviour.Startの後 IPostStartable
MonoBehaviour.FixedUpdate IFixedTickable
MonoBehaviour.FixedUpdateの後 IPostFixedTickable
MonoBehaviour.Update ITickable
MonoBehaviour.Updateの後 IPostTickable
MonoBehaviour.LateUpdate ILateTickable
MonoBehaviour.LateUpdateの後 IPostLateTickable

生存期間を管理するLifetimeScope

次に生存期間の管理についてまとめます。

以下のように、IContainerBuilder.Register()を行う際にはその引数にLifetimeを指定します。

builder.Register<FooService>(Lifetime.Singleton);

上記のようにLifetime.Singletonとした場合、FooServiceは最初に依存性注入される際にインスタンス化され、
それ以降は何度注入されても同じインスタンスが使いまわされます。
Lifetimeに親子関係があっても一つのインスタンスが共有されますが、
親と子で同じ型がRegisterされていた場合にはそれぞれ別のインスタンスが生成されます。
また所属するLifetimeScopeがDestroyされたタイミングでIDisposable.Dispose()が呼ばれます。

次にLifetime.Transientです。

builder.Register<FooService>(Lifetime.Transient);

これは注入されるたびに新しいインスタンスが生成されます。
生存期間がLifetimeScopeに依存しないためLifetimeScopeがDestroyされてもIDisposable.Dispose()は呼ばれません。

最後にLifetime.Scopedです。

builder.Register<FooService>(Lifetime.Scoped);

これは依存性注入される際に、同じLifetimeScope内に既に作られたインスタンスがあったらそれを使い、なければインスタンス化します。
つまり、親のLifetimeScopeで作られたインスタンスと同じ型のインスタンスが子のLifetimeScopeで注入される際には新しく作られます。
また所属するLifetimeScopeがDestroyされたタイミングでIDisposable.Dispose()が呼ばれます。

LifetimeScopeの親子関係の設定方法

さて次にLifetimeScopeの親子関係を設定する方法についてまとめます。

LifetimeScopeのInspectorから

最も手軽な方法はLifetimeScopeのInspectorから設定する方法です。
Parentの項目からLifetimeScopeのサブクラスの型が一覧化されるので、親にしたい型を選択します。

f:id:halya_11:20201213230932p:plain
LifetimeScopeのインスペクタから親子関係を設定

あとは親がインスタンス化されている状態で子をインスタンス化すれば親子関係が構築されます。

子のシーンやPrefabを読みこんで設定する

次にLifetimeScopeがアタッチされたGameObjectを持つシーンやPrefabを読み込んで子として設定する方法を紹介します。
これを行うには以下のように、子を読み込む前にLifetimeScope.EnqueueParent()に親を渡しておきます。

LifetimeScope someParentScope;
using (LifetimeScope.EnqueueParent(someParentScope))
{
    // ここで子のシーンを読み込んだりPrefabをInstantiateしたりする
}

usingのスコープ内でシーンやPrefabを読み込めば自動的に親子関係が構築されます。

さらにそのまま子のIContainerBuilderを操作したい場合にはLifetimeScope.Enqueue()を使います。

LifetimeScope someParentScope;
using (LifetimeScope.EnqueueParent(someParentScope))
using (LifetimeScope.Enqueue(builder =>
{
    // 生成された子のIContainerBuilderに対する操作
    builder.Register<IFooService>(Lifetime.Scoped);
}))
{
}

なおPrefabを子とする場合には以下のようにLifetimeScope.CreateChildFromPrefab()を使って生成することもできます。

LifetimeScope parentScope;

// 子のLifetimeScopeを作成
var childScope = parentScope.CreateChildFromPrefab(prefab, builder =>
{
    builder.Register<IFooService>(Lifetime.Scoped);
});
コードベースで子を生成する

PrefabやシーンにLifetimeScopeをアタッチするのではなくコードから生成する場合には以下のようにLifetimeScope.CreateChild()を使います。

LifetimeScope parentScope;

// 子のLifetimeScopeを作成
var childScope = parentScope.CreateChild(builder =>
{
    builder.Register<IFooService, FooService>(Lifetime.Scoped);
});

// 破棄
childScope.Dispose();

IContainerBuilderに登録を行う方法まとめ

次にIContainerBuilderに登録する際のいろんなやり方をまとめます。
これに関しては以下のコードにコメントの形でまとめておきます。

using UnityEngine;
using VContainer;
using VContainer.Unity;

public class GameLifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        // クラスの型を登録
        builder.Register<FooService>(Lifetime.Singleton);
        // インターフェースとして登録
        builder.Register<IFooService, FooService>(Lifetime.Singleton);
        builder.Register<FooService>(Lifetime.Singleton).As<IFooService>();
        // インターフェースとして複数登録
        builder.Register<IFooService, IFooService2, FooService>(Lifetime.Singleton);
        builder.Register<FooService>(Lifetime.Singleton).As<IFooService, IFooService2>();
        // 全てのインターフェースを登録
        builder.Register<FooService>(Lifetime.Singleton).AsImplementedInterfaces();
        // 全てのインターフェースとクラスの型を登録
        builder.Register<FooService>(Lifetime.Singleton).AsImplementedInterfaces().AsSelf();
        // コンストラクタ引数を指定しつつクラスの型を登録
        builder.Register<FooService>(Lifetime.Singleton).WithParameter<string>("someValue");
        builder.Register<FooService>(Lifetime.Singleton).WithParameter("argName", "someValue");
        // インスタンスを登録
        builder.RegisterInstance(new FooService());
        // インスタンスをインターフェースとして登録
        builder.RegisterInstance<IFooService>(new FooService());
        builder.RegisterInstance(new FooService()).As<IFooService>();
        builder.RegisterInstance(new FooService()).AsImplementedInterfaces();
        // ファクトリを登録
        builder.RegisterFactory<int, Bar>(x => new Bar(x));
        // ファクトリを登録しつつその中でIObjectResolverを使う
        builder.RegisterFactory<int, BarService>(container =>
        {
            var dependency = container.Resolve<FooService>();
            return x => new BarService(x, dependency);
        }, Lifetime.Scoped);
        // エントリポイントを登録
        builder.RegisterEntryPoint<EntryPoint>(Lifetime.Singleton);
        // エントリポイントを一気に登録
        builder.UseEntryPoints(Lifetime.Scoped, entryPoints =>
        {
            entryPoints.Add<EntryPoint>();
            entryPoints.Add<EntryPoint2>();
            entryPoints.Add<EntryPoint3>();
        });
        
        // 以下はUnity用
        
        // MonoBehaviourを登録
        MonoBehaviour someComponent;
        builder.RegisterComponent(someComponent);
        // シーン上のコンポーネントを登録
        builder.RegisterComponentInHierarchy<FooBehaviour>();
        // シーン上のコンポーネントのインターフェースを登録
        builder.RegisterComponentInHierarchy<FooBehaviour>().AsImplementedInterfaces();
        // Prefabをインスタンス化してそのコンポーネントを登録
        builder.RegisterComponentInNewPrefab(somePrefab, Lifetime.Scoped);
        // Prefabを親を指定してインスタンス化してそのコンポーネントを登録
        builder.RegisterComponentInNewPrefab(somePrefab, Lifetime.Scoped).UnderTransform(parentTransform);
        builder.RegisterComponentInNewPrefab(somePrefab, Lifetime.Scoped).UnderTransform(() => parentTransform);
        // GameObjectをインスタンス化してそのコンポーネントを登録
        builder.RegisterComponentOnNewGameObject<FooBehaviour>(Lifetime.Scoped, "ObjectName");
        // 一気に登録する
        builder.UseComponents(components =>
        {
            components.AddInstance(fooBehaviour);
            components.AddInHierarchy<FooBehaviour>();
            components.AddInNewPrefab(somePrefab, Lifetime.Scoped);
            components.AddOnNewGameObject<FooBehaviour>(Lifetime.Scoped, "ObjectName");
        });
    }
}

色んな依存性注入方法

最後に依存性を注入する方法についてまとめます。

基本的なインジェクション

VContainerでは基本的な依存性注入の方法としてコンストラクタインジェクションとメソッドインジェクション、
プロパティ/フィールドインジェクションが用意されています。

使い方としては以下のようにそれぞれInjectアトリビュートを付けるだけです。

public class BarService : IBarService
{
    // コンストラクタインジェクション
    // ビルド時にストリップされる問題の対策用に明示的にInjectアトリビュートを付ける
    [Inject]
    public BarService(IFooService fooService)
    {
    }
    
    // メソッドインジェクション
    // 基本的にはコンストラクタインジェクションにすべき
    // MonoBehaviourとかコンストラクタが使えないものに使う
    [Inject]
    public void Setup(IFooService fooService)
    {
    }
    
    // プロパティインジェクションとフィールドインジェクション
    [Inject] public IFooService FooService{ get; set; }
    [Inject] public IFooService _fooService;
}
該当する型のインスタンスを全て注入

今、IFooServiceを実装したインスタンスが複数回Registerされているとします。

builder.RegisterInstance(new FooService());
builder.RegisterInstance(new FooService());
builder.RegisterInstance(new FooService());

これらのインスタンスをすべて注入したい場合には、注入される側の引数をIEnuerable型にします。

public class BarService : IBarService
{
    // 登録されているIFooServiceをすべて取得
    // IReadOnlyList<IFooService>で受け取ってもok
    [Inject]
    public BarService(IEnumerable<IFooService> fooServices)
    {
    }
}

これですべてのインスタンスを注入できます。
なおIEnumerableではなくIReadOnlyListでもOKです。

IObjectResolverを注入する

以下のようにIObjectResolverを注入することもできます。

public class BarService : IBarService
{
    // コンテナも注入できる
    [Inject]
    public BarService(IObjectResolver container)
    {
    }
}

参考

github.com

vcontainer.hadashikick.jp