【Unity】Unity向けDependency Injection講座
概要
Unity向けDependency Injectionの方法について紹介します。
もしDependency Injectionを知らない方は前回の記事も併せてどうぞ!
ちなみにZenject(Unity向けDIフレームワーク)は使用しません!
環境
- Unity 2019.4.3f1
今回の主目的
UnityEditor上で依存を切り替え可能にすること
DIの種類
前回の#DIの種類でも紹介しましたが、
DIには主に "Constructor Dependency Injection"と"Method/Property Dependency Injection"があります。
前回と同様に以下のILogger
を注入する前提で紹介します。
public interface ILogger { void Log(string message); }
Method/Property Dependency Injection
public変数や[SerializeField]
を経由させて依存注入することになります。
注意点としてInspectorで扱うにはMonoBehaviour
継承クラスである必要があります。
Inspector上でも扱いたいですが「抽象に依存」させたいため、abstractクラスとして実装します。
public abstract class BaseLogger : MonoBehaviour, ILogger { public abstract void Log(string message); }
後は上記 abstractクラス をpublic変数や[SerializeField]
で定義します。
using UnityEngine; using System.Threading.Tasks; public class PropertyDIMessenger : MonoBehaviour { [SerializeField] public BaseLogger logger; public void Recv(string message) { logger.Log(message); } // 動作確認用 async void Start() { await Task.Delay(1000); Recv("Hello, I'm Messenger!"); } }
そして、UnityEditorのInspector上でBaseLogger
を継承したコンポーネントを渡してあげましょう
Constructor Dependency Injection
残念ながらUnityのMonoBehaviour
のコンストラクタは直接コールすることは基本的にありません。
よって、Unityのコンポーネントでは Constructor Dependency Injection はできません。
ただ、
インスタンス生成後は依存の切り替えはなしのDIは実装可能です!
その方法は、以下の2つで実現可能です。
[SerializeField]
private変数を定義する- Method/Property Dependency Injection で紹介したものと同様
Awake()
などのメソッド内でGetComponent
経由で注入
ということでAwake()
メソッドを使ったDIを紹介します!
※Awake()
だけでなく、Start()
, OnEnable()
でも可能です!
Awake を使った Dependency Injection
以下のように実装します!
using UnityEngine; using System.Threading.Tasks; [RequireComponent(typeof(ILogger))] public class AwakeDIMessenger : MonoBehaviour { private ILogger _logger; void Awake() { _logger = GetComponent<ILogger>(); } public void Recv(string message) { _logger.Log(message); } // 動作確認用 async void Start() { await Task.Delay(1000); Recv("Hello, I'm Messenger!"); } }
ポイントとしては
RequireComponent
で依存する抽象インターフェースを指定GetComponent
で依存する依存オブジェクトを取得(注入)
Awake を使ったDIのメリット
- インスタンス生成後は依存の切り替え不可ということを明示可能
- interfaceに依存させることが可能
- abstractクラスの用意が不要となる
今回使用してるILogger
実装クラスであれば以下のようにabstractクラスを継承させなくてもよくなります。
using UnityEngine; public class BaselessDebugLogger : MonoBehaviour, ILogger { public void Log(string message) { Debug.Log(message); } }
Inspector上では以下のようになります。
RequireComponentで指定したコンポーネントが追加されていない場合
以下のようにエラーメッセージが表示されます。
よって、依存するコンポーネントから先に追加してください。
注意:スクリプトで直接AddComponent
する場合はエラーメッセージは出ません!
Script Execution Order に注意
Awake()
などの実行順序はScript Execution Orderで決まります。
もし注入される依存のAwake()
が実行されていることが前提の場合は注意してください!
以下は注意が必要な例です。
以下のようにAwake()
がコールされていることが前提のLogger AwakeInitializedLogger
があったとします(結構無理やりに見えますがw)。
using System; using UnityEngine; /// <summary> /// Awake後のみ使用可能なLogger(ExecutionOrder検証用) /// </summary> public class AwakeInitializedLogger : MonoBehaviour, ILogger { private Action<string> _loggerAction = null; void Awake() { _loggerAction = message => Debug.Log(message); } public void Log(string message) { // Awake前に実行するとNullReferenceExceptionが発生 _loggerAction(message); } }
そして、以下のようにAwake()
で早速Loggerを使用する AwakeLoggingMessenger
があったとします。
using UnityEngine; // [DefaultExecutionOrder(-1)] // こちらを指定することでAwake()のコールを優先可能 [RequireComponent(typeof(ILogger))] public class AwakeLoggingMessenger : MonoBehaviour { void Awake() { var logger = GetComponent<ILogger>(); // logger取得後すぐにLogをコール logger.Log("Good Morning!"); } }
そして、「Awake を使った Dependency Injection」と同様に依存注入を実施しましょう。
このDIの構成、問題があります。
AwakeLoggingMessenger.Awake()
、AwakeInitializedLogger.Awake()
の順番で実行されるとNullReferenceException
が発生します。
Order of Execution の何がいけないのか
「それってOrder of Executionを設定すればいいだけじゃない?」って思った人もいるかもしれません。
確かにその通りではあるのですが、
DIのメリットの1つである「抽象に依存」というメリットが弱くなります!
注入される依存の状態を考慮する必要が出た時点で、少し蜜結合になっていると言えます。
余談:Awake, OnEnableの順番について
「じゃあOnEnableで依存注入すれば」と自分は考えました。
が、残念ながらそれも問題解決にならず。。。
オブジェクトA, BがそれぞれAwake()
, OnEnable()
を持っていて同時に生成される場合、以下の順番でメソッドがコールされます。
A.Awake()
A.OnEnable()
B.Awake()
B.OnEnable()
これはOrder of Execution for Event FunctionsによるとAwake()
後は即座にOnEnable()
が実施される仕様のためですね。
Script Execution Order の問題がなぜ起きるのか
理由は単純で Script Execution Order は依存関係を設定するために存在します。
逆に言えば Script Execution Order の設定なしの場合は依存関係が考慮されません。
設定すれば少し蜜結合になり、設定しなければ問題が発生するという悩ましい部分があります。
もちろん、今回紹介したDebugLogger
の様に問題が起きないパターンも存在します(インスタンスができた時点で使用できるため)。
Script Execution Orderの問題を回避したい場合は、次に紹介するDIフレームワークを使用することをオススメします!
Unity向けDIフレームワークについて
Unity向けDIフレームワークには以下のようなものがあります(詳細は省略)。
Unity向けDIフレームワークのメリット
- 依存関係を指定する設計になっているため、依存関係を考慮した初期化が実施される
- 基本的に依存するオブジェクトが先に初期化される
GetComponent
の方法では「必ず依存注入されること」を保証できない(AddComponent
を使用する場合など)
- Script Execution Orderの意識がある程度不要
- DIフレームワークは基本的にScript Execution Orderは意識しなくてもいいように設計されています(
Awake()
より前に依存解決と依存注入が実施されるなど)
- DIフレームワークは基本的にScript Execution Orderは意識しなくてもいいように設計されています(
上記のメリットがあるため、場合によっては利用を検討してみても良いと思います。
サンプルプロジェクト
雑感
DIはアプリケーションの柔軟性やテスタビリティにおいてとても重要なテクニックなのでぜひ活用してみて下さい。
最近、記事書くことが目的になりつつあって個人開発がおろそかになってる気が。。。
でも皆さんのお役立てできれば何よりです。
最後まで読んでいただきありがとうございました!
それではまた~。