Extenject(Zenject)のTestがNSubstituteをサポートしていた
割と最近の出来事だった
元々Zenjectはテスト時のモックライブラリとしてMoqを採用していたんだけど、このPRでまた別のモックライブラリであるNSubstituteを使えるようになった。選択肢が増えたのだった。
Moq
NSubstitute
使い方
まだreleasesには含まれていないのでmasterブランチから直接取ってくることにはなる。
Assets\Plugins\Zenject\OptionalExtras
に新しくAutoSubstitute.zip
があるので、展開してAssets\Plugins\Zenject\OptionalExtras\TestFramework
下に放り込んであげましょう。
雑にIFooを作る
public interface IFoo { string SayFoo(string suffix); }
DummyObjectを作るだけなら、FromSubstitute
を呼ぶだけでOK
Container.Bind<IFoo>().FromSubstitute();
メソッドが呼ばれたかとか諸々確認したいときはRecievedをメソッドチェーンに挟む
var foo = Substitute.For<IFoo>(); // 引数で"Bar"が渡されると"FooBar"を返す。それ以外が来るとFail foo.Received().SayFoo("Bar").Returns("FooBar");
すでにUnityで使ってる事例があったのでこっちを見るほうがよさそう。 qiita.com
どちらを選ぶかは割と好みだけど、個人的にはNSubstituteの方がメソッドチェーンで書きやすくて好き。
29歳になった
ケムリクサの最終話を観てたら歳を取っていた。たつき監督ありがとう。
去年は焼き鳥食べてたらしいです。
http://adarapata.hatenablog.com/entry/2018/03/28/225817
ちょうど誕生日と引越しの荷造りが重なっててあまり落ち着いて祝えないので4月くらいに自分へのご褒美に馬刺しを買おう。
ゲーム業界に転職してちょうど丸一年ぐらいにもなる。機能実装したりテスト書いたり賑やかしたり、毎日Unity触ってるというのはよく考えたら凄い変化だ。社内の人の知見は大変に有益で、時折わからない。やはりそれぞれの専門領域が非常に深く一回聞いただけで理解とは行かないことも多い。もうちょい筋肉をつけなければならない。
仕事以外でも継続的にイベントごとで変わらず活動できてるので割といい感じな気がする。Gotanda.unity参加したり完全理解系したり八耐やったり技術書典で本書いたりと。
ここ最近で大きく変わったなあと思うのは、Unityゲーム開発者ギルドに入ってから知り合いが増えたことだろうか。 scrapbox.io
割と毎日雑談したり相談したりしているおかげで、外部イベントの時にあの人ですね!が増えて大変に便利。僕は割としょうもない話をしています。
20代最後なので何か大きいことするか!と一瞬考えたけど、30になった瞬間急激に老化する呪いにかかってるわけではないので引き続きゆっくり好きなことやっていこうと思います。
好きなことだと最近はボイトレを半年くらいやってます。課題曲が聖飢魔IIの「蝋人形の館」なので先生の前でずっとシャウトしてます。半年くらいでようやく喉を痛めにくくなってきたので日々の積み重ねはやはり大事だなと思った。非常に楽しいので80くらいまでやりたい。
例のものを置いておきますので各位よろしくお願いします。 t.co
ZenjectのConstructionMethodたち
ZenjectでBindするとき、そのインスタンスをどのように用意するかという設定が必要です。その場で初期化するのか、既存のインスタンスを引っ張ってくるのかなど。これらはコンストラクションメソッドと呼ばれており、READMEに全部書かれています。
割と数が多いのでまとめてみました。間違っているものがあればコメントなどもらえるとありがたいです。
- FromNew()
- FromInstance(T instance)
- FromMethod(Func<T> method)
- FromMethod(Func<InjectContext, T> method)
- FromMethodMultiple(Func<IEnumerable<T>> method)
- FromMethodMultiple(Func<InjectContext, IEnumerable<T>> method)
- FromFactory<IFactory<T>>()
- FromIFactory(Action<ConcreteBinderGeneric<IFactory<T>>> factoryBindGenerator)
- FromComponentInNewPrefab(UnityEngine.Object prefab)
- FromComponentsInNewPrefab(UnityEngine.Object prefab)
- FromComponentInNewPrefabResource(string path)
- FromComponentsInNewPrefabResource(string path)
- FromNewComponentOnNewGameObject()
- FromNewComponentInNewPrefab(UnityEngine.Object prefab)
- FromNewComponentInNewPrefabResource(string path)
- FromNewComponentOn(GameObject gameObject)
- FromNewComponentOn(Func<InjectContext, GameObject> gameObjectGetter)
- FromNewComponentSibling()
- FromNewComponentOnRoot()
- FromResource(string path)
- FromScriptableObjectResource(string path)
- FromNewScriptableObjectResource(string path)
- FromComponentInHierarchy()
- FromComponentsInHierarchy()
- FromComponentSibling()
- FromComponentsSibling()
- FromComponentInParents()
- FromComponentsInParents()
- FromComponentInChildren()
- FromComponentsInChildren()
- FromResolve()
- FromResolveAll()
- FromResolveGetter<T>(Func<T, T2> getter)
- FromResolveAllGetter<T>(Func<T, T2> getter)
- FromSubContainerResolve()
- FromSubContainerResolveAll()
- SubContainerのConstruction Method
- ByNewPrefabMethod(UnityEngine.Object prefab, Action<DiContainer> installerMethod)
- ByNewPrefabInstaller<TInstaller>(UnityEngine.Object prefab)
- ByNewPrefabResourceMethod(string resourcePath, Action<DiContainer> installerMethod)
- ByNewPrefabResourceInstaller<TInstaller>(string resourcePath)
- ByNewGameObjectInstaller<TInstaller>()
- ByNewGameObjectMethod(Action<DiContainer> installerMethod)
- ByMethod(Action<DiContainer> installerMethod)
- ByNewGameObjectInstaller<TInstaller>()
- ByNewContextPrefab(UnityEngine.Object prefab)
- ByNewContextPrefabResource(string resourcePath)
- ByInstance(DiContainer subContainer)
- おわりに
- 追記
FromNew()
コンストラクタを呼び出します。特にConstruction Methodを定義しない場合デフォルトでFromNewが呼ばれます
Container.Bind<Foo>().FromNew().AsCached();
Container.Bind<Foo>().AsCached(); // FromNew()は省略できる
コンストラクタが複数あった場合、最初に引数なしコンストラクタを呼ぼうとします。見つからなかった場合引数ありコンストラクタを呼びます。
FromInstance(T instance)
既存のインスタンスを渡してBindします。
Container.Bind<Foo>().FromInstance(new Foo()); Container.BindInstance(new Foo()); // 同じ挙動
ユニークなインスタンスを明示的に渡すので、ScopeをAsTransientにしても効果はありません。実質強制的なAsCached指定?
FromMethod(Func<T> method)
FromMethod(Func<InjectContext, T> method)
インスタンスの生成をメソッドに移譲します。
private Foo GetFoo() => new Foo(); Container.Bind<Foo>().FromMethod(GetFoo).AsCached(); Container.Bind<Foo>().FromMethod(() => new Foo()).AsCached(); Container.Bind<Foo>().FromMethod(injectContext => new Foo()).AsCached();
InjectContext
はInjectに関するメタ情報が詰まったクラスですが、基本的には使うことはないと思います。
FromMethodMultiple(Func<IEnumerable<T>> method)
FromMethodMultiple(Func<InjectContext, IEnumerable<T>> method)
配列やリストの生成をメソッドに移譲します。
private IEnumerable<Foo> GetFoos() => new List<Foo>(); Container.Bind<Foo>().FromMethodMultiple(GetFoos).AsCached(); Container.Bind<Foo>().FromMethodMultiple(injectContext => new List<Foo>()).AsCached();
複数形である以外は FromMethod
と同様です。
FromFactory<IFactory<T>>()
任意のIFactoryクラスで生成します。
public class Foo { public class Factory : PlaceholderFactory<Foo> { } } Container.Bind<Foo>().FromFactory<Foo.Factory>();
Zenject Factoryパターンについてはこちらも合わせてどうぞ。
FromIFactory(Action<ConcreteBinderGeneric<IFactory<T>>> factoryBindGenerator)
カスタムファクトリで生成します。
public class Foo { public Foo(string hoge) { } public class Factory : IFactory<Foo> { private string _arg; public Factory(string arg) { _arg = arg; } public Foo Create() => new Foo(_arg); } } Container.Bind<Foo>().FromIFactory(x => x.To<Foo.Factory>().WithArguments("hoge")).AsCached();
FromFactory
との違いは、Factory生成の自由度です。 FromFactory
は暗黙的にFromNew()
で生成されるためFactory自体の生成方法に制限がかかりますが、こちらは自由に定義できます。引数つけてもいいしSubContainerからとってきてもよい。
公式ドキュメントだとScriptableObjectからFactoryを引っ張ったりしてます。
class FooFactory : ScriptableObject, IFactory<Foo> { public Foo Create() { // ... return new Foo(); } } // ScriptableObjectから生成したFooFactoryからFooを生成してBindする Container.Bind<Foo>().FromIFactory(x => x.To<FooFactory>().FromScriptableObjectResource("FooFactory")).AsSingle();
FromComponentInNewPrefab(UnityEngine.Object prefab)
InstantiateしたPrefabからComponentをBindします。
Container.Bind<FooBehavior>().FromComponentInNewPrefab(fooPrefab).AsCached();
内部的にはGetComponentInChildren
で探すので、複数あった場合は先に見つかった方をBindします。
FromComponentsInNewPrefab(UnityEngine.Object prefab)
FromComponentInNewPrefabの複数版です。
Container.Bind<FooBehavior>().FromComponentsInNewPrefab(fooPrefab).AsCached();
こちらはGetComponentsInChildren
で探します。
FromComponentInNewPrefabResource(string path)
パスからPrefabをInstantiateしてBindします。
Container.Bind<FooBehavior>().FromComponentInNewPrefabResource("some/foo").AsCached();
内部的にはResources.Load
が走ります
FromComponentsInNewPrefabResource(string path)
FromComponentInNewPrefabResourceの複数版です。
Container.Bind<FooBehavior>().FromComponentsInNewPrefabResource("some/foo").AsCached();
FromNewComponentOnNewGameObject()
空のGameObjectを生成してAddComponentしたものをBindします。
Container.Bind<FooBehavior>().FromNewComponentOnNewGameObject().AsCached();
FromNewComponentInNewPrefab(UnityEngine.Object prefab)
PrefabをInstantiateしてAddComponentしたものをBindします。
Container.Bind<FooBehavior>().FromNewComponentInNewPrefab(fooPrefab).AsCached();
FromNewComponentInNewPrefabResource(string path)
パスからPrefabをInstantiateしてAddComponentしたものをBindします。
Container.Bind<FooBehavior>().FromNewComponentInNewPrefabResource("some/foo").AsCached();
パスから読み込むのでResources.Load
が呼ばれます。
FromNewComponentOn(GameObject gameObject)
FromNewComponentOn(Func<InjectContext, GameObject> gameObjectGetter)
既存のGameObjectにAddComponentしたものをBindします。
Container.Bind<FooBehavior>().FromNewComponentOn(fooGameObject);
FromNewComponentSibling()
依存しているComponentと同階層にAddComponentしたものをBindします。
public class BarBehavior : MonoBehavior { [Inject] private FooBehavior _foo; } Container.Bind<FooBehavior>().FromNewComponentSibling();
この場合、BarBehavior
がアタッチされているGameObjectにFooBehavior
がAddComponentされます。
この特性から、Injectされる側がComponentでないと使えません。また、複数依存しているオブジェクトがあった場合それぞれの同階層にAddComponentされます。よって強制的にAsTransient指定されるイメージです。AsSingleやAsCachedを明示的に呼んでもエラーは出ませんが適用はされません。
FromNewComponentOnRoot()
現在のContextと同じ階層にAddComponentしたものをBindします。
Container.Bind<FooBehavior>().FromNewComponentOnRoot().AsCached();
例えばSceneContextでInstallerを呼んだ場合はSceneContextと同じ階層にFooBehaviorが作成されます。基本的にSceneContextで使うことはなく、GameObjectContext
などのSubContainerで使うのがメインになるでしょう。
FromResource(string path)
Resourceディレクトリのファイルを読み込んでBindします。
Container.Bind<Texture>().FromResource("some/texture").AsCached();
Resources.Load
でロードできるファイルは全部対応してます。
FromScriptableObjectResource(string path)
ResourceディレクトリのScriptable Objectを読み込んでBindします。
Container.Bind<FooScriptable>().FromScriptableObjectResource("some/foo").AsCached();
直接元データをBindするので、動的に値を更新するとファイルが書き換わってしまうので注意
FromNewScriptableObjectResource(string path)
ResourceディレクトリのScriptable Objectから読み込んでコピーしたインスタンスをBindします。
Container.Bind<FooScriptable>().FromNewScriptableObjectResource("some/foo").AsCached();
元データを書き換えたくない場合はこちらを使いましょう。
FromComponentInHierarchy()
シーンのヒエラルキーを辿ってコンポーネントを探してBindします。
Container.Bind<FooBehavior>().FromComponentInHierarchy();
GetComponentOnChildren
で検索します。ParentContractがある場合親のシーンも辿ると書いてるけど検証できていない・・
FromComponentsInHierarchy()
FromComponentInHierarchyの複数版です。
Container.Bind<FooBehavior>().FromComponentInHierarchy()
こちらはGetComponentsOnChildren
で検索します。
FromComponentSibling()
依存しているComponentの同階層を検索してBindします。
public class BarBehavior : MonoBehavior { [Inject] private FooBehavior _foo; } Container.Bind<FooBehavior>().FromComponentSibling();
上記の場合、BarBehaviorと同じ階層にアタッチされているFooBehaviorをInjectします。 この特性から、Injectされる側(今回はFooBehavior)がComponentでないと使えません。
内部的にはGetComponent
を呼んでいます。
FromComponentsSibling()
FromComponentSiblingの複数版です。
Container.Bind<FooBehavior>().FromComponentsSibling();
内部的にはGetComponents
を呼んでいます。
FromComponentInParents()
依存しているComponentの同階層と、親を検索してBindします。
Container.Bind<FooBehavior>().FromComponentInParents();
内部的にはGetComponentInParent
が呼ばれています。
FromComponentsInParents()
FromComponentInParentsの複数版です。
FromComponentInChildren()
依存しているComponentの同階層と子を検索してBindします。
Container.Bind<FooBehavior>().FromComponentInChildren();
内部的にはGetComponentInChildren
が呼ばれています。
FromComponentsInChildren()
FromComponentInChildrenの複数版です。内部的にはGetComponentInChildren
が呼ばれています。
FromResolve()
Containerから検索して再度Bindします。基本的に使うことはありませんが、インタフェースを別のインタフェースにBindしたいときなどに使えます。
public interface IFoo { } public interface IBar : IFoo { } public class Foo : IBar { } Container.Bind<IFoo>().To<IBar>().FromResolve(); Container.Bind<IBar>().To<Foo>();
上記の場合、IBarとしてBindされたFooをIFooとしてもBindします。
FromResolveAll()
FromResolveの複数版です。
FromResolveGetter<T>(Func<T, T2> getter)
Containerから取得したオブジェクトから取得してBindします。
Container.Bind<Foo>().FromResolveGetter<Bar>(bar => bar.GetFoo());
階層的な構造になっている場合は結構便利です。
FromResolveAllGetter<T>(Func<T, T2> getter)
FromResolveGetterの複数版です。
FromSubContainerResolve()
SubContainerから検索してBindします。使う場合SubContainerを作成する必要があるので、このメソッドの後にさらにSubContainerのConstruction Methodを呼ぶことになります。
FromSubContainerResolveAll()
FromSubContainerResolve()
の複数版です。
SubContainerのConstruction Method
ByNewPrefabMethod(UnityEngine.Object prefab, Action<DiContainer> installerMethod)
Prefabからインスタンスを生成し、それにSubContainerを持たせてメソッドで初期化します。
public GameObject SubContainerPrefab; public override void InstallBindings() { Container.Bind<Foo>().FromSubContainerResolve() .ByNewPrefabMethod(SubContainerPrefab, InstallSubContainer).AsCached(); } private void InstallSubContainer(DiContainer container) { container.Bind<Foo>().AsCached(); }
この方法で生成されたインスタンスは自動でGameObjectContext
がアタッチされます。なのでどんなPrefabでもSubContainerを持つことができます。
ByNewPrefabInstaller<TInstaller>(UnityEngine.Object prefab)
Prefabからインスタンスを生成し、それにSubContainerを持たせてInstallerで初期化します。
Container.Bind<Foo>().FromSubContainerResolve().ByNewPrefabInstaller<FooInstaller>(SubContainerPrefab); class FooInstaller : Installer { public override void InstallBindings() { Container.Bind<Foo>(); } }
基本的な挙動はByNewPrefabMethodと同じで、SubContainerへのBindがInstallerクラスに変わっただけです。
ByNewPrefabResourceMethod(string resourcePath, Action<DiContainer> installerMethod)
Resourcesからインスタンスを生成し、それにSubContainerを持たせてメソッドで初期化します。
Container.Bind<Foo>().FromSubContainerResolve().ByNewPrefabResourceMethod("Path/To/Prefab", InstallFoo); void InstallFoo(DiContainer subContainer) { subContainer.Bind<Foo>(); }
Resources.Load
が走ります。
ByNewPrefabResourceInstaller<TInstaller>(string resourcePath)
Resourcesからインスタンスを生成し、それにSubContainerを持たせてInstallerでBindします。
Container.Bind<Foo>().FromSubContainerResolve().ByNewPrefabResourceInstaller<FooInstaller>("Path/To/MyPrefab"); class FooInstaller : Installer<FooInstaller> { public override void InstallBindings() { Container.Bind<Foo>(); } }
TInstaller : InstallerBase
なので、Installer<T>
を継承したもののみ使えます。
MonoInstaller
はできないので注意
ByNewGameObjectInstaller<TInstaller>()
空のGameObjectを生成し、それにSubContainerを持たせてInstallerでBindします。
Container.Bind<Foo>().FromSubContainerResolve().ByNewGameObjectInstaller<FooInstaller>(); class FooInstaller : Installer<FooInstaller> { public override void InstallBindings() { Container.Bind<Foo>(); } }
後述のByInstaller<TInstaller>
とほぼ同じ動きをしますが、空のGameObjectにGameObjectContextがアタッチされているのがポイントです。IInitializable
やITickable
なども適切にBindされるし、GameObjectを破棄すればSubContainerは破棄されます。
ByNewGameObjectMethod(Action<DiContainer> installerMethod)
空のGameObjectを生成し、それにSubContainerを持たせてメソッドでBindします。
Container.Bind<Foo>().FromSubContainerResolve()
.ByNewGameObjectMethod(InstallSubContainer);
void InstallSubContainer(DiContainer subContainer)
{
subContainer.Bind<Foo>();
}
メソッドでBindする以外はByNewGameObjectInstaller
と同じです。
ByMethod(Action<DiContainer> installerMethod)
メソッドでSubContainerを初期化します。
Container.Bind<Foo>().FromSubContainerResolve()
.ByMethod(InstallSubContainer);
void InstallSubContainer(DiContainer subContainer)
{
subContainer.Bind<Foo>();
}
こちらはITickable
IInitializable
IDisposable
といったインタフェースを適切にBindできません。それらを処理するKernelクラスがいないからです。もし必要ならWithKernel()
を合わせて呼びます。
Container.Bind<Foo>().FromSubContainerResolve().ByMethod(InstallSubContainer).WithKernel();
ByNewGameObjectInstaller<TInstaller>()
InstallerでSubContainerを初期化します。
Container.Bind<Foo>().FromSubContainerResolve().ByInstaller<FooInstaller>(); class FooInstaller : Installer<FooInstaller> { public override void InstallBindings() { Container.Bind<Foo>(); } }
こちらもByMethod
同様に、Kernel不在のためITickable
を処理できません。WithKernel()
を呼ぶとよいでしょう。
ByNewContextPrefab(UnityEngine.Object prefab)
GameObjectContext
をアタッチしたPrefabでSubContainerを初期化します。
Container.Bind<Foo>().FromSubContainerResolve().ByNewContextPrefab(MyPrefab); class FooFacadeInstaller : MonoInstaller { public override void InstallBindings() { Container.Bind<Foo>(); } }
もちろんPrefab側にはGameObjectContext
がアタッチされている必要があるので注意。
ByNewContextPrefabResource(string resourcePath)
GameObjectContext
をアタッチしたPrefabをLoadしてSubContainerを初期化します。
Container.Bind<Foo>().FromSubContainerResolve().ByNewContextPrefabResource("Path/To/MyPrefab");
Resources.Load
が呼ばれます。
ByInstance(DiContainer subContainer)
直接SubContainerとなるDiContainerを渡します。
おわりに
多すぎる。
追記
この記事を書くにあたってドキュメント見てたら間違いっぽいのを見つけたのでPR出した。 github.com
ブログ書くのはいいぞ。
Zenject Factoryの話
はじめに
このエントリはUNIBOOK10で書いた記事を転載したものです。
UNIBOOK10、いい話がいっぱいあるから買ってね!
Zenject Factory
ZenjectはUnityで動作するDIライブラリです。煩わしい依存関係の解決を助ける非常に強力な武器となりますが、 強力かつ多機能であるためどこから学んでいくべきか悩むことも多いです。公式のドキュメントはとても充実していますが、前提となる知識が多かったり、基本的なBindingの話以外は日本語での資料が少ないというのも理由の1つでしょう。
この記事では、ZenjectがもつFactoryによる動的なオブジェクト生成について簡単な解説を行います。
動的に生成したインスタンスへのインジェクション
シーン読み込み時にInstallerを実行して、オブジェクトをバインドし依存関係を解決していくというのがZenjectのスタンダードな使い方でしょう。Containerはシーン開始時の1回のみインジェクションしてくれます。
しかし、実際の開発においてすべてのインスタンスが初期化時に存在するとは限りません。たとえば、アクションゲームで敵キャラが動的に生成されることは十分に考えられるでしょう。
次の例で考えてみましょう。
- Enemyは動的に生成される
- Enemyはプレイヤーに依存している
- PlayerはContainerにバインドされている
- 敵の種類は今後増える可能性がある
最低限のコードで書くとこうなります。
public class Player { } public interface IEnemy { } public class Enemy : IEnemy { Player _player; public Enemy(Player player) { _player = player; } } public class MainGameInstaller : MonoInstaller<MainGameInstaller> { public override void InstallBinding() { Container.Bind<Player>().AsCached(); } }
「今後増える可能性がある」という前提があるのでIEnemyインタフェースを定義して複数に対応できるようにしています。 このEnemyを扱いたい場合どのように書けばよいでしょう。EnemyはPlayerに依存しているので、生成する側はコンストラクタで渡すためのPlayerを知っている必要があります。
Enemyを生成するクラスEnemySpawnerを素直に書くと次のようになるでしょう。
public class EnemySpawner { [Inject] Player _player; List<IEnemy> _enemies = new List<IEnemy>(); public void Spawn() { var enemy = new Enemy(_player); _enemies.Add(enemy); } }
これにより、使う側はSpawnerさえ持っておけばPlayerのことは知らずに済みます。 しかしこの方針だと、Enemyの更新に伴いSpawnerのもつオブジェクトが増えていきます。 たとえばEnemyがPlayerだけでなくて難易度情報なども必要になってくるとどうなるでしょう。
public class Enemy : IEnemy { Player _player; Difficulty _difficulty; public Enemy(Player player, Difficulty difficulty) { _player = player; _difficulty = difficulty; } } public class EnemySpawner { [Inject] Player _player; [Inject] Difficulty _difficulty; List<IEnemy> _enemies = new List<IEnemy>(); public void Spawn(){ var enemy = new Enemy(_player, _difficulty); _enemies.Add(enemy); } }
Enemyが膨れ上がるにつれEnemySpanwerも修正が必要になり管理コストが上がっていきます。 EnemySpawnerはEnemyを生成したいだけあり、どうやって生成するかなどのEnemyの内部実装をそこまで知る必要はありません。Spanwer自体はプレイヤーのことも難易度のことも関心がないのです。 今後別のIEnemyが実装されたときにまた別の引数が必要になるかもしれません。それらをSpawnerが把握するのは大変です。
一応、DiContainerを渡せばどれだけ引数が増えても対応は可能です。
public class Enemy : IEnemy { Player _player; Difficulty _difficulty; public Enemy(DiContainer container) { _player = container.Resolve<Player>(); _difficulty = container.Resolve<Difficulty>(); } } public class EnemySpawner { [Inject] DiCotainer _container; List<IEnemy> _enemies = new List<IEnemy>(); public void Spawn(){ var enemy = new Enemy(_container); _enemies.Add(enemy); } }
しかしこれは、次の問題を生み出してしまいます。
- Player,Difficultyが存在するのか実行時まで気づきにくい
- DiContainerへの依存が発生し、結果的に依存先が1つ増えている
仮にDifficultyがバインドされていない場合、それに気づくことができるのはSpawnを呼び出したタイミングになってからです。Zenjectはシーン内の依存関係を解決するバリデーション機能がエディタ上で実行できて大変便利なのですが、それらの恩恵に預かれないのはもったいないです。
また、コード量は減りましたが「コンテナそのものに依存する」という新たな依存が発生しています。これはサービスロケータと呼ばれるデザインパターンでDIとはまた異なってきます。
これらの問題を解決するためには、Enemyを生成したいEnemySpawnerから、生成のために必要な関心事を切り出してあげたほうがよさそうです。このような場合Fatoryパターンが効果的です。
Factoryパターン
関心事を切り出すために、Enemyの生成とそれに必要な情報を集約したFactoryクラスを定義します。
public class Enemy : IEnemy { Player _player; public Enemy(Player player) { _player = player; } public class Factory { readonly Player _player; public Factory(Player player) { _player = player; } public Enemy Create() { return new Enemy(_player); } } } public class EnemySpawner { [Inject] Enemy.Factory _enemyFactory; List<IEnemy> _enemies = new List<IEnemy>(); public void Spawn() { var enemy = _enemyFactory.Create(); _enemies.Add(enemy); // 何かする } } public class MainGameInstaller : MonoInstaller<MainGameInstaller> { public override void InstallBinding() { Container.Bind<Player>().AsCached(); Container.Bind<Enemy.Factory>().AsCached(); } }
先ほどとの違いは2点です。
- Enemy.Factoryクラスが定義された
- EnemySpawnerからEnemyに関連する要素がなくなった
Enemy.FactoryはEnemyインスタンスを作るだけのクラスです。インスタンスを作るために必要な要素をすべて持っており、外部から引数を渡さずともCreate()を呼ぶだけでEnemyを返してくれます。これを事前にバインドしています。
これにより、EnemySpawnerはEnemyではなくEnemy.Factoryに依存します。Enemyのコンストラクタを呼びだす必要がなくなったので不要なメンバ変数が消えてFactory.CreateするだけでEnemyを取得できるようになりました。 仮に今後IEnemyの種類が増えても、実態を知らずともそれぞれのFactoryを呼びだすだけで取得できるので差し替えが以前より簡単になりました。
Factoryパターンのメリットは依存度を下げられることにあります。直接実態を生成しようとするとどうしても依存関係を知る必要があるため、コードのあちこちにクラスの役割とは直接の関係がないコンストラクタ呼び出しのためのメンバ変数が増えてしまいます。これは避けられないので、ならば生成処理を一か所に集約することで依存関係を把握しなければいけないクラスを減らしてしまおうというやり方です。
なお、現状は特定のFactoryに依存しているのでIEnemyの実体を動的に切り替えということはできません。
BindFactory
先ほどは自前でFactoryパターンの実装を行いましたが、Zenjectでは、Factoryパターンを簡単に実装できるような仕組みがあります。
PlaceholderFactory
を実装してBindFactory
でバインドするだけです。
public class Enemy { Player _player; public Enemy(Player player) { _player = player; } public class Factory : PlaceholderFactory<Enemy> { } } public class MainGameInstaller : MonoInstaller<MainGameInstaller> { public override void InstallBinding() { Container.Bind<Player>().AsCached(); Container.BindFactory<Enemy, Enemy.Factory>(); } }
挙動は先ほどと同じですが、かなりコードがシンプルになりました。変更したのは次の二点です。
- Enemy.FactoryにPlaceholderFactory
を継承させた - 通常のBindをやめてContainer.BindFactory<Enemy, Enemy.Factory>()を呼んだ
元々作っていたfactoryの実装は「Enemyのコンストラクタに必要なインスタンスの保持」と「インスタンスの生成」でしたが、PlaceholderFactory<T>
は先述した2つの機能を備えています。
なので単純な依存関係の管理と生成だけなら継承するだけで全く同じことができます。
BindFactory<Enemy, Enemy.Factory>()
は、Enemyの生成を行うFactoryクラスをバインドします。FactoryはEnemyの依存関係を見て、コンテナから引数に該当するオブジェクトをよしなに渡してくれます。
今回の場合だとバインドされたPlayerをコンストラクタとして渡してくれます。
また、PlaceholderFactoryは動的にパラメータを渡すこともできます。たとえば、敵の移動速度だけ外部から流し込めるようにしたいときは次のとおりです。
public class Enemy : IEnemy { readonly Player _player; readonly float _speed; public Enemy(float speed, Player player) { _player = player; _speed = speed; } public class Factory : PlaceholderFactory<float, Enemy> { } } public class EnemySpawner { [Inject] Enemy.Factory _enemyFactory; List<IEnemy> _enemies = new List<IEnemy>(); public void Spawn() { var enemy = _enemyFactory.Create(Random.Range(1F, 100F)); _enemies.Add(enemy); } } public class MainGameInstaller : MonoInstaller<MainGameInstaller> { public override void InstallBinding() { Container.Bind<Player>().AsCached(); Container.BindFactory<float, Enemy, Enemy.Factory>(); } }
PlaceholderFactory<float, Enemy>
のように、先頭に渡したい型を指定します。
Bind時も同様にBindFactoryの先頭に型を指定するだけです。
これでEnemy.Factory.Createに引数を持たせれるので動的にパラメータを渡せます。
ちなみに、コンストラクタを呼べないMonoBehaviourの場合は次のとおりです。
public class Enemy : MonoBehaviour, IEnemy { Player _player; [Inject] public void Construct(Player player) { _player = player; } public class Factory : PlaceholderFactory<Enemy> { } } public class MainGameInstaller : MonoInstaller { public GameObject EnemyPrefab; public override void InstallBindings() { Container.Bind<Player>().AsSingle(); Container.BindFactory<Enemy, Enemy.Factory>() .FromComponentInNewPrefab(EnemyPrefab); } }
コンストラクタがメソッドインジェクションに変わるだけでほとんど同じです。
先ほどの例だとIEnemyは一種類でしたが、数が増えてきたらどう扱うべきでしょうか。
public interface IEnemy { } public class Slime : IEnemy { } public class Orc : IEnemy { }
SlimeとOrcのどちらが生成されるか、実行時に確定させたい場合どうしたらいいでしょうか。この場合はBindFactory時に型を指定するだけでよいです。
public class EnemyFactory : PlaceholderFactory<IEnemy> { } public class EnemyInstaller : MonoInstaller { public bool UseOrc; public override void InstallBindings() { if (UseOrc) { Container.BindFactory<IEnemy, EnemyFactory>().To<Orc>(); } else { Container.BindFactory<IEnemy, EnemyFactory>().To<Slime>(); } } } public class EnemySpawner { EnemyFactory _factory List<IEnemy> _enemies = new List<IEnemy>(); public EnemySpawner(EnemyFactory factory) { _factory = factory; } public void Spawn() { var enemy = _factory.Create(); _enemies.Add(enemy); } }
Toで明示的に型を指定すれば任意のインスタンスを生成するEnemyFactoryになります。EnemySpawner側はもはや一切の実態を気にすることなくIEnemyの管理に集中できます。
カスタムファクトリ
上記の例ではInstallerでFactoryの種類を決めていました。しかし場合によっては実行中に動的にFactoryを切り替えたい場合があります。 その場合はどうやって切り替えるか、というロジックもFactoryに内包させられると使う側は楽になるでしょう。しかしPlaceholderFactoryにそのような機能はありません。
ここは、大元であるIFactoryインタフェースを実装した自前のカスタムファクトリを作る必要があります。
今回は「難易度で生成する敵を変更する」カスタムファクトリを作ります。
public enum Difficulty { Noraml, Hard } public interface IEnemy { } public class Orc : IEnemy { public class Factory : PlaceholderFactory<Orc> { } } public class Slime : IEnemy { public class Factory : PlaceholderFactory<Slime> { } } public class EnemyFactory : PlaceholderFactory<IEnemy> { } public class CustomEnemyFactory : IFactory<IEnemy> { Orc.Factory _orcFactory; Slime.Factory _slimeFactory; Difficulty _difficulty; public CustomEnemyFactory(Orc.Factory orcFactory, Slime.Factory slimeFactory, Difficulty difficulty) { _orcFactory = orcFactory; _slimeFactory = slimeFactory; _difficulty = difficulty; } public IEnemy Create() { if(_difficulty == Difficulty.Hard) { return _orcFactory.Create(); } return _slimeFactory.Create(); } } public class EnemySpawner { EnemyFactory _enemyFactory; List<IEnemy> _enemies = new List<IEnemy>(); public EnemySpawner(EnemyFactory enemyFactory) { _enemyFactory = enemyFactory; } public void Spawn() { var enemy = _enemyFactory.Create(); _enemies.Add(enemy); } } public class EnemyInstaller : MonoInstaller { public override void InstallBindings() { Container.Bind<Difficulty>().FromInstance(Difficulty.Noraml).AsCached(); Container.BindFactory<Orc, Orc.Factory>(); Container.BindFactory<Slime, Slime.Factory>(); Container.BindFactory<IEnemy, EnemyFactory>() .FromFactory<CustomEnemyFactory>(); } }
ロジックはすべてIFactory
閑話:空のFactoryは必要か
毎回PlaceholderFactory<T>
を継承しただけのFactoryクラスを定義していますが、厳密には定義しなくてもバインドはできます。
次のようにPlaceholderFactoryを直接バインドすることも可能です。
Container.BindFactory<Enemy, PlaceholderFactory<Enemy>>();
メンバ変数で持つときも同様に直接PlaceholderFactory
しかし、ドキュメントではこれを非推奨としています。理由は次の二点です。
- パラメータの変更があった場合にコンパイルエラーが発生しないので気づくのが遅くなる
PlaceholderFactry<T>
よりT.Factory
の方が可読性が高いと考えている。
たとえば、PlaceholderFactory
public class Enemy : IEnemy { readonly Player _player; readonly float _speed; public Enemy(float speed, Player player) { _player = player; _speed = speed; } } public class EnemySpawner { [Inject] PlaceholderFactory<float, Enemy> _enemyFactory; List<IEnemy> _enemies = new List<IEnemy>(); public void Spawn() { var enemy = _enemyFactory.Create(Random.Range(1F, 100F)); _enemies.Add(enemy); } } public class MainGameInstaller : MonoInstaller<MainGameInstaller> { public override void InstallBinding() { Container.Bind<Player>().AsCached(); Container.BindFactory<Enemy, PlaceholderFactory<Enemy>>(); } }
このコードはコンパイルは通りますが、実行時にエラーを出してしまいます。理由は単純で、バインド処理のところにfloat型を追加するのを忘れているからです。
PlaceholderFactory
上記の理由から、基本的にはEnemy.FactoryのようにネストしたFactoryを作ることが推奨されています。 特に強い理由がなければその都度定義した方がよいでしょう。
終わりに
Zenjectは強力ですがその分難易度も高いと言われています。特に動的なインスタンスのInjectはやり方をしっかり考えないとContainerを各所に引きずり回してインジェクションなどをやってしまいがちです。これはコードをより難解なものにしてしまうので、Factoryを用いた設計を覚えてシンプルな仕組みに書き直していくのがよいでしょう。
Zenject Decorator Binding
ZenjectにはDecoratorパターンをサポートするDecorator Bindingなる機能があったりします。あんまりメインで使うような機能ではありませんが覚えておくと便利かもしれません。
Decorator パターン
DecoratorパターンはGoFで定義されてるデザインパターンの一つです。既存のオブジェクトに対して動的な機能追加を行う場合などに効果を発揮します。 例えば超絶シンプルなRPGを作るときに、攻撃力とHPを持つクラスがあるとします。ステータス部分は共通のインタフェースとして、戦士と魔法使いの二種類を作ります。
public interface IStatus { int Life { get; } int Attack { get; } } public class Knight : IStatus { public int Life => 20; public int Attack => 10; } public class Wizard : IStatus { public int Life => 15; public int Attack => 2; }
これらはIStatusで抽象化できるので、攻撃の数値部分などは同じように扱えます。ところでRPGにはやはり攻撃力アップなどバフ効果も欲しいですね。その場合はAttackの補正処理をかけることになるのですがどこに実装しましょう。例えばIStatusを持った抽象クラスのBaseCharacterなどが生まれて、そこにバフクラスを渡すような処理にするのもありだと思います。
public abstruct class BaseCharacter : IStatus { public virtual int Life { get; } public virtual int Attack { get; } public void SetBuffStatus(IStatusBuffer buff) { // 内部パラメータをあれこれする処理 } } var knight = new Knight(); knight.SetBuffStatus(new AttackUpBuffer()); // バフをかける
この場合、バフの種類が増えていくとちょっと大変そうですね。メソッド内に変更の実装を書くならSetBuffStatusが肥大化します。じゃあBuff側に実装を書いてそのメソッドを呼ぶだけ、という形にすると今度はBuff側から更新できるようにBaseCharacterがパラメータ更新メソッドを外部に公開することになります。どちらにせよ、既存への影響を与えがちです。気合でなんとかしましょう。
Decoratorパターンは、上からインスタンスを被せることで変更を少なくします。
// 攻撃力を高めるバフ public class AttackUpDecorator : IStatus { private readonly IStatus _decorator; public int Life => _decorator.Life; public int Attack => _decorator.Attack * 2; // 強い! public AttackUpDecorator(IStatus decorator) { _decorator = decorator; } } var knight = new Knight(); var buffKnight = new AttackUpDecorator(knight); // バフをかける
同じinterfaceを持ったDecoratorクラスを用意することで、同じ振る舞いを持たせたまま拡張ができるようになります。
この実装方法のメリットは以下です。
- 既存のオブジェクトへの変更が少ない
- 呼び出し側が変化を気にしなくてよい
すでに動いているプロジェクトに対して効果的だったりします。
Zenjectでの実装方法
Zenjectでバインドしているインスタンスに同様のDecorateを行いたい場合は以下で実現できます。
public class StatusInstaller : MonoInstaller { public override void InstallBindings() { // いつものバインド Container.Bind<IStatus>().To<Knight>().AsCached(); // IStatusをAttackUpDecoratorでDecorateする Container.Decorate<IStatus>().With<AttackUpDecorator>().AsCached(); } }
Decorate<T>().With<InheritedT>
といった感じで一行で実装できます。便利。この場合ResolveAttackUpDecorator
です。
どう使うの?
上記のようなRPGはかなり極端な例なので実際使えるかという推敲すべきですが、デバッグ、プロファイリングの処理を書くには有効ではないかなと思います。あとゲームで言うとローディング中の表示をDecorateで切り出すのももしかしたらありかもしれないですね。
public interface IAsyncLoadable<T> { Task<T> Load(); } public class FooLoader : IAsyncLoadable<Foo> { public async Task<Foo> Load() { // なんか読み込みロジック } } public class LoadingIconDecorator : IAsyncLoadable<T> { private readonly IAsyncLoadable<T> _decorator; public LoadingIconDecorator(IAsyncLoadable<T> decorator) { _decorator = decorator; } public async Task<T> Load() { ShowLoadIcon(); // ローディングアイコン出すなどの処理 await _decorator.Load(); CloseLoadIcon(); // 閉じるなど } } Container.BindInterfacesAndSelfTo<FooLoader>().AsCached(); Container.Decorate<IAsyncLoadable<Foo>>() .With<LoadingIconDecorator>() // FooLoader実行時にローディングアイコンを出すようにDecorate .AsCached();
実際便利かはわからないので誰かやってみて!
性質上、DecoratorContextとの相性はよいのでこちらも併せて使っていくといい感じに分離できておススメです。
https://github.com/svermeulen/Zenject#scene-decorators
ただ、Bind終わった後に動的なDecorateしたいなあという気持ちが個人的には強かったりする。シーン開始時は何もないけど、ゲーム中に動的にFactoryから生成されたものにDecorateされたりとか。その辺うまくやれないか考えてみよ。
InjectとInjectLocal attribute
Zenjectのソース読んでたらInjectLocal
というAttributeが内部にあったのでなんだろうと調べたメモ。実用的なやつではない。
どのContainerからInjectされるのか
そもそも、Bindされたオブジェクトはどこから依存しているオブジェクトをもらえるのでしょう。これは基本的には自身がBindされているContainerになります。次のコードを見てみましょう。Fooに依存しているBarを生成するコードです。
public class Foo { public ContainerName { get; private set; } public Foo(string containerName) { ContainerName = containerName; } } public class Bar { [Inject] Foo _foo; public void DoBar() => Debug.Log(_foo.ContainerName); }
public class FooInstaller : MonoInstaller { public override void InstallBindings() { // 現在のContainerとProjectContextの両方でFooを用意しておく ProjectContext.Instance.Container.Bind<Foo>().AsCached().WithArguments("ProjectContext"); Container.Bind<Foo>().AsCached().WithArguments("Scene"); var subContainer = Container.CreateSubContainer(); subContainer.Bind<Foo>().AsCached().WithArguments("Sub"); // 現在のContainerにBarをBindする Container.Bind<Bar>(); Container.Resolve<Bar>().DoBar(); } }
=> Scene
この場合、Barに渡されるFooインスタンスは同じContainerに所属しているインスタンスを渡されます。 同じContainerにFooが存在しない場合、親のContainerを探しに行きます。親も持っていなかったらさらにその親・・と最終的にProjectContextまで辿っていくことになります。逆に自身のSubContainerを見に行くなど下っていくことはありません。
上を辿っていくのが基本的なルールとして、どのくらいまで辿っていくのかという部分はAttributeやOptionで制御できたりします。
InjectLocal
InjectLocal
をつけると親を辿りません。
public class Bar { // 同じContainerからしか探さない [InjectLocal] Foo _foo; public void DoBar() => Debug.Log(_foo.ContainerName); } public class FooInstaller : MonoInstaller { public override void InstallBindings() { // 現在のContainerとProjectContextの両方でFooを用意しておく ProjectContext.Instance.Container.Bind<Foo>().AsCached().WithArguments("ProjectContext"); // BarがいるContainerにはFooをBindしない //Container.Bind<Foo>().AsCached().WithArguments("Scene"); var subContainer = Container.CreateSubContainer(); subContainer.Bind<Foo>().AsCached().WithArguments("Sub"); // 現在のContainerにBarをBindする Container.Bind<Bar>(); Container.Resolve<Bar>().DoBar(); } }
=> ZenjectException: Unable to resolve 'Foo' while building object with type 'Bar'. Object graph: Bar
自身の所属するContainerにはないのでそこでエラーが出てしまいます。
こんな感じで実は細かい設定ができたりします。Attributeの書き換えでもいいですし、Inject(Source = InjectSources.Local)
といったInjectSources
列挙体で渡すやりかたでも同じことができます。列挙体は以下の四つです。
- Any
- Local
- Parent
- AnyParent
InjectSources.Any
自身から探して、見つからなければ親を延々と辿っていきます。
通常のInject
がAnyに設定されているのでいつものやつです。
InjectSources.Local
上記の通り、自身のContainerだけを探して見つからなければエラーを吐きます。
InjectLocal
の設定と同じです。
InjectSources.Parent
自身の直接の親のContainerを探しに行きます。自身と同じContainerに存在していても参照されません。親が持っていない場合エラーを吐きます。
InjectSources.AnyParent
自身の親を延々と辿っていきます。自身のContainerは参照しません。
参考
InjectSourcesのテストコード見るとなるほど感あります。
ちなみにZenject内部ではKernel内部の各種Managerが混ざらないようにInjectLocalで分けてるっぽい。
サブコンテナ使ったりProjectContextでBindしてるインスタンスと同一のものが必要になったりとかしたら明示的に分けていいかもしれませんね。スコープが狭まることはいいこと。
2018年ふりかえり
とはいえ、先月ふりかえったばっかりなのであまり書くことはないかもしれない。
若干被ることも多いけど年末の行事なので書くことにした。なのでアニメとかゲームの話書こう。
仕事
一番はやはり転職が大きかった気がする
WEB&ネイティブアプリからゲーム作るエンジニアになりました。果たして生きていけるのかと思ったけど、割といい感じにやってます。趣味での開発は案外有用だったので、RxとかZenjectとか導入したり、逆に業務やコンシューマでのアレコレを教えてもらったりしてます。まだ頭は追いついてないです。
また、初めてチームビルディング周りを勉強したりし始めました。前職でやってた人たちを思い出して見よう見まねから始まり、本を読んだり勉強会で話を聞いたりするなど。
あと、社内のLT大会のにぎやかしとして小ネタを喋ったりしていた。
イベント
社外でのLT、登壇は三つだけだった
- Zenjectとテスト - Gotanda.unity #5
- Unityプロダクトにテストを導入していくまで - Unityテスト完全に理解した
- Zenject Optionalアレコレ -【年末だよ!】UnityおとなのLT大会
あと、夏コミと技術書店でUnity関連の本を少し書いたりしていた。
上のイベントふりかえると全部Zenjectの話してる気がする。
イベントに関しては自分が登壇したのはそんなにないけど、会場の誘致をしたりなどしていた。
- Unity テスト完全に理解した
- Gotanda.unity #7
- Unity Zenject完全に理解した
- 大八耐2018 in 東京
- Unity ECS完全に理解した
- Unity Network 完全に理解した
会場提供おじさん業を繰り返し行った結果、社内の人も気軽に勉強会見に来るようになったのでよい。
ゲーム
クリアしたものだけ列挙
スパロボXは久々の異世界系だったけど普通に楽しめた。ヴィルキスとビルバインの二大切り込み隊長にはお世話になりました。個人的にはストーリーは暗めのVが好きだけど、ワタルのおかげ基本明るいのでそれはそれでよかった。Tは予約します。 世界樹はとにかくボリュームが多くて三か月くらいは時間かかったけど満足。基本脳筋戦術が好きなのでプリンスなどがバフかけまくってインペリアルの全力ドライブが最高に気持ちよかった。Vからだけど裏ボスも割と好きなパーティで戦えるようになったので楽しい。次回作はどうなるか。
あと、友人から勧められたアルトネリコ2が面白かった。システムが斬新で覚えるまでが苦労したけどリズムとRPGがいい感じに混ざっていてよかった。アドベンチャーパートは濃厚というかこれがやりたかったからRPG要素入れたんだなという勢いだった。クローシェルートクリアしたので二週目やろうと思ったけど引継ぎがないのが残念。
メタルマックスゼノは5週クリアしました。難易度ゴッドの最強の賞金首を倒したあたりで一旦ストップ。
アニメ・映画
一か月の無職期間にNetflix契約したのもあってだいぶ観ることが増えた。覚えてる奴だけ抜粋。
- キングコング髑髏島の巨人
- インターステラー
- キャビン
- マッキー
- バーフバリ~王の凱旋~
- 殿、利息でござる
- ポプテピピック
- キルラキル
- 王立宇宙軍 オネアミスの翼
- トップをねらえ!1&2合体版
- マジンカイザーSKL
- バキ
- 逆シャア
- アニゴジ三部作
- 未確認で進行形
- Devilman Crybaby
- NEW GAME!2期
- クレヨンしんちゃん逆襲のロボとーちゃん
- クレヨンしんちゃんモーレツオトナ帝国
- クレヨンしんちゃんアッパレ戦国大合戦
- クレヨンしんちゃん嵐を呼ぶジャングル
- ゾンビランド・サガ
- SSSS.GRIDMAN
劇場で見て一番良かったのはバーフバリでした。2時間越えの長丁場だけど最後までバリバリバリバーフバリできるやつだった。アニゴジは賛否両論凄いけど個人的には全編ふりかえると悪くないなあと思った。怪獣プロレスはないけどそれはそれで。実はリアルタイムで見たのはゾンビランドサガとGRIDMANの二つだけ。これらは途中で評判聞いて観始めたら面白くて、気づいたら毎週楽しみにしていた。改めて見直したら、設定が奇抜でも結局王道な展開が好きなのかもしれない。GRIDMANもゾンビランドサガも最後は読めてるけど楽しめるやつだったし。
でも今期もやはりガルパンが最高でしたね。
大洗
5月と11月に二回行ってきた。最高だった。
まとめ
今年の頭に絵とか音楽とかやってみるぞ!と意気込んでみたが、蓋を開けてみればDIだの設計だのテストだの去年よりエンジニアよりのことに頭を使う一年でした。ただ楽しかったので来年はさらに特化していいかもしれない。ブログとかLTとかは引き続きやって行くけど、縁と機会があるなら他所にそういうZenjectアドバイスするおじさんとしてお邪魔しに行くのもありかもしれませんね。楽しそう。
今年もお疲れさまでした。