すぎしーのXRと3DCG

主にXR, Unity, 3DCG系の記事を投稿していきます。

【Extenject】Composite Installer を紹介!

f:id:tsgcpp:20210221104046j:plain

概要

今回は Extenject に追加された Compsote Installer について紹介します!

github.com

ちなみにPullRequestを出したのは私です(欲しかったので)。

github.com

まだComposite Installerが同梱されているリリースバージョンはありませんが、 せっかくなので活用方法と、ついでにマージされるまでの小話について記事にしようと思います。

記事の最後にunitypackageとタグへのリンクを記載していますので、使ってみたいと思った方はご利用ください!

今回の記事は Extenject にある程度慣れている方向けの内容です。

Compsote Installer について

Composite Installer とは Compositeパターンを使った Extenject のInstaller です。

Compositeパターン

Extenject観点で説明しますと、 Composite InstallerをDIContainerに登録するだけで、そのComposite Installerに登録されているすべてのInstallerで InstallBindings()が実施されます。

また、Composite Installer に 別のComposite Installer を登録することも可能です。

f:id:tsgcpp:20210221105446j:plain

Compositeパターンはメジャーなデザインパターンのため、解説記事もたくさんあります。
詳しくは検索してみてください。

Compsote Installer の活用術

1. 再利用可能なInstallerグループを作成可能

Compositeパターンの通り複数のInstallerを1つのComposite Installerにグループとして集約することで、再利用性のあるInstallerとしての管理が可能になります。

  • 機能ごとにInstallerのグループとして管理
  • 機能ごとにComposite Installerを作成しシーンごとに取捨選択して利用

f:id:tsgcpp:20210220201233j:plain

Extenjectを利用してかつ Pure C# が増えてくると比例してInstallerクラスも増えてくると思いますが、
そんなときに目的や機能ごとにComposite InstallerにInstallerグループを分割登録して管理すると良いと思います!

2. 疎結合Installer, 抽象Installerとして活用

特定のInstallerの前にComposite Installerをレイヤーとして設けることで、ContextとInstaller間を疎結合化することができます。

  • 特定のInstallerに依存しない形で各Contextに登録可能
  • ContextのPrefabを編集することなく登録Installer群を変更可能

f:id:tsgcpp:20210220203403j:plain

特にComposite Scriptable Object Installerはオススメです!

ContextはアセットをGUID経由で取り込む形になるため、
特定の機能にInstallerを追加したい場合はContextを編集せずComposite Scriptable Object Installerアセットに新しいInstallerを登録するだけで済みます。

f:id:tsgcpp:20210220204459p:plain

3. 特定の機能提供向けInstallerとして配布

複数のチームでExtenjectを利用している場合の連携に活用できると思います。

  • 特定の機能を集約したComposite Installerをパッケージに含めて共有可能
  • Composite Installerへの修正やInstaller追加もパッケージの更新のみで配布先に反映することが可能

Compsote Installer の使い方

以下にドキュメントページがあります。
こちらも私の方で作成して一緒にPullRequestに出しました(ガバガバな英語で恐縮ですが)。

CompositeInstaller.md

せっかくなので、簡単にではありますが日本語でも使い方を紹介します。

CompositeMonoInstaller

名前の通り MonoInstaller の Compositeパターン版です。

コンポーネントはすでにExtenjectの中に含まれていますので、他のMonoBehaviourと同様にAdd Component可能です。

また、Prefab化することで Prefab Installerとしても利用することができます。

f:id:tsgcpp:20210220210533p:plain

f:id:tsgcpp:20210220210650p:plain

CompositeScriptableObjectInstaller

名前の通り ScriptableObjectInstaller の Compositeパターン版です。

Create -> Zenject -> Composite Scriptable Object Installer をクリックすることで作成可能です。

f:id:tsgcpp:20210220211141p:plain

f:id:tsgcpp:20210220211620p:plain

先程も述べましたが、個人的には ScriptableObjectInstaller をよく使うので特にオススメしたいです!

FYI: 循環参照の検知

Compositeパターンは性質上、循環参照ができてしまいます(詳細は後述)。
指定ミスによるヒューマンエラーを回避するために、循環参照を検知したときはInspector上のプロパティが赤くなるEditorも用意しました。

f:id:tsgcpp:20210220212259p:plain

f:id:tsgcpp:20210220212029j:plain

ちなみにこの状態で起動すると例外(ZenjectException)が飛びます。

Composite Installer 開発小話

標準搭載化

ありがたいことにPullRequestを出したところ、CollaboratorのMathijs-Bakkerさんに「標準的な機能になるから標準フォルダに移動してもいいんじゃないか?」とコメントをいただけました。

実質的に標準搭載化となったため追加修正を加えた上でマージされることになりました。
そのおかげで5日ぐらいは修正に費やすことになりましたけど、正直に嬉しかったのでモチベは高かったですw。

「プルリクって普通標準搭載になるもんじゃないの?」って思われた方もいるかもしれないので補足すると、
Extenject (Zenject) には OptionalExtras というオプション的機能を詰め込んだフォルダがあります。
当初はこちらに 拡張機能としてComposite InstallerのPullRequestを出していたのですが、上記のようなご提案をいただけたため標準搭載となりました。

みなさんの開発の手助けになれば嬉しいです。

そういえば、PullRequestですがmergeじゃなくてrebaseで取り込まれたのは若干気になりました。
ソースコード自体はそのまま取り込まれたので問題ないと思いますけど。

循環参照対策

Compositeパターンは循環参照に注意する必要があることを知っていたので、循環参照対策も主に2つ入れています。

  • 循環参照があるComposite Installer があった場合は例外を出す
  • エディタ上で循環参照を検知したらInspectorに赤く表示する

こちらを導入するに当たり循環参照の検証処理も少し工夫したので共有します。

循環参照が起こるケース

Compositeパターンにおける循環参照は以下のケースがあります。

  • 自分自身への参照を持つ場合

f:id:tsgcpp:20210221022218j:plain

  • 葉要素(子孫)が自身への参照を持つ場合
    • 以下の例では Self と Leaf06 で循環参照

f:id:tsgcpp:20210220215252j:plain

  • 葉要素(子孫)同士が循環参照している場合
    • 以下の例では Leaf06 と Leaf07 が互いに循環参照

f:id:tsgcpp:20210220215435j:plain

循環参照の検証方法

以下の手順で行います

  • 葉要素がCompositeではないこと
    • Compositeパターンの葉要素は元となったinterfaceなのでダウンキャスト(as)でCompositeかどうかの判定が必要
  • 葉要素がCompositeである場合は自身でないこと
  • 自身ではない場合は先祖一覧の中に子要素がないこと
  • 自身を先祖一覧に登録
  • 先祖一覧を用いて子要素すべてで同様の検証を実施

上記をComposite Installer の子要素が見つからなくなるまで行うことで循環参照を検知することができます。

先祖一覧を記録しながら探索することになるため、可変Listが必要になります。

また、デザインパターンなどを考慮するとダウンキャストはアンチパターンになり得ますが、
今回は広く利用されることを想定し循環参照問題の回避を優先してダウンキャスト込みの検証方法にしました。

ちなみに検証コードは以下となります。

CompositeInstallerExtensions.cs

検証処理のアロケーションの削減

私は余計なアロケーションは極力避けたい性質なので、アロケーションをなくしたい欲が出ました。
一応、RuntimeだけでなくEditor上でもInspectorの更新毎に検証処理が動いてしまうため、ある程度アロケーションを回避したかったという理由もあります。

結論を述べますと 4連結までの検証はアロケーションが回避される ようになっています。

やり方は単純で先祖1つ、先祖2つ、先祖3つ、先祖4つで検証するメソッドを用意して連結するテクニック(つまり面倒くさいやり方)で対応しました!
見たほうが説明が早いと思いますので以下のコードを御覧ください。

CompositeInstallerExtensions.ValidateAsComposite

4連結まで対応したのは「多くても3連結ぐらいかな?」と思ったためです。

4連結以上の場合はアロケーションが発生しますので、「いやもうちょっとアロケーション発生しないようにしたい」って人は上記のコードを参考にPullRequestを出してください。

もちろん、テスト (TestCompositeInstallerExtensions) も追加してあげてください!

stackallocとかいうunsafeコードを使うことも一瞬考えたけど流石にすぐ却下しましたw

余談: 循環参照対策なしの場合

循環参照のあるComposite Installerに対してInstallBindingsをコールした場合、以下の現象が発生しました。

  • PlayではStackOverflowException or エディタ自体が落ちる
    • どちらになるかはPlay毎に異なり、StackOverflowException 確定ではありませんでした
  • Zenject Validate では落ちることなくフリーズし、エディタが入力を一切受け付けなくなる

どちらにしてもInstallBindings の初めに検証(Assert)を入れる必要がありました。

アロケーション発生させたくないとか言ってられない!

そもそもなぜ作ろうとおもったのか

  • Installer群を一括でContextに登録する機能がほしかったから
    • Installerが増えて管理しづらかったので
    • 1つのInstallerクラス内にすべてのBind処理を書くと保守性が悪化するため避けたかった
  • 開発におけるワークフローを考えてみたときにComposite Installerがあればいろいろと応用が効くと思ったから
    • 具体的には「Compsote Installer の活用術」で述べた通りです

他に良さそうな活用方法があればぜひ教えて下さい!

Composite Installerを試したい場合

9.2.0 にrebaseでComposite Installerのcommitを取り込んだタグを用意しました。
もともと拡張として作成していたこともあってExtenjectの基本機能に変更はありません(Inspector表示用のEditorに少し変更が入ったぐらいです)。

metaファイルも同じなのでリリースバージョンが出てもCompositeMonoInstaller, CompositeScriptableObjectInstallerはそのまま使えると思います。
MITライセンスなので言う必要はないと思いますが、自己責任でお願いします!

github.com

フォークしたリポジトリ(tsgcpp上)にタグを登録していますが、本体にはすでに取り込まれている内容なためtsgcppの記名は不要です。

クレジット

  • draw.io @diagrams.net
    • 図形の作成に利用させていただきました

雑感

まさかの3ヶ月ぶりの投稿です。。。
個人開発を優先していたらあっという間に月日が流れていました。

記事を書いた理由はそろそろ情報発信しないとなと思ったことと、
Extenjectを使ったワークフローの提案的なことをやってみたいと思ったからです。

Extenjectに限らずDIコンテナはゲームやアプリケーションを面白くするための仕組みではありませんが、
工夫次第で幅広いシステムに対応させることが可能になると思います。

UnityでSOLID原則などを取り入れる上でも欠かせない存在かな思いますので、ぜひ活用してください!

それでは~