6. VContainerを使う
しかし次に問題になってくるのが、DIの作業が大変すぎるということです。別にMonoBehaviourを使っていたときも十分大変でしたが、Pure C#ではクラス数が多くなればなるほど、どのインスタンスを先に生成し、そしてどこに渡せばいいのかということが煩雑になり手に負えなくなります。
// クラスが増えるほどComposition Rootが肥大化していく...
public class GameLifeTimeScope : MonoBehaviour
{
[SerializeField] ScoreView scoreView;
[SerializeField] InventoryView inventoryView;
[SerializeField] BattleView battleView;
[SerializeField] MapView mapView;
// ... Viewが増えるたびにここも増える
void Start()
{
// 生成順序を間違えると動かない!
var damageCalculator = new DamageCalculator();
var scoreRule = new ScoreRule();
var inventoryRule = new InventoryRule();
var battleRule = new BattleRule(damageCalculator);
var mapRule = new MapRule();
var enemyManager = new EnemyManager(damageCalculator, scoreRule);
var questManager = new QuestManager(battleRule, inventoryRule, mapRule);
// ... 際限なく増えていく。DIの地獄!
}
}そこで登場するのがDIコンテナです。
DIコンテナとは
DIコンテナとは、クラスの登録と依存解決を自動で行ってくれる仕組みです。開発者はクラスを登録するだけで、DIコンテナが以下の作業を全て自動でやってくれます。
- インスタンスの生成
- コンストラクタの引数を解析し、必要な依存先を特定する
- 依存先のインスタンスを自動で注入する
- 生成順序の自動解決(Aを作るにはBが必要、Bを作るにはCが必要...を自動で解決)
- インスタンスのライフタイム管理(シングルトン、スコープ付きなど)
VContainerとは
VContainerは、Unity向けに作られた高速・軽量なDIコンテナフレームワークです。すこし昔はZenjectというフレームワークがよく使われていましたが、現在ではVContainerの方が主流です。
LifetimeScopeを使ったComposition Root
VContainerでは LifetimeScope というクラスを継承してComposition Rootを作成します。先程の手動DIの例を、VContainerを使って書き直してみましょう。
using VContainer;
using VContainer.Unity;
// VContainerのLifetimeScopeを継承する — これがComposition Rootになる
public class GameLifeTimeScope : LifetimeScope
{
[SerializeField] ScoreView scoreView;
protected override void Configure(IContainerBuilder builder)
{
// Pure C#クラスの登録
builder.Register<DamageCalculator>(Lifetime.Singleton);
builder.Register<ScoreRule>(Lifetime.Singleton);
builder.Register<EnemyManager>(Lifetime.Singleton);
// Presenterをエントリーポイントとして登録
builder.RegisterEntryPoint<ScorePresenter>();
// MonoBehaviourの登録(シーン上のインスタンスを渡す)
builder.RegisterComponent(scoreView);
}
}
// → 「何を生成するか」を宣言するだけ!
// → 生成順序やコンストラクタへの注入は全てVContainerが自動で行う
// → クラスが増えても1行追加するだけで済む手動で書いていたComposition Rootと比べ、でインスタンスを生成して引数で渡す手間が消えとてもスッキリしました。
Register
VContainerにクラスを登録するには builder.Register<T>(Lifetime) を使います。Lifetime 引数によってインスタンスのライフタイムを指定します。特に何もなければ Singleton にするとよいです。
// Pure C#クラスの登録(生存期間を明示的に指定する)
builder.Register<ScoreRule>(Lifetime.Singleton); // アプリ全体で1つのインスタンス
builder.Register<EnemyManager>(Lifetime.Scoped); // スコープ内で1つのインスタンス
builder.Register<DamageEffect>(Lifetime.Transient); // 注入のたびに新しいインスタンスインターフェースと実装を紐付けて登録することもできます。
// IRouteSearchを要求されたらAStarRouteSearchを注入する
builder.Register<AStarRouteSearch>(Lifetime.Singleton).As<IRouteSearch>();MonoBehaviourの登録には専用のメソッドを使います。
// シーン上の既存のMonoBehaviourを登録([SerializeField]で参照を持つ)
builder.RegisterComponent(scoreView);
// ヒエラルキーから自動的にMonoBehaviourを探して登録
builder.RegisterComponentInHierarchy<ScoreView>();RegisterEntryPoint
VContainerの大きな特徴の一つが、Pure C#のクラスをUnityのライフサイクルイベントに接続できることです。MonoBehaviourを使わずに、Pure C#のクラスで Start() や Update() 相当の処理を実行できます。そうなると、エントリーポイントですらPure C#で書くことができるようになります。
builder.RegisterEntryPoint<T>() でエントリーポイントとして登録し、対応するインターフェースを実装することで、それぞれのタイミングで処理が実行されます。
// Pure C#のクラスをエントリーポイントにできる!
public class ScorePresenter : IStartable, IDisposable
{
readonly ScoreRule scoreRule;
readonly ScoreView scoreView;
readonly CompositeDisposable compositeDisposable = new();
// VContainerが自動でコンストラクタ引数を注入してくれる
public ScorePresenter(ScoreRule scoreRule, ScoreView scoreView)
{
this.scoreRule = scoreRule;
this.scoreView = scoreView;
}
// IStartableを実装 → MonoBehaviourのStart()と同じタイミングで呼ばれる
void IStartable.Start()
{
scoreRule.Score.Subscribe(score =>
{
scoreView.SetScore(score);
}).AddTo(compositeDisposable);
}
public void Dispose()
{
compositeDisposable.Dispose();
}
}これにより、DIの手間を考える必要がなくなりました。単一責任になるようにクラスを分割することもよりやりやすくなり嬉しいです。