この記事について
Unity用のDIコンテナであるVCointanerの使い方を解説します。DIコンテナを使うと複雑な依存関係を楽に管理できるようになります。
DI(DependencyInjection)そのものについて詳しい説明はここではしません。
目次ページ: VContainer入門
VContainerの導入
VContainerはUPM(UnityPackageManager)を通してプロジェクトにインポートできます。
まずはUnityで新しいプロジェクトを作成し、プロジェクトのPackagesフォルダにあるmanifest.json
を開きます。
そしてdependencies内に以下の設定を追加します。
"jp.hadashikick.vcontainer": "https://github.com/hadashiA/VContainer.git?path=VContainer/Assets/VContainer#1.8.0"
これでVContainerが使えるようになりました。
VCointanerを試してみる
動作確認用のクラスを作成
まずはサンプルとして使うための適当な依存関係があるクラスを作っておきます。
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
はコンストラクタでLogger
とCalculator
を受け取ります。[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は以下の流れで使えます。
-
IContainerBuilder
を生成する。 -
IContainerBuilder
に使いたいクラスを登録する。 -
IContainerBuilder
からIObjectResolver
を生成する。 -
IObjectResolver
を通して使いたいクラスを生成する。
実際にVContainerを使う際はLifetimeScope
を使うことが多いです。LifetimeScope
はIContainerBuilder
とIObjectResolver
をいい感じに管理してくれるクラスです。
このLifetimeScope
を通して使う場合でも内部の動作を知っていた方がわかりやすいので、まずはIContainerBuilder
とIObjectResolver
を直接使ってみます。
実際のコードは以下になります。このコンポーネントを適当なGameObjectにアタッチして実行すると動作確認できます。
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.Build
でIObjectResolver
を生成できます。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.Singleton
・Lifetime.Transient
・Lifetime.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.Scoped
はLifetime.Singleton
に似ていますがIObjectResolverが親子関係を持ったときの動作が異なります。親子関係と一緒に説明するのでここでは省略します。
IDisposableの自動Dispose
IDisposableを実装したクラスをResolveするとLifetimeによっては自動的にDisposeされます。
-
Lifetime.Singleton
かLifetime.Scoped
の場合、IObjectResolverがDisposeされるとResolveで生成されたインスタンスも一緒にDisposeされます。 -
Lifetime.Transient
の場合はDisposeされません。IObjectResolverは作りっぱなしなのでDisposeする責任はインスタンスを渡された側になります。
まとめ
Register -> Build -> Resolveの順に使うイメージを持っておいてください。
次回はVContainerの本来の使い方であるLifetimeScopeを通した方法を説明します。