かりんちゃんの随心遊戯日誌

ゲームの日記、たまに政治の話、香港の話

VContainerのファクトリとは何だ

仕事のプロジェクトでVContainerを導入したことで、依存関係の複雑さがかなり解消されたものの、VContainerで提供した1つ機能:Factoryはなんだろうかとずっと疑問を持てて使ってなかった。
名前からすると、新しいオブジェクトを生成させるものであろう。でも、実は昔からわざわざFactoryクラスを作成する趣味はないので、公式の解説も割と曖昧ですし、結局ずっとピンとこなくて使ってなかった。
実は、MVCは理解するものの、コードのわかり易さも重視していますね。なのであちこち抽象クラスを利用してプロジェクト全体が無駄に分かりづらいのはあまり好きではない。

しかし、最近プロジェクトがかなり落ち着いたので、コードカバレッジ向上のために単体テストを見直しました際、どうも、スクリプトのプレハブからGameObjectを生成する部分が複雑しすぎて、再びFactoryを思い出した。
もしかして、Factoryはこのためのものなのでは?とほかのタスクもなく、時間が余裕があったので、調査し始めた。

結論というと、Factoryはとあるオブジェクトを生成するためのもの。

もちろんコンストラクタなどで済ませることができる。しかし、もしその生成に別の依存関係が必要としたり、あるいは煩わしい初期化処理が多い場合
「疎結合化」という観点、そして「管理しやすい」という観点で、Factoryクラスでやらせるほうがコードがスッキリする、ということです。

例えば、今までプレハブからゲームオブジェクトをInstantiateすることだが、そのプレハブではアセットバンドルサービスから暗号化されたフォントを利用しないといけません。
暗号化されたフォントは最初では使用できず、事前にGameObjectでセットすることが出来ないから。
とすると毎回GameObjectをInstantiateするも、アセットバンドルサービスを持ってこないといけない。

勿論、親サービスにアセットバンドルサービスをInjectすることもいいが(生成後セットする)、親サービスでは他の部分がこのアセットバンドルサービスを使うことはないので
わざわざこれだけのためにInjectするのは綺麗ではないし、いらない密結合ができちゃう。

だからだ、このInstantiateをFactoryで行い、さらにアセットバンドルサービスもFactoryクラスに直接Injectする
すると親サービスは単純にFactoryからInvoke(生成するメソッド)を呼び出すだけで、余計なサービスをInjectすることもなく、初期化設定済みのゲームオブジェクトをゲットできる。

疎結合化ができるわけだ。

ただ、RegisterFactoryのやり方は他と違って、自動的に依存するサービスをResolveする能力がありません(他のRegisterInstanceではコンストラクタの依存のものを自動的にResolveする。これはRegisterの仕様を見るとわかるが、RegisterInstanceなどはインターフェースやクラスを直接登録することに対し、RegisterFactoryは単純にFunction(関数)を登録することです。つまり、実はFactoryとかはただこのパターンがよく使われただけで、実際関数を登録するもので、その関数が最終的に指定された型のオブジェクトを返却すれば、別に中身は何でも良いのです。
よって、RegisterFactoryで依存するサービスを先にResolveし、Factoryを作成して、Createを呼び出す。

ちなみにやり方は決まっており、<>の最後は必ず生成するクラスで、その前では対応するパラメータ。

builder.RegisterFactory<int, string, CustomButton>(container =>
{
	var service1 = container.Resolve<IService1>();
	var service2 = container.Resolve<IService2>();
	var view = container.Resolve<SomeView>();
	var factory = new CustomButtonFactory(service1, service2, view);
	return (val1, val2) => factory.Create(val1, val2);
}, Lifetime.Scoped);

という感じです。val1とval2はCustomButtonを生成するためのパラメータ(例えばボタンの文字とか)。

すると、後はCustomButtonFactoryのクラスを用意し、普通にコンストラクタで依存するサービスをInjectして、Createメソッドを作り込めばいいのです。

あとは利用する親サービスにFactoryをInjectし、Invokeを使うだけです。例えば上記の例だと

public ParentService(IOtherService otherService, Func<int, string, CustomButton> factory)
{
	this._factory = factory;
}

public void CreateButton()
{
	var button = this._factory.Invoke(123, "abc");
}

上記の処理ではCreateButtonを呼び出す際、_factory.Invoke(123, "abc")が自動的にサービスをResolveして、factory.Create(123, "abc")を呼び出し、作成したCustomButtonを返却します。


簡単なものだとあまり意味はないが、その生成がとても煩わしい、あるいは他のサービスに依存すると、これを使うほうがスッキリするので
特にプレハブからInstantiateなどでは、覚えたらかなりきれいなコードを書けます。