LoginSignup
31
13

More than 1 year has passed since last update.

VContainer入門(1) - IContainerBuilderとIObjectResolver

Last updated at Posted at 2021-03-21

この記事について

Unity用のDIコンテナであるVCointanerの使い方を解説します。DIコンテナを使うと複雑な依存関係を楽に管理できるようになります。

DI(DependencyInjection)そのものについて詳しい説明はここではしません。

目次ページ: VContainer入門

VContainerの導入

VContainerはUPM(UnityPackageManager)を通してプロジェクトにインポートできます。

まずはUnityで新しいプロジェクトを作成し、プロジェクトのPackagesフォルダにあるmanifest.jsonを開きます。
そしてdependencies内に以下の設定を追加します。

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

これでVContainerが使えるようになりました。

VCointanerを試してみる

動作確認用のクラスを作成

まずはサンプルとして使うための適当な依存関係があるクラスを作っておきます。

Mock.cs
using UnityEngine;
using VContainer;

// 先頭に[Logger]を付けてログ出力するクラス
public sealed class Logger
{
    public void Log(string message) => Debug.Log("[Logger] " + message);
}

// 足し算するだけのクラス
public sealed class Calculator
{
    public int Add(int a, int b) => a + b;
}

// LoggerとCalculatorに依存するクラス
public sealed class HogeClass
{
    private readonly Logger logger;
    private readonly Calculator calculator;

    [Inject]
    public HogeClass(Logger logger, Calculator calculator)
    {
        this.logger = logger;
        this.calculator = calculator;
    }

    public void LoggerTest()
    {
        logger.Log("LoggerTest");
    }

    public void CalculatorTest(int a, int b)
    {
        int result = calculator.Add(a, b);
        logger.Log($"{a} + {b} = {result}");
    }
}

HogeClassはコンストラクタでLoggerCalculatorを受け取ります。[Inject]属性が付いているのはVCointanerがこのコンストラクタを使えるようにするためです。1

VContainerを利用しない場合、HogeClassは以下のように使えます。

var logger = new Logger();
var calculator = new Calculator();

var hoge = new HogeClass(logger, calculator);
hoge.LoggerTest(); // [Logger] LoggerTest と出力される
hoge.CalculatorTest(3, 5); // [Logger] 3 + 5 = 8 と出力される

VContainerはHogeClassにLoggerとCalculatorを渡して生成する部分を肩代わりしてくれます。次の項から実際に使ってみます。

VContinerを使う流れ

VCointanerは以下の流れで使えます。

  1. IContainerBuilderを生成する。
  2. IContainerBuilderに使いたいクラスを登録する。
  3. IContainerBuilderからIObjectResolverを生成する。
  4. IObjectResolverを通して使いたいクラスを生成する。

実際にVContainerを使う際はLifetimeScopeを使うことが多いです。LifetimeScopeIContainerBuilderIObjectResolverをいい感じに管理してくれるクラスです。

このLifetimeScopeを通して使う場合でも内部の動作を知っていた方がわかりやすいので、まずはIContainerBuilderIObjectResolverを直接使ってみます。

実際のコードは以下になります。このコンポーネントを適当なGameObjectにアタッチして実行すると動作確認できます。

TestMonoBehaviour.cs
using UnityEngine;
using VContainer;

public sealed class TestMonoBehaviour : MonoBehaviour
{
    private void Start()
    {
        // 1. IContainerBuilderを生成
        IContainerBuilder containerBuilder = new ContainerBuilder();

        // 2. IContainerBuilderに使いたいクラスを登録
        containerBuilder.Register<Logger>(Lifetime.Singleton);
        containerBuilder.Register<Calculator>(Lifetime.Singleton);
        containerBuilder.Register<HogeClass>(Lifetime.Singleton);

        // 3. IContainerBuilderからIObjectResolverを生成
        using (IObjectResolver objectResolver = containerBuilder.Build())
        {
            // 4. IObjectResolverで使いたいクラスを生成
            HogeClass hoge = objectResolver.Resolve<HogeClass>();

            hoge.LoggerTest(); // [Logger] LoggerTest と出力される
            hoge.CalculatorTest(3, 5); // [Logger] 3 + 5 = 8 と出力される
        }
    }
}

コード中の1、2、3、4をそれぞれ説明していきます。

1. IContainerBuilderを生成

IContainerBuilderの実体としてContainerBuilderクラスを使います。普通にnewで生成します。

2. IContainerBuilderに使いたいクラスを登録 (Register)

IContainerBuilder.Registerを使ってクラスを登録できます。型引数に登録したいクラス、引数にLifetimeを渡します。

Lifetimeは生成されたオブジェクトの生存期間を指定するものです。あとで説明するのでとりあえずLifetime.Singletonを渡しておいてください。

3. IContainerBuilderからIObjectResolverを生成 (Build)

IContainerBuilder.BuildIObjectResolverを生成できます。IObjectResolverはDisposeが必要なのでusingで囲っています。

4. IObjectResolverで使いたいクラスを生成 (Resolve)

IObjectResolver.ResolveでBuildする前に登録しておいたクラスが生成できます。ここではHogeClassを生成しています。型引数に生成したいクラスを指定すればLoggerクラスやCalculatorクラスも生成できます。

Register/Build/Resolveの内部動作

VContainerの内部動作も知っていた方が便利なので簡単に説明しておきます。

Register

Registerした時点では登録されたクラスをContainerBuilderの内部に保存しているだけです。

Build

ContainerBuilderをBuildするとIObjectResolverが生成されます。
この段階で、Registerされたクラス全てをリフレクションで解析して生成に必要な情報を集めます。2

HogeClassの場合はInject属性がついたコンストラクタを発見し、引数にLoggerとCalculatorが必要なことが記録されます。
LoggerとCalculatorはInject属性がついたメンバがないのでただ生成すればいいことが記録されます。

Resolve

Build時に記録した情報に基づいて指定されたクラスのインスタンスを返します。

ここで__Resolve__という単語は「依存関係を解決して生成されたオブジェクトを取り出すこと」を表します。

HogeClassの場合はLoggerとCalculatorが必要なので、まずはLoggerとCalculatorをResolveします。それからこのLoggerとCalculatorを使ってHogeClassのコンストラクタを呼び出してHogeClass自体を生成します。
要するにobjectResolver.Resolve<HogeClass>()の中ではnew HogeClass(new Logger(), new Calculator());と同様のことが実行されています。

依存先のクラス(ここではLoggerやCalculator)も再帰的にResolveされていくのが重要です。
例えば、LoggerがコンストラクタでFileWriterというクラスを受け取らなければならなくなったとします。この場合でもFileWriterがRegisterされていれば自動的にFileWriterがResolveされてLoggerのコンストラクタに渡され、そのLoggerがまたHogeClassに渡されます。依存関係が何段階になっても同様にResolveされていきます。3

Lifetimeについて

あとで説明すると言っていたLifetimeについて説明します。Lifetimeを変えるとResolve時の動作が少し変わります。

LifetimeにはLifetime.SingletonLifetime.TransientLifetime.Scopedの3つがあります。

Lifetime.Singleton

Lifetime.SingletonではResolveで生成されたインスタンスがIObjectResolver内でキャッシュされます。つまり複数回Resolveを呼んでも同じインスタンスが返ってきます。

using (IObjectResolver objectResolver = containerBuilder.Build())
{
    // hogeとhoge2は同じインスタンスになる
    HogeClass hoge = objectResolver.Resolve<HogeClass>();
    HogeClass hoge2 = objectResolver.Resolve<HogeClass>();
}

IObjectResolverごとにキャッシュされるためIObjectResolverが異なると別のインスタンスになります。
次の例では2回BuildしてIObjectResolverを2つ生成しています。

using (IObjectResolver objectResolver = containerBuilder.Build())
using (IObjectResolver objectResolver2 = containerBuilder.Build())
{
    // hogeとhoge2は違うインスタンスになる
    HogeClass hoge = objectResolver.Resolve<HogeClass>();
    HogeClass hoge2 = objectResolver2.Resolve<HogeClass>();
}

Lifetime.Transient

Lifetime.TransientではResolveするたびに別のインスタンスが生成されます。

private void Start()
{
    IContainerBuilder containerBuilder = new ContainerBuilder();

    containerBuilder.Register<Logger>(Lifetime.Singleton);
    containerBuilder.Register<Calculator>(Lifetime.Singleton);
    containerBuilder.Register<HogeClass>(Lifetime.Transient); // Lifetime.Transientを使ってみる

    using (IObjectResolver objectResolver = containerBuilder.Build())
    {
        // hogeとhoge2は違うインスタンスになる
        HogeClass hoge = objectResolver.Resolve<HogeClass>();
        HogeClass hoge2 = objectResolver.Resolve<HogeClass>();
    }
}

HogeClassのRegisterでLifetime.Transientを渡すように変えたのでhogeとhoge2は別のインスタンスになります。

LoggerとCalculatorはLifetime.Singletonのままなので、hogeとhoge2のコンストラクタに渡されるLoggerとCalculatorはそれぞれ同じインスタンスになります。これもLifetime.Transientにすれば別のインスタンスが渡されることになります。

Lifetime.Scoped

Lifetime.ScopedLifetime.Singletonに似ていますがIObjectResolverが親子関係を持ったときの動作が異なります。親子関係と一緒に説明するのでここでは省略します。

IDisposableの自動Dispose

IDisposableを実装したクラスをResolveするとLifetimeによっては自動的にDisposeされます。

  • Lifetime.SingletonLifetime.Scopedの場合、IObjectResolverがDisposeされるとResolveで生成されたインスタンスも一緒にDisposeされます。
  • Lifetime.Transientの場合はDisposeされません。IObjectResolverは作りっぱなしなのでDisposeする責任はインスタンスを渡された側になります。

まとめ

Register -> Build -> Resolveの順に使うイメージを持っておいてください。

次回はVContainerの本来の使い方であるLifetimeScopeを通した方法を説明します。

  1. コンストラクタが複数あるときにVContainerがどのコンストラクタを使うか識別するのに使われます。IL2CPPでストリッピングされないようにする効果もあります。省略しても動きますが必ず付ける方が安全です。

  2. VContainerのコード生成が有効になっているとBuildとResolveのリフレクションが省略されて実行速度が上がります。コード生成についてはいずれ説明します。

  3. 依存関係が循環すると例外が発生します。

31
13
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
13