【Unity】依存性注入とは？と、なっているのでZenject(Extenject)を入門してみた

# 最初に

どうも、ろっさむです。

今回は「依存性注入」とは何なのかを理解するために、Zenjectを入門してみる手順をまとめたものとなります。

## 開発環境

- Unity：2020.2
- Extenject：9.2.0

# 依存性注入とは何か

英語だと「Dependency Injection」、通称「DI」と呼ばれているものです。依存性注入と言われていますが、「Dependency」を「必要なもの」とか「ないと困るもの」とかのイメージで考えると良さそうです。

参考：["Dependency"を使って、パンチの効いた制約関係の表現をする](https://ameblo.jp/winning-english/entry-12578646062.html)

DI自体は、「IT用語辞典バイナリ」にて以下のような説明がされています。

> DIとは、プログラミングにおけるデザインパターン（設計思想）の一種で、オブジェクトを成立させるために必要となるコードを実行時に注入（Inject）してゆくという概念のことである。

参考：[IT用語辞典バイナリ > DI](https://www.weblio.jp/content/Dependency+Injection)

つまりクラスA内で必要となるクラスBの機能を、必要となったタイミングで引っ張ってくる感じです。そうすることで、クラス間での密な結合を防ぐことができます。よくわからんなーって人は後ほど記載する、使用例とサンプルコードを確認してみてください。

この引っ張ってくる方法を提供してくれるのが、今回紹介する「Extenject」となります。

# Extenjectとは

DIを実現するためのフレームワークです。

こちらはUnityのアセットストアからインポートを行うことができます。

[Extenject Dependency Injection IOC](https://assetstore.unity.com/packages/tools/utilities/extenject-dependency-injection-ioc-157735)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/e55b4471-9095-eaa4-a5ad-ae95d9d9a6fa.png)

こちらは元々「Zenject」というフレームワークをメインで管理していた方が、会社の退社後に個人でメンテを始めたフレームワークのようです。現在は「Extenject」の方が最新のUnityへの対応などが行われているため、これから使用する場合は「Extenject」をインポートすることをお勧めします。

参考：[Zenjectをメンテナンスするために生まれたExtenjectというリポジトリ](https://note.com/enpel/n/n0d6ce4060332)

## import 方法

先ほどのアセットストアのリンクから`マイアセットに追加する > Unityで開く > Package ManagerにてExtenject Dependecy Injection IOC を検索するか、窓を開く > DL > Import`までを行ってください。

そうすると `Assets > Plugins > Zenject`というフォルダが作成されます。早速試していきましょう。

# 使用例とテストコード

例えば、MyCharacterクラスがあるとします。

今回は適当に`Hierarcy > 3D Object > Sphere`を作成して、`MyCharacter`という名前をつけておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/dfcaffbc-03a8-7b37-54e9-89fb8718d760.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/5121b963-d2ae-31ec-9763-952df3b992d6.png)

今回はこのキャラクターの移動制御を行うための処理を`Extenject`を使用して構築していきます。

ここでなぜ`Extenject`を使用するのかというと、input処理を`UnityEngine.Input`以外からも受け付けられるようにするためです。
例えば、現在主流なのは`UnityEngine.Input`を用いた移動処理の実装だと思いますが、この他にもUnity2020から使用可能となった`Input System`や[Rewired](https://assetstore.unity.com/packages/tools/utilities/rewired-21676)という入力管理系アセットに差し替えたくなる時が来るかもしれません。その場合、Input処理を変更するのに、`UnityEngine.Input`だけに依存した処理にしていると、多くのリファクタが発生してしまいます。
今回の`MyCharacter`クラスで考えてみましょう。

移動処理用のコンポーネントとして`InputForMove`を用意しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/71a9fdde-c1bb-1c01-9d2b-782a6b4ca3c0.png)

この`Update()`内部では、`UnityEngine.Input`を直接参照しています。この状態から、例えば先ほどあげた`Input System`でのInput処理を使いたい場合は、この`Update()`内部を書き換える必要が出てきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/0bff3ca4-a364-fff8-d9e6-1f3dc00dc6b2.png)

`interface`を使用しても良いのですが、結局どこかでオブジェクトを`new()`する必要があり、プロジェクトが大規模になるほど、その場所はコードの奥深くに位置する可能性が高くなります。

ここで、`new()`を一つの場所に集約しつつも、疎結合となるような作り方を行うために`Extenject`を使用します。

### まずはinterfaceと各実装を用意

Inputクラスが共通で持つ処理を`Interface`側に記述をし、各Inputクラスで実装を行うようにしてみましょう。

#### Interface作成

雑に`IInputtable`という名前にしておきます。

```C#
using UnityEngine;

public interface IInputtable
{
    Vector3 InputForMove();
}
```

この`Vector3`値は移動するために使用する、Input情報を用いて作成したものとなります。

### Inputクラス作成

まずはKeyboard入力用を作成してみます。

```C#
using UnityEngine;

public class InputFromKeyboard : IInputtable
{
    public Vector3 InputForMove()
    {
        return new Vector3(Input.GetAxis("Horizontal"), 
            0, Input.GetAxis("Vertical"));
    }
}
```

### オブジェクトにアタッチ用のスクリプト作成

実際に動かすオブジェクトに対してアタッチするスクリプトを作成し、内部でInput入力を受け取って位置を更新する処理を記述します。

```C#
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`を選択し、作成してみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/793f0558-5b6d-94f7-e69b-63c99725e0fe.png)

今回は`IInputtable`のインターフェース型の変数に注入する場合、`InputFromKeyboard`クラスを使用したいので、以下のように記述してみます。

```C#
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`から作成が可能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/769adf22-4907-3f0c-7ac9-68cdd60a9096.png)

今回の`Context`では`Scene Context`を指定しているため、この`Context`が配置されているシーンが影響範囲として設定されます。

`Context`の種類に関して更に詳しく知りたい方は以下の記事がおすすめです。

参考：[【Unity】【Zenject】DIの影響範囲を指定するContextの使い方まとめ](https://light11.hatenadiary.com/entry/2019/02/25/233318)

作成された`Scene Context`に対して、先ほど作成した`Sample Installer`をアタッチしておき、それを`Scene Context`の`Mono Installers`に追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/9cdc44b2-75f9-fe6f-803e-bc97f7c1d563.png)

これで`Sample Installer`が走るようになりました。

実際に動作テストを行っても正常に動くかと思われます。

もしも開発途中で別のInputクラスが使用したくなっても、`Installer`クラス内部での`InputFromKeyboard`を置き換えれば良いだけとなります。非常に便利ですね。

`Extenject`にはまだまだ様々な機能があるようなので、今後もじっくり学習していきます。

また、以下の記事も今後学習を進めていくのに、有用かと思われますのでリストとして載せておきます。

- [Zenjectを使うときに気を付けていること](https://adarapata.hatenablog.com/entry/unity-advent-calendar-2019)
- [2018-09-07
Unity #Zenject完全に理解した に行ってきました](https://www.nowsprinting.com/entry/2018/09/07/084118)
- [Zenjectの初期化処理の順番に潜む罠](https://virtualcast.jp/blog/2020/04/zenject_initialize_order_trap/)
- [【Unity】new DiContainer()から理解するZenject【DI】](https://qiita.com/su10/items/bc7363bf7b84abfe6c58)


# 参考

- [【Unity】【Zenject】Zenjectをサクッと使って理解する](https://light11.hatenadiary.com/entry/2019/02/16/225023)
- [【Unity】Zenjectに学ぶDependency Injection](https://qiita.com/akihiro_0228/items/d6fe2bfea5306900624d)
- [Zenject入門その1　疎結合とDI Container](https://qiita.com/toRisouP/items/b3d3c43db40857ca4ad4)
- [【Unity】ZenjectのTutotialをやってみた（１）](https://qiita.com/kannan_xiao4/items/5f93d6402656334af4c1)
- [2018-09-07
Unity #Zenject完全に理解した に行ってきました](https://www.nowsprinting.com/entry/2018/09/07/084118)
- [【Unity】DIフレームワーク「Extenject（Zenject）」についてまとめました](https://www.theylive.co.jp/unitydi%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E3%83%AF%E3%83%BC%E3%82%AFextenjectzenject%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6%E3%81%BE%E3%81%A8/)
- [Dependency InjectionとZenject](https://note.com/thedesignium/n/n771fb79ab263)
- [【Unity(C#)】Extenject(Zenject)使って入力機能を切り離したものを管理してみた](https://qiita.com/OKsaiyowa/items/c1841951aa1ce18999ef)
- [【Unity】ZenjectのAsSingle(),AsCached(),AsTransient()の違い](http://yutakaseda3216.hatenablog.com/entry/2017/04/21/153555)