imog

主にUnityとかの事を書いています

仕事納めとスクラムの話

今日で仕事が納まった。それと同時にチームメンバーが一人チームを去ることになった。送別会もしたのでお別れはまあそれなりにしたんだけど、結構自分に影響を与えたのでなんとなくブログとして残しておくことにした。

 

うちの部署は複数のスクラムチームで構成されているスクラムオブスクラムという形を取っている。僕がこのチームに配属されたのは二ヶ月前くらいのことだ。部署内の別のチームからこのチームに異動したのがきっかけだ。チームメンバーの一人であり、今回お別れした人は認定スクラムマスターを持つエンジニアであり、このチームはちゃんとアジャイルなチームになろうとしているという話を事前に聞いていた。(以降、この人を彼と呼ぶ)

前職でもスクラム開発はやっていたつもりだったし、以前のチームでもスクラムを行なっていたのでなるほどなるほどという感じでチームのやり方に乗っかっていくことにした。見積もりをして、計画で洗い出し、ポイントを計測して振り返りでTRYを洗い出そう。そんな感じに思っていた。しかし、今まで自分が経験したやり方とはチームのやり方の違いに衝撃を受けた。特にこの2週間くらいの出来事を列挙する。

 

 並行してタスクをこなすことがなくなった

 

スプリントでやることはPBI(プロダクトバックログアイテム)を基にして計画で洗い出す。一つのPBIが大きいならどんどん分割していく。完了条件が定義しやすくなるまで細かく刻んでいく。そこから具体的に何が必要かを職種問わず洗い出して付箋に貼り出していく。この時に誰がやるかとかは一切考えない。

この時に、分割で複数のPBIが現れることになる。分割しなくても関係ない別のタスクがPBIとして存在することもある。(新機能追加と既存の改修が同時に存在したり)でも優先度は計画時点ですでに決まっている。ここで、今までだったら複数人エンジニアがいるのだから並行してやるのでは?と思ってたがそうはならなかった。必ず優先度の高いPBIを全員でこなす。それが終わらない限りは次のPBIには触れなかった。そもそもスプリントに入らなそうなら今スプリントでやるPBIから外した。理由を聞いたら「並行して個人で行う時点でそれはチームの意味があまりない」とのことだった。最初はなるほどな〜くらいの意識だったが、朝会夕会もしくは業務時間でこれどう進めようかという話を全員で行えるのは気持ちが楽なことに気がついた。なによりやることがシンプルで、眼前にあるPBIをどう完了するかを考えるだけでよかったのは脳のスタックの消費が少なかった。

 

 一人でコードを書かなくなった

 

このチームは自分を含めてエンジニアが3人いる。上記でPBIをこなす際に全員でという話をした通り、エンジニアも全員でこなす。つまりモブプログラミングの形式を取って実装を進めていた。一人が実際のコーディングを行い周りでやいのやいの話す形式だ。

 

tech.smarthr.jp

 

全員でPBIを見ながら改めてこの実装が必要では?という話をする。逆にその実装はこのPBIを完了させるのに必要ではないのでは?なんてことを喋りながら付箋にやることを書き出す。それぞれに得意分野は違う。一人はプロダクトのキャリアが長くドメインに詳しいので、その観点から現状の仕様を話してどう実装すべきかを提案する。自分はチーム内ではプロダクトのドメインに乏しいが技術的な観点での話はできるので、こういう実装方法ならテスタビリティが高いのでは?ということを提案して実装を進めていく。それをやいのやいの話しながら動くものを作っていった。

この作業は効果的に働いたと思う。全員で考えて作ることで発生しうる問題をある程度予測できたからだ。三人寄れば文殊の知恵というが本当にその通りだなと感じた。

副次的な効果としてコードレビューの手間が省けた。そもそもみんなで作っているので改めてレビューする部分が少ないからだ。(とはいえPRで俯瞰して見た時に微妙な書き方のツッコミはしたくなったので無ではない)

 

もう一つの効果としてQAとの関わり方が少し変わった。

ちょっと前までモブプロでエンジニアが作る -> ビルドしてQAに動作確認してもらう -> 不具合を見つけたら報告してもらうというフローを採用していたが、これだとスプリントの前半はQAが暇になって後半が詰まってしまうよねという問題が発生した。これを解決したいねーというのがチームの課題になった時に、「モブプロに入って貰えばいいのでは?」という話になった。所轄モブワークである。エンジニアが作ろうとしている時点でチェック項目が生まれるのだから、ビルドを待たずとも最初から入ってもらえればその時点でチェック項目を作成できるし、Unityエディタ上で動作チェックもできるだろうという考えだ。

このやり方は成功した。エンジニアがこういう実装が必要という段階でQAさんが「このパターンとか大丈夫ですか?」みたいなツッコミが入り初期段階で懸念点を潰すことに成功したのだった。エンジニアが複数人集まってもうっかり気づけない項目が結構あることを思い知らされた。プロフェッショナルはすごい。

加えて隣の席に座っていたプランナーも会話が聞こえるので普通に入りやすく、こういう感じでやりたいとかこういうデータで試しにやってみるねなどのコミュニケーションが円滑に行われた。目の前で動かすし、即座にこうしたいが入るので、少なくとも出来上がったものに認識のずれはなかった。

 

なにより単純に楽しかった。どう作っていくかを話し合うのはそれ自体が楽しいのだ。

 

チームで作っていくという認識を改めた

 

「チームで作っている」というのは仕事なら当たり前なんだけど、本当にできていたのだろうかと考えるきっかけとなった。仕様が固まってないのでできませんとか、実装したけどQAの手が空いてないので進んでませんとか、僕が書いたコードではないのでわかりませんとか、仕様の詳細は企画に聞いてくださいとか。

彼は「チームの成果物はチームが責任を持つべきだ」と強く言った。スプリントレビューに出すものは、みなさんどうしたらいいですかね?と周りに意見を求めるものではなく、うちはコレを作った!バーン!と責任を持って出してGood!Bad!みたいなフィードバックを貰わなければならないという話だった。そうしないとチームとして成長できない。そのかわり責任はチームでシェアしたいよねということも付け加えた。だからこそ個人で分担するのではなくチーム全員で立ち向かわなくてはならない。隣の人が今日何をしていたかわからないというのはチームとして動けているのか?それはslackにログを流していれば解決することなのか?とか。

 

しかしその逆に、チーム外に関してはもっと閉鎖的で良いと考えていると彼は話した。この話のキッカケになったのはコードレビューだ。チーム内コードレビューは簡単だが、別のチームのコードレビューはコンテキストが把握しきれないことも多くそれなりに大変だった。コレに対して、レビューの必要性そのものを考えるべきということだった。それぞれチーム内レビューで完結させて、そこで不具合が出るのならばそれがそのチームの現状であるので、チームで改善していくのが良いのではという話。その改善の過程で他のチームにレビューを求めるならそれは応じてあげればいい。

自分はわりとプロダクト全部のコードを知っておくべきだと考えていたし、レビューそのものが技術的な成長のきっかけだと考えていたのでこの考え方は新鮮だった。意見の是非ではなく素直に参考にしたいと思った。

 

 

年末年始はUnityのDOTSについて調べようかと思ってたんだけど、一回アジャイル開発の本を読み直そうと思った。それくらいこの2週間は衝撃で、楽しかった。見積りをしてポイントを出したスクラム開発であることは今までと変わらないけど、今までとは明らかに違う部分に触れられた。

来年からは彼がいない状態のチームでやっていくことになる。今までのようなチームを維持できるかはわからないけど、彼がいないから上手くいかなくなりましたというのはそれこそ自己組織化ができていないのでそうならないように立ち回っていきたい。

おつかれさまでした。

Zenjectを使うときに気を付けていること

このエントリはUnity Advent Calendar 20194日目のやつです。

Zenjectは便利ですが、実際に扱っている人の知見やtipsがまだまだ世に出回ってない印象です。APIの使用方法も欲しいですが実際に使ってる人たちがどういうお気持ちで使っているのかというのはどんどん出回って欲しいので、今回は僕がZenjectを使う際にチョットだけ気を付けていることを列挙します。 そんなにテクニカルなことではなく心持ちみたいなのが多めです。

これが正しい!というのは中々決められないと思うので、1つの考え方として参考になれば幸いです。

基本的にコンストラクタインジェクションを行う

Zenjectでインジェクションを行う場合、4つの選択肢があります

  • フィールドインジェクション
  • プロパティインジェクション
  • メソッドインジェクション
  • コンストラクタインジェクション
public class Foo {
    [Inject]
    private Bar bar;
    [Inject]
    public Hoge hoge { get; private set; }
    [Inject]
    public void Construct(Fuga fuga) { }

    public Foo(Piyo piyo) { }
}

この中でコンストラクタは1度しか呼ばれないので、コンストラクタインジェクションするということは、あとから外部から変更される可能性を無くすことができます。 また、オブジェクト間で循環参照が発生したとき、コンストラクタインジェクションはエラーを吐きますがそれ以外の三つは循環参照を許容します。循環参照は複雑性が高くなり、明確な目的がない限りは避けていきたいところなのでバリデーションしてもらえるコンストラクタインジェクションはを使うように気を付けています。しかしMonoBehaviourはコンストラクタが呼べないので泣く泣くメソッドインジェクションを行っています。

public class FooBehaviour : MonoBehaviour {
    [Inject]
    public void Construct(Fuga fuga) { }
}

名前はコンストラクタの代わりなのでConstructと付けるのが好きです。

intefaceをBindする

基本ですが、クラスはBindせずinterfaceだけを渡すようにします

public interface IFoo {}
public class Foo : IFoo {}

Container.BindInterfaces<Foo>().AsCached(); // IFooで登録するがFooは登録しない
//Container.Bind<Foo>().AsCached(); // 直接クラスは登録しない

これを実現するために、Injectされる側もinterfaceでメンバ変数を持つような実装になります。あと、今回のIFooのような1つにしか実装されないようなものも毎回インターフェースで渡すことになります。これは基本的に依存関係逆転の原則(DIP)のルールに従うためです。もう一つとしてはDIを行われる側から詳細(実際のクラス)を減らしたいお気持ちがあるからです。

理想を言うのであれば、詳細の情報はinstallerから漏れ出ない状態が嬉しいです。Installerはクリーンアーキテクチャ本で言うところの「Mainコンポーネント」の役割を持っていると考えています。Mainコンポーネントは最も下位レイヤーの処理であり、初期状態を作成したり設定を構築したりと大変泥臭い部分です。ここでどんなデータがあるのか、どんな詳細を持っているのか、どう渡すのかという部分が詰め込まれることで、Mainコンポーネントを変えるだけでテスト、開発、本番環境などを切り替えれるようになります。この動きをInstallerで行うためには、実際のクラス情報はInstallerだけが知っている設計にする必要があり、そうなると各依存関係はintefaceでのやり取りにしたいよね・・という感じです。

とはいえ、Factoryとかはよく実体を返しがちなので徹底するならちゃんとカスタムファクトリ作らないとね~ってなる。あくまで理想としてです。

primitiveな型をBindしない

整数型や文字列などをそのままInstallerにBindするのは控えてます。

public class MainInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container.Bind<string>().FromInstance("name").AsCached();
        Container.Bind<int>().FromInstance(100).AsCached();
    }
}

public class Player : MonoBehaviour {
    [Inject]
    public void Construct(int life, string name) {}
}

Containerは型で全てを判断するのでBind<int> Bind<string>と言ったプリミティブ型はInstallerを見直したときに何を表すのか非常にわかりにくいです。また、同一コンテナ内で競合を起こしやすいです。上記の例では別のクラスが攻撃力としてintを求めていたとしてもぶつかってしまいます。WithIdオプションで差別化は可能ですが、意味のある値ならば素直にクラスを作ってあげる方が良いでしょう。

ListをBindしない

Container内部でIEnumerable<T>が見つからなかったとき、Zenjectは空のリストをInjectする特性があります。

public class MainInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container.Bind<Foo>().AsCached();
    }
}

public class Foo {
    private List<Bar> _barList;
    public class Foo(List<Bar> barList){
        _barList = barList;
    }
}

上記の場合、List<Bar>がないのでエラーが出そうに見えますが_barListには空のリストが入り、バリデーションエラーも発生しません。これはうっかりBind忘れを起こしていた場合にも正常に動作してしまうので非常に問題に気づきにくいです。これもやはりちゃんとクラス作ってあげましょう。

手動でもDIできるようにする

実際に手動でやることはあんまりないですが、DiContainerが無いと動かせない状況は避けています。具体的にはprivateなメソッドへのInjectは避けようという考えです。

public class FooBehaviour : MonoBehaviour {
    [Inject]
    private void Construct(Fuga fuga) { }
}

上記のコードはZenjectの機能を用いないと初期化処理が難しい状況になっています。メソッドを不用意に公開しないというのは有効な手ではありますが、この初期化処理はZenjectに自動で行ってもらっているだけで外部公開してはいけないものではないと考えています。ここをPublicメソッドにしてもらえれば、軽い動作確認を行うときにInstallerを用意する必要はなくなるし、テストコードを書くときにContainerを用意せずに済むので少し楽になります。 手動で行うべきところをZenjectに自動でやってもらってるくらいの気持ちで[Inject]を付けるといいかもしれません。

Validateを使う

ZenjectにはValidate機能が備わってて、シーン内の依存関係であるならPlayModeで実行せずにShift+Alt+vで自動チェックしてくれます。

image

毎回PlayMode実行してAssert Hit!と怒られるよりは速いので癖をつけておくのはおススメです。 ただ、Factoryが生成してくれるかみたいな動的な部分は対応していないのでご注意ください。

DiContainerを持ち歩かない

割と基本ではありますがContainerの存在を極力隠します。目安としてはInstallerのコード以外でContainerを呼ばないように心がけています。Installer以外、つまりアプリケーションにContainerが入り込んでくるのはコンテナを用いたサービスロケータパターンのような振る舞いをしている恐れがあります。せっかくContainerが何も意識させることなくInjectしてくれてるのに自らConainerに依存するのは勿体ないです。動的な生成を行いたい場合はDiContainer.Instantiateではなく必ずFactoryを生成するようにしましょう。

adarapata.hatenablog.com

例外として、ファクトリにContainerを持たせるのはアリかなと思う派です。理由としては依存関係のバリデーション機能が提供されているから使いたい・・というところです。

github.com

コードの部分を抜粋すると次の通り

public class CustomEnemyFactory : IFactory<IEnemy>, IValidatable
{
    DiContainer _container;
    DifficultyManager _difficultyManager;

    public CustomEnemyFactory(DiContainer container, DifficultyManager difficultyManager)
    {
        _container = container;
        _difficultyManager = difficultyManager;
    }

    public IEnemy Create()
    {
        if (_difficultyManager.Difficulty == Difficulties.Hard)
        {
            return _container.Instantiate<Demon>();
        }

        return _container.Instantiate<Dog>();
    }

    public void Validate()
    {
        _container.Instantiate<Dog>();
        _container.Instantiate<Demon>();
    }
}

public class TestInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container.BindFactory<IEnemy, EnemyFactory>().FromFactory<CustomEnemyFactory>();
    }
}

Validate内部での処理は実際には生成されないいわゆるDry-runです。上記の場合、DogDemonの生成が正しく行えるか依存関係を遡ってチェックしてくれます。

積極的にSubContainerを使う

SceneContextのInstallerに全部書いていくと非常にFatなInstallerになってしまうので、隙あらば積極的にSubContainerに移し替えていきましょう。

speakerdeck.com

個人的にはなんらかのスクリプトがアタッチされているPrefabには全部GameObjectContextを付けてSubContainer運用にしていいくらいまであります。Installerの数がかなり増えるので管理だけは気を付けて・・。

もし、様々な事情が絡み合いSubContainerが使えないという場合は、役割を明確に分離してInstallerを複数に切り分けるのも手です。後にContainerを分割する際に手がかかりにくいです。とはいえ、1つのContainerにめっちゃInstallされるので競合問題は起きうるので気を付けましょう。

Initializeで購読する

初期化をどのタイミングで行うか問題はやや面倒ですが、ZenjectだとIInitializableを実装しておくとまとめて初期化処理を呼んでくれるので使うことが多いです。

public interface IFooUseCase {
    void DoFoo();
}

public class FooUseCase : IFooUseCase, IInitializable {
    public void Initialize() { /* なんか初期化 */ }
    public void DoFoo() {}
}

どうせ初期化処理の順番管理するクラスはいずれ必要になるので、Zenjectに任せてしまえるのは楽です。Container.BindInitializableExecutionOrder<FooUseCase>(1) とかでInitializableの中でもクラスごとに順序は制御できます。

また、ゲームはイベント駆動で動きがちなので何らかのイベントや、UniRxを使っているならストリームを購読することが多いです。Initializeではそれら外部のイベントを購読する処理だけを書くことが多いです。

public class TimeOverUseCase : ITimeOverUseCase, IInitializable {
    private IGameTimer _timer;

    public TimeOverUseCase(IGameTimer timer) {
        _timer = timer;
    }

    public void Initialize() {
        _timer.TimeOverObservable.Subscribe(_ => DoGameEndSequence());
    }
    public void DoGameEndSequence() { /* */ }
}

ユニットテストを書くときには上記のように購読とビジネスロジックが明確に分かれていると楽です。このコードの例だと、プロダクションではイベント駆動で勝手に動きますがテストを書くときはInitializeを呼ばずに直接DoGameEndSequenceを呼べるのでユニットテストで手を抜けます。

デバッグ機能はSceneDecoratorContextで切り分ける

例えば、現在の状態を表示したいなあと思ったときに画面にデバッグ用のUIを仕込みたいことが度々あります。 画像で言うPlayerCharacterTurnStartがそれ。 image

この辺りは該当のシーンのSceneDecoratorContextを作ると楽です。今回だとBattleシーンの細かいログとか取りたいのでBattleDebugシーンを作ってそこにSceneDecoratorContextを貼ってあげます。

image

そこに使い捨てでもいいので雑なデバッグクラスを差し込んであげる。

public class DebugInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container.BindInterfacesTo<StateChangeNotifyUseCase>().AsSingle();
        Container.BindInterfacesTo<StatePresenter>().AsCached();
    }
}

Zenjectをいい感じに使えているなら、デバッグ時に欲しい情報はDecorateされるシーン側のContainerにBindされているはずなので、サクサクっと画面表示などはできます。 もしインスタンスそのものに手を加えたいとかの場合(例えばデバッグ時だけは実行時間をロギングするとか)は、DecoratorBindingも組み合わせてみると綺麗にやれるのでおススメです。

adarapata.hatenablog.com

直接コード書いていくより手間はかかりますが、プロダクションに入れたくないものが絶対に混入しないという安心感は心の平穏を保ってくれます。

おわりに

君だけのZenjectテクニックを教えてくれよな!

技術書典7でZenjectチョットワカルBookを頒布します

9/22の技術書典7にて、Zenjectについてつらつら書いた「ZenjectチョットワカルBook」というのを頒布します。ページは以下。

techbookfest.org

また、電子版もboothで同日から公開します。

e-f-b.booth.pm

会場では物理本を1000円で頒布します。かんたん後払いシステムをご利用の方は物理本+電子版をお渡しするのでお得です!ぜひかんたん後払いをご利用ください。

blog.techbookfest.org

何の本なの?

ZenjectというDIフレームワークの基礎的な使い方をまとめた本です。前後篇を想定しており、今回はシーンのDIまで。

  • DIとはなんなのか
  • BindとInjectionとはなんなのか
  • Installerとはなんなのか
  • Contextとはなんなのか
  • Bindはどんなオプションがあるのか
  • Factoryとはなんなのか
  • シーンを跨いだDIはどうやるのか

ざっくりと上の部分に対しての解説にはなっていると思います。

対象読者は誰なのか?

残念ながら全くZenjectを使ったことない人にはお勧めできません。そもそもアセットのダウンロードとかを省いちゃってるし・・・。 最低でもZenjectのライブラリがプロジェクトに入ってるところからのスタートになります。また、逆引き辞典にはなりません。ZenjectのAPIは数があまりにも多くそれを列挙するだけでも1冊の本になりそうだったので、使いそうなものだけ列挙しています。4章「様々なBind」では多くのメソッドの説明をしていますがそれも全部ではないのでご注意ください。

完全初心者向けでもなければ上級者向けでもないこの本は、「ネットやドキュメント見ながらなんとなくZenjectでDIしてるんだけどこれでいいのかな?と不安になりながら進んでいる人」に対してほんの少し後押しをするような内容になってます。完全に理解は難しいのでチョットワカルBookです。

最後に

Zenjectは採用事例がじわじわ増えてきているけど、世の中に記事が少ないのもあり「よくわからなくて難しい」という声をよく聞きます。これを機にZenject人口が増えていけば、みんな大手を振ってプロジェクトに導入しようぜ!って話ができるし、僕も仕事がしやすくなりますのでそこんところよろしくお願いしたします。そしてみんなZenject記事書いてください。

Extenject(Zenject)のTestがNSubstituteをサポートしていた

割と最近の出来事だった

github.com

元々Zenjectはテスト時のモックライブラリとしてMoqを採用していたんだけど、このPRでまた別のモックライブラリであるNSubstituteを使えるようになった。選択肢が増えたのだった。

Moq

github.com

NSubstitute

nsubstitute.github.io

使い方

まだreleasesには含まれていないのでmasterブランチから直接取ってくることにはなる。

Assets\Plugins\Zenject\OptionalExtrasに新しくAutoSubstitute.zipがあるので、展開してAssets\Plugins\Zenject\OptionalExtras\TestFramework下に放り込んであげましょう。

image

雑に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に全部書かれています。

github.com

割と数が多いのでまとめてみました。間違っているものがあればコメントなどもらえるとありがたいです。

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パターンについてはこちらも合わせてどうぞ。

adarapata.hatenablog.com

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がアタッチされているのがポイントです。IInitializableITickableなども適切に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で書いた記事を転載したものです。

unity-bu.booth.pm

UNIBOOK10、いい話がいっぱいあるから買ってね!

Zenject Factory


ZenjectはUnityで動作するDIライブラリです。煩わしい依存関係の解決を助ける非常に強力な武器となりますが、 強力かつ多機能であるためどこから学んでいくべきか悩むことも多いです。公式のドキュメントはとても充実していますが、前提となる知識が多かったり、基本的なBindingの話以外は日本語での資料が少ないというのも理由の1つでしょう。

この記事では、ZenjectがもつFactoryによる動的なオブジェクト生成について簡単な解説を行います。

github.com

動的に生成したインスタンスへのインジェクション

シーン読み込み時にInstallerを実行して、オブジェクトをバインドし依存関係を解決していくというのがZenjectのスタンダードな使い方でしょう。Containerはシーン開始時の1回のみインジェクションしてくれます。

しかし、実際の開発においてすべてのインスタンスが初期化時に存在するとは限りません。たとえば、アクションゲームで敵キャラが動的に生成されることは十分に考えられるでしょう。

次の例で考えてみましょう。

  1. Enemyは動的に生成される
  2. Enemyはプレイヤーに依存している
  3. PlayerはContainerにバインドされている
  4. 敵の種類は今後増える可能性がある

最低限のコードで書くとこうなります。

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を実装したCustomEnemyFactoryに集約されています。このクラスはそれぞれ実際にインスタンス生成を行うFactoryを所持しており条件に応じて切り替えています。 FromFactoryメソッドを指定することで、EnemyFactoryのCreateが呼ばれたときに処理を委譲できます。 EnemyFactoryへの影響は全くなく、ロジックの差し替えも容易です。

閑話:空のFactoryは必要か

毎回PlaceholderFactory<T>を継承しただけのFactoryクラスを定義していますが、厳密には定義しなくてもバインドはできます。 次のようにPlaceholderFactoryを直接バインドすることも可能です。

Container.BindFactory<Enemy, PlaceholderFactory<Enemy>>();

メンバ変数で持つときも同様に直接PlaceholderFactoryを持ってしまえばFactoryクラスを作る必要はありません。コードを書く手間が省けるのでよさそうに思えますね。

しかし、ドキュメントではこれを非推奨としています。理由は次の二点です。

  • パラメータの変更があった場合にコンパイルエラーが発生しないので気づくのが遅くなる
  • PlaceholderFactry<T> より T.Factory の方が可読性が高いと考えている。

たとえば、PlaceholderFactoryをそのまま使って、あとからパラメータを1つ追加したとしましょう。 次のコードのようになります。

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はバインドされているがEnemySpawnerが参照しているPlaceholderFactory<float, Enemy>がInjectできずにエラーが発生します。些細なことですがよくありがちなエラーです。これがFactoryクラスを定義しているならば変更時点でコンパイルエラーが教えてくれたはずです。 また、パラメータが増えるほどPlaceholderFactoryという名前が何のFactoryなのか難しくなってきます。PlaceholderFactory<Player, Difficulty, Enemy>という名前のFactoryは理解に時間がかかってしまうでしょう。たった数行のコードを減らすことで得られるメリットはあまり大きくありません。

上記の理由から、基本的にはEnemy.FactoryのようにネストしたFactoryを作ることが推奨されています。 特に強い理由がなければその都度定義した方がよいでしょう。

終わりに

Zenjectは強力ですがその分難易度も高いと言われています。特に動的なインスタンスのInjectはやり方をしっかり考えないとContainerを各所に引きずり回してインジェクションなどをやってしまいがちです。これはコードをより難解なものにしてしまうので、Factoryを用いた設計を覚えてシンプルな仕組みに書き直していくのがよいでしょう。