独自の発展をしている部分があって、Unity 向けのサンプルや解説を見ると逆に DI が分からなくなる問題。原因は大きく分けて3つ。
この特殊記法は便利な一方でちょっと怖い。使える場面でも使ってないなーという例も見る。警戒すべき??その1
[Inject]
という Unity 向け DI コンテナにしか存在しない特殊な記法と、一般的な DI を混ぜて話をしている。
界隈でとある本が流行って、DI という手段を目的にするような記事が増えた時期があった。その2
DI コンテナを使ってない。コンストラクター経由で依存性を注入していない。DI やってない?? 各種サービス、OS、特定の何かに決め打ちで依存するのはまずいし怖いってことで、抽象化(≒依存性の逆転)して DI やってる・出来る状態になってる事の方が多い。 DI・DIコンテナがコンストラクターを使って依存性を注入します。になってるのは、この手法をオブジェクト指向のプログラミング言語向けに一般化しようとしたらコンストラクターだよな、絶対存在するし。てところから(だと思う)その3
MonoBehaviour
が特殊なクラスでコンストラクターを使えないため、独自の発展をするに至った。
--
コンストラクターで「自分の側で定義したインターフェイス」を受け取ろう、という手段が DI。目的は「制御の反転」(inversion of control) 参考 → 1 2 3
「DIコンテナ」が便利だから使われていて「DIコンテナ」の事を「DI」と呼んでいるだけ、Unity 以外で使われている意味での DI、依存性の注入はマジで一切、何の関係もない。 今までのおまじないを忘れて ※ チラチラ見てると Unity でメソッドに ※ Unity での DI は今までシングルトンとか使ってた部分を代替するモノ。ぐらいのイメージ。
[ App → IService ] ← MyService:IService
public App() { m_service = new MyService(); }
App.Singleton.SetService(new ExternalService());
[Inject]
って書くだけで良くなるらしいぞ! このぐらいの捉え方で良い。int m_value; // 値型は 0 が勝手に入ってる。
[Inject] Server m_server; // クラスは null、、、じゃなくて勝手にインスタンスが入ってる!!
void OnRequestReceived() {
m_server.DoIt();
}
[Inject]
してる例はあんまり、、、無くはないかな?? ぐらい。MessagePipe
というなんも考えずにメッセージ・イベントの送受信ができるライブラリも、今見てみると 「[Inject]
のおかげでなんか勝手にインスタンス入ってる!!」 を徹底活用したモノだと分かる。BuiltinContainerBuilder
+ GlobalMessagePipe
に問い合わせる手法の方が処理の流れが明確で良さそうだけど。
そして、DI を行うためのライブラリで、加えて DI 以外のことも出来るおまけが付いてるのが DI コンテナ。[Inject]
はオマケ部分
Unity 向けのサンプルや説明は DI ではなく「DIコンテナ」を使って「開発を楽にする」方法を示している場合が多く、混乱が生じる。DIコンテナのオマケで悩んだ結果 → static abstract
Unity と DI コンテナ
Unity 的には、「DIコンテナ」に付いていたオマケ部分の [Inject]
がスゴイ便利でうおーーっとなった。その背景には MonoBehaviour
の存在がある。
そもそもこの
[Inject]
というアトリビュートを使った手法、調べた限りでは Unity 向けの DI コンテナにしか存在しない。
MonoBehaviour
Unity といえば MonoBehaviour
。こいつはコンストラクタを持たず(持てるけど)、近いものとして Awake()
があるが、これは引数を受けない。
DI を実現しようとした場合も、DI と関係ない場合も、
- GameObject にスクリプトを付ける。
- コンストラクターへの引数の代わりとしてインスペクターで必要なフィールドを設定する。
- シーンが小分けでオブジェクトが存在しない等、GUI からは出来ない可能性がある。
- そもそも GUI で設定が面倒。
- C# で制御しようとするとこれもまた難しい。
- いつ、どこで?
-
DontDestroyOnLoad
な GameObject に持たせる? - 常時監視して該当するスクリプトのフィールドを埋める?
- シーンロードを担当するクラスが読み込んだ後に設定する?
- あいつが先にロードされていないとダメ。
- ライフサイクルの管理は?
内容は単純だけど、見通しが悪くどうするか結構大変。散らかりがち。
一方DIコンテナはDI実現のために気を利かせて [Inject]
を作った。そしてこれがこの問題に見事にハマった。
糖衣構文としての [Inject]
[Inject]
これはDIコンテナの機能の一部だけど、DIの概念とは何の関係もない、単なる糖衣構文と言っても差し支えないモノ。特にクラスのフィールドにインスタンスを埋める使い方。
この糖衣構文は Unity MonoBehaviour
にまつわる様々な面倒事を [Inject]
、と付けるだけで全て肩代わりしてくれるという優れモノ。(大抵はフィールドにシングルトンのインスタンスを設定する、という機能として使われてない??)
加えてライフサイクルの管理までしてくれる。
別に依存性がどうこう、インターフェイスを誰が定義したかとか関係ない。依存性じゃなくてインスタンスを注入する手段として、便利に使われている。
インスタンス&ライフサイクル管理ライブラリ
結果、Unity 向けDIコンテナはもちろん DI にも使える、が、MonoBehaviour
インスタンス&ライフサイクル管理ライブラリとして独自の発展を遂げ、DI! Inject! 依存性の注入! と言ったら大抵は糖衣構文の方を意味してると思ってほぼ差し支えない情勢。だと思う。
調べるきっかけになったプロジェクトも DI と捉えず、ライフサイクル管理とインスタンスの取得にDIコンテナを使ってるな?? として見るとすんなり理解できた。
昔→シングルトン。今→DIコンテナ。
DI はインターフェイスで受ける! とかもなくて、マッピングする術としてココはクラスじゃなくてインターフェイスだなー、ぐらいの感覚で使ってる。
あと DI でテストが楽に~~というのも結果的についてくる副産物で目的ではさそう。それを実現する手段なら探せば他にもたくさんあるだろう。
--
依存性の注入と C# 11 を投稿したら Qiita が色々とおススメに出してくれました。
Unity ユーザー的にはインフラへの依存を(クラスの定義的な意味で)避けるって言われても、いや有名どころのライブラリ読み込んで抽象化する位で十分なんですが、という感じですよね。
MonoBehaviour
のライフサイクルを考えないでも全部やってくれるのは便利だし、今後はDIコンテナを旨いこと使っていきたい。
以上です。お疲れ様でした。
-
言い過ぎ ↩