最初に
どうも、ろっさむです。
今回は「依存性注入」とは何なのかを理解するために、Zenjectを入門してみる手順をまとめたものとなります。
開発環境
- Unity:2020.2
- Extenject:9.2.0
依存性注入とは何か
英語だと「Dependency Injection」、通称「DI」と呼ばれているものです。依存性注入と言われていますが、「Dependency」を「必要なもの」とか「ないと困るもの」とかのイメージで考えると良さそうです。
参考:"Dependency"を使って、パンチの効いた制約関係の表現をする
DI自体は、「IT用語辞典バイナリ」にて以下のような説明がされています。
DIとは、プログラミングにおけるデザインパターン(設計思想)の一種で、オブジェクトを成立させるために必要となるコードを実行時に注入(Inject)してゆくという概念のことである。
つまりクラスA内で必要となるクラスBの機能を、必要となったタイミングで引っ張ってくる感じです。そうすることで、クラス間での密な結合を防ぐことができます。よくわからんなーって人は後ほど記載する、使用例とサンプルコードを確認してみてください。
この引っ張ってくる方法を提供してくれるのが、今回紹介する「Extenject」となります。
Extenjectとは
DIを実現するためのフレームワークです。
こちらはUnityのアセットストアからインポートを行うことができます。
Extenject Dependency Injection IOC
こちらは元々「Zenject」というフレームワークをメインで管理していた方が、会社の退社後に個人でメンテを始めたフレームワークのようです。現在は「Extenject」の方が最新のUnityへの対応などが行われているため、これから使用する場合は「Extenject」をインポートすることをお勧めします。
参考:Zenjectをメンテナンスするために生まれたExtenjectというリポジトリ
import 方法
先ほどのアセットストアのリンクからマイアセットに追加する > Unityで開く > Package ManagerにてExtenject Dependecy Injection IOC を検索するか、窓を開く > DL > Import
までを行ってください。
そうすると Assets > Plugins > Zenject
というフォルダが作成されます。早速試していきましょう。
使用例とテストコード
例えば、MyCharacterクラスがあるとします。
今回は適当にHierarcy > 3D Object > Sphere
を作成して、MyCharacter
という名前をつけておきます。
今回はこのキャラクターの移動制御を行うための処理をExtenject
を使用して構築していきます。
ここでなぜExtenject
を使用するのかというと、input処理をUnityEngine.Input
以外からも受け付けられるようにするためです。
例えば、現在主流なのはUnityEngine.Input
を用いた移動処理の実装だと思いますが、この他にもUnity2020から使用可能となったInput System
やRewiredという入力管理系アセットに差し替えたくなる時が来るかもしれません。その場合、Input処理を変更するのに、UnityEngine.Input
だけに依存した処理にしていると、多くのリファクタが発生してしまいます。
今回のMyCharacter
クラスで考えてみましょう。
移動処理用のコンポーネントとしてInputForMove
を用意しておきます。
このUpdate()
内部では、UnityEngine.Input
を直接参照しています。この状態から、例えば先ほどあげたInput System
でのInput処理を使いたい場合は、このUpdate()
内部を書き換える必要が出てきます。
interface
を使用しても良いのですが、結局どこかでオブジェクトをnew()
する必要があり、プロジェクトが大規模になるほど、その場所はコードの奥深くに位置する可能性が高くなります。
ここで、new()
を一つの場所に集約しつつも、疎結合となるような作り方を行うためにExtenject
を使用します。
まずはinterfaceと各実装を用意
Inputクラスが共通で持つ処理をInterface
側に記述をし、各Inputクラスで実装を行うようにしてみましょう。
Interface作成
雑にIInputtable
という名前にしておきます。
using UnityEngine;
public interface IInputtable
{
Vector3 InputForMove();
}
このVector3
値は移動するために使用する、Input情報を用いて作成したものとなります。
Inputクラス作成
まずはKeyboard入力用を作成してみます。
using UnityEngine;
public class InputFromKeyboard : IInputtable
{
public Vector3 InputForMove()
{
return new Vector3(Input.GetAxis("Horizontal"),
0, Input.GetAxis("Vertical"));
}
}
オブジェクトにアタッチ用のスクリプト作成
実際に動かすオブジェクトに対してアタッチするスクリプトを作成し、内部でInput入力を受け取って位置を更新する処理を記述します。
using UnityEngine;
using Zenject;
public class InputForMove : MonoBehaviour
{
[Inject]
private IInputtable _inputObject;
void Update()
{
if(_inputObject != null)
{
Move(_inputObject.InputForMove());
}
}
void Move(Vector3 vec)
{
var position = transform.localPosition;
transform.localPosition = position + vec;
}
}
ここでの_inputObject
は[Inject]
を付けることによって、中身が「注入」されます。
さて、問題はこの_inputObject
の中身に、どうやってInputFromKeyboard
を入れるかですが、ここを解決するにはInstaller
用のクラスを作成する必要があります。
Installerの作成
Installerクラスは、Zenject
の注入を行うにあたって、必要となる依存関係の定義を行うクラスになります。
ProjectタブなどからZenject > Mono Installer
を選択し、作成してみましょう。
今回はIInputtable
のインターフェース型の変数に注入する場合、InputFromKeyboard
クラスを使用したいので、以下のように記述してみます。
using Zenject;
public class SampleInstaller : MonoInstaller
{
public override void InstallBindings()
{
Container
.Bind<IInputtable>()
.To<InputFromKeyboard>()
.AsSingle();
}
}
一つずつ要素を分解して確認してみましょう。
Container
注入するに当たって、
- 要求される可能性のあるinterface
- そのinterfaceに対応したクラス(このクラスのインスタンスを注入)
- インスタンスの生成方法や、扱い方
を設定します。その設定先がこのContainer
となります。
Bind
「要求される可能性のあるinterface」をここで記述しています。
今回の場合だと、IInputtable
型を必要とする変数、プロパティなどを記述しているクラス全てが対象となることを明示しています。ただし、このInstaller
自体の影響範囲内に限ります(後述)。
To
「interfaceに対応したクラス(このクラスのインスタンスを注入)」をここで記述しています。
IInputtable
型を必要とするクラスの変数などに、To
で示したInputFromKeyboard
型のインスタンスを注入します。
AsSingle
「インスタンスの生成方法や、扱い方」をここで記述しています。
AsSingle
だけではなく、他のパターンもあるので見ていきましょう。
項目 | 概要 |
---|---|
AsSingle | このContainer全体で、To の際に定めたクラスのインスタンスを1つだけ持ち、そのインスタンスを再利用し続ける。シングルトン状態。基本的にこれがよく使われる。が、動的Bindには向いていない。 |
AsCached | 既にインスタンス登録が行われている場合は、そのインスタンスを使用する。UnBindするとインスタンスの参照は破棄されるため、動的にBindを行いたい場合はこちらを使用する。 |
AsTransient | インスタンスを再利用せず、To の際に定めたクラス型のインスタンスが要求される度に、新しいインスタンスを使用する。 |
なので、今回は「IInputtable
型のインスタンスが必要になった際には、InputFromKeyboard
型のインスタンスを一つだけ作成し、Container
に登録を行って、一つのインスタンスを使い回す」といった状態になります。
次に、このInstaller
で記述した内容が、どこまで影響範囲を持つのかを設定する必要があります。
Contextを作成する
Zenject > Scene Context
から作成が可能です。
今回のContext
ではScene Context
を指定しているため、このContext
が配置されているシーンが影響範囲として設定されます。
Context
の種類に関して更に詳しく知りたい方は以下の記事がおすすめです。
参考:【Unity】【Zenject】DIの影響範囲を指定するContextの使い方まとめ
作成されたScene Context
に対して、先ほど作成したSample Installer
をアタッチしておき、それをScene Context
のMono Installers
に追加します。
これでSample Installer
が走るようになりました。
実際に動作テストを行っても正常に動くかと思われます。
もしも開発途中で別のInputクラスが使用したくなっても、Installer
クラス内部でのInputFromKeyboard
を置き換えれば良いだけとなります。非常に便利ですね。
Extenject
にはまだまだ様々な機能があるようなので、今後もじっくり学習していきます。
また、以下の記事も今後学習を進めていくのに、有用かと思われますのでリストとして載せておきます。
- Zenjectを使うときに気を付けていること
- 2018-09-07
Unity #Zenject完全に理解した に行ってきました - Zenjectの初期化処理の順番に潜む罠
- 【Unity】new DiContainer()から理解するZenject【DI】
参考
- 【Unity】【Zenject】Zenjectをサクッと使って理解する
- 【Unity】Zenjectに学ぶDependency Injection
- Zenject入門その1 疎結合とDI Container
- 【Unity】ZenjectのTutotialをやってみた(1)
- 2018-09-07
Unity #Zenject完全に理解した に行ってきました - 【Unity】DIフレームワーク「Extenject(Zenject)」についてまとめました
- Dependency InjectionとZenject
- 【Unity(C#)】Extenject(Zenject)使って入力機能を切り離したものを管理してみた
- 【Unity】ZenjectのAsSingle(),AsCached(),AsTransient()の違い