【UnityC#】MonoBehaviourでもインターフェイスを利用して疎結合を実現する2つの方法

# はじめに

C#をはじめとするオブジェクト指向言語では、クラス同士が直接アクセスするのを避け、[インターフェイスを介してアクセスさせることで疎結合が実現される](https://qiita.com/yutorisan/items/d28386f168f2f3ab166d)ことは周知の事実です。そのため、__UnityC#でも`MonoBehaviour`同士をインターフェイスを介して依存させたい！でも`MonoBehaviour`の制約のせいでうまくいかない！__って方は割と多いのではないでしょうか。

例えば、以下のように`FugaComponent`が`HogeComponent`のインターフェイス`IHogeComponent`に依存したいとき。

```csharp
public interface IHogeComponent
{
    void SayHoge();
}
public class HogeComponent : MonoBehaviour, IHogeComponent
{
    public void SayHoge()
    {
        print("Hoge");
    }
}
```

```csharp
public class FugaComponent : MonoBehaviour
{
    // 疎結合のためインターフェイスを通じてアクセスさせたい。
    private IHogeComponent m_hoge; //←このm_hoge、どこから持ってくる！？

    void Start()
    {
        m_hoge.SayHoge();    
    }
}
```

`FugaComponent`はインターフェイス`IHogeComponent`をフィールドに持つことで`HogeComponent`と`FugaComponent`との結合度を弱めているのですが、`m_hoge`の中身はどうやって代入するのかはなかなか難しい問題です。というのも、

- __`SerializeField`属性をつけてインスペクタから代入しては？__<br>→インターフェイスはシリアライズできません！

- __コンストラクタから渡す？__<br>→`MonoBehaviour`なクラスの場合はコンストラクタ作れません！

- __`GetComponent<T>`からインターフェイス取得しては？__<br>→同じGameObjectにアタッチされているコンポーネント同士ならOKだけど、違う場合はGameObjectへの参照が必要になって美しくないし安全性も落ちる！

このような問題が発生するからです。
そこで本記事では、上記のような問題を解決するべく、`MonoBehaviour`なクラスにインターフェイスを引き渡す方法を2つ紹介します。

# 方法1. Odin Inspectorを使ってインターフェイスをシリアライズする

通常、次のようにインターフェイスをSerializeFieldしてもインスペクタ上には表示されません。


```csharp
public class FugaComponent : MonoBehaviour
{
    [SerializeField]
    private IHogeComponent m_hoge;
}
```

▲インターフェイスをSerializeFieldしても…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/eb69d12c-2bb6-cd32-f61e-8473055a7ea6.png)
▲インスペクタ上には表示されない

ところが、__[Odin Inspector](https://assetstore.unity.com/packages/tools/utilities/odin-inspector-and-serializer-89041?locale=ja-JP)__というアセットに含まれている__`SerializedMonoBehaviour`クラス__を使うと、インスペクタ上でインターフェイスを引き渡すことができるようになります。

```csharp
public class FugaComponent : SerialiedMonoBehaviour
{
    [SerializeField]
    private IHogeComponent m_hoge;
}
```

▲`MonoBehaviour`の代わりに`SerializedMonoBehaviour`を継承させると…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/bd831cfa-8578-b8e0-54c5-32fd59c4c703.png)
▲インターフェイスもインスペクタ上に現れる！

この方法であれば、__インスペクタ上でも`IHogeComponent`を実装したコンポーネントがアタッチされたGameObjectしかセットできない__ですし、ソースコード上にも余計なメンバが現れないため、安全かつ美しいです。

ただし、[Odin Inspector](https://assetstore.unity.com/packages/tools/utilities/odin-inspector-and-serializer-89041?locale=ja-JP)は__60.50ドルの有料アセット__です（たまに半額セールもある）。
お金をかけたくない場合は、次に紹介するまったく異なるアプローチを取ることもできます。



# 方法2. Zenjectを使ってインターフェイスを注入する

[Zenject(Extenject)](https://assetstore.unity.com/packages/tools/utilities/extenject-dependency-injection-ioc-157735)は、UnityC#で__Dependency Injection(DI、依存性の注入)[^1]__を実現する__無料アセット__です。


DI(依存性の注入)は簡単に言うと、クラス同士の依存関係を設定しておくと、必要な時に必要なオブジェクトを引き渡してくれる（=注入してくれる）仕組みです。

[^1]: この記事はZenject, DI(依存性の注入)について詳しく解説する記事ではありません！正確性に欠ける表現がある可能性があることをご了承ください。

Zenjectの使い方は複雑なので、順を追って説明します。

## 注入してほしいインターフェイスに`Inject`属性をつける

インスペクタからオブジェクトを引き渡す際は`SerializeField`属性を付けますが、Zenjectによってオブジェクトを注入する際は__`Inject`属性__を付けます。

```csharp
public class FugaComponent : MonoBehaviour
{
    [Inject]
    private IHogeComponent m_hoge;
}
```

ちょうど、`SerializeField`の代わりに`Inject`を付けるイメージです。こうすることで、__Zenjectに対して`IHogeComponent`を注入して欲しいです！という意思表示__ができます。

## 依存関係定義ファイル`MonoInstaller`を作成する

インターフェイスを注入すると言っても、実際に注入されるのは具体的なオブジェクトのインスタンスです。とあるインターフェイスの注入を要求されたとき、__具体的にはどんなオブジェクトを注入すれば良いのか__をZenjectに対して設定する必要があります。この設定ファイルを__Installer__と言います。

Projectビューのプラスボタンをクリックして、Zenject→Mono Installerをクリックします。任意の名前をつけてファイルを保存します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/1bd36ef3-7039-dc82-7392-79a371859c69.png)

作成されたファイルには、次のようなコードが記載されています。

```csharp
using UnityEngine;
using Zenject;

public class UntitledInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
    }
}
```

この`InstallBindings`メソッド内に、「どのインターフェイスが要求されたらどのオブジェクトを注入するか」を定義していきます。
例えば、`IHogeComponent`インターフェイスが要求されたら`HogeComponent`を注入するように設定する場合は、次のようにします。※あくまで一例です。

```csharp
using UnityEngine;
using Zenject;

public class UntitledInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container
            .Bind<IHogeComponent>()
            .To<HogeComponent>()
            .FromComponentInHierarchy()
            .AsCached();
    }
}
```

まずメソッド内1行目の`Container`ですが、これは`DiContainer`クラスのインスタンスです。`DiContainer`は設定通りにオブジェクトを注入してくれる核となる存在です。これからこの`DiContainer`に対してさまざまな依存関係の設定を行っていきます。

2行目の`Bind<T>`メソッドは「__このインターフェイスが要求されたら__」という意味です。`DiContainer`に対して、`IHogeComponent`が要求されたら(`Inject`されたら)、なんらかのオブジェクトを注入してね、と設定します。

3行目の`To<T>`メソッドは`Bind<T>`で設定されたインターフェイスが要求されたときに、__実際に注入するオブジェクト__を設定します。この例では、`IHogeComponent`が要求されたときに`HogeComponent`を注入するように設定しています。

4行目の`FromComponentInHierarchy`メソッドは、「注入するオブジェクトはヒエラルキー上から持ってきてね」と設定しています。
もちろんこれは一例であり、注入するオブジェクトはさまざまな場所から持ってくることができます。詳しくはこちらのサイトが参考になります。

https://light11.hatenadiary.com/entry/2019/02/20/234834

5行目の`AsCached`メソッドは、注入オブジェクトをキャッシュして再利用するように設定しています。例えば、`IHogeComponent`が複数のクラスから要求された場合、2回目以降はヒエラルキー上から持ってくるのではなく、キャッシュされたオブジェクトをそのまま注入します。
他にも`AsSingle`や`AsTransient`などがあります。詳しくはこちらのサイトが参考になります。

https://mikimemo.hateblo.jp/entry/2018/08/14/225644

以上で、ひとまず依存関係の設定は完了しました。

## Contextを用意してInstallerをセットする

最後に、作成したInstallerの影響範囲を設定します。これには、__Context__というものを使用します。
Contextにも様々ありますが、ここではひとつのシーン全体に影響する__SceneContext__を使用します。

もし、Contextに対して詳しく知りたい場合は、以下のようなサイトが参考になります。

https://light11.hatenadiary.com/entry/2019/02/25/233318

ヒエラルキービューのプラスボタンをクリックして、Zenject→Scene Contextをクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/115c97b3-77a3-c238-8979-bd3ed742564f.png)

すると、シーン上にSceneContextコンポーネントがアタッチされたGameObjectが作成されると思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/c25217dd-8bfa-d68a-c873-c580505cf84f.png)

SceneContextコンポーネントの`MonoInstallers`の部分に、先ほど作成したInstallerをセットします。
Add Componentボタンを押して先ほどのInstallerをアタッチし、それをMonoInstallersにドラッグしてセットできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/5b3ae286-9b04-fede-3cd0-ea91e4cd7c10.png)

以上で、SceneContextに対してInstallerのセットが完了しました。
これで晴れて、このSceneContextの影響範囲に対して、セットしたInstallerの設定内容に従って、オブジェクトの注入が実行されるようになります。

## Scene上に注入を要求するコンポーネントを配置する

SceneContextの影響範囲内（同一Scene内）に、オブジェクト注入を要求するコンポーネントを配置します。また、今回の例では`FromComponentInHierarchy`に設定しているため、ヒエラルキ上に注入されるオブジェクト（コンポーネント）も配置されている必要があります。

次のように、Scene上に注入される`HogeComponent`、注入を要求する`FugaComponent`をそれぞれアタッチしたGameObjectを配置します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/8ec407c3-6d04-a8b3-b9ec-eaf2a19d5c8e.png)

動作確認用に、`HogeComponent`と`IHogeComponent`を以下のように実装しました。

```csharp
public interface IHogeComponent
{
    void SayHoge();
}
public class HogeComponent : MonoBehaviour, IHogeComponent
{
    public void SayHoge()
    {
        print("Hoge");
    }
}
```

`FugaComponent`は`IHogeComponent`の注入を要求し、`SayHoge()`を実行します。

```csharp
public class FugaComponent : SerializedMonoBehaviour
{
    [Inject]
    private IHogeComponent m_hoge;

    void Start()
    {
        m_hoge.SayHoge();    
    }
}
```

この状態でPlayすると、コード上では`m_hoge`に対してオブジェクトを代入していないのに、きちんと`SayHoge()`が呼べていることが分かります。Zenjectによるオブジェクトの注入が効いている証拠です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/d4d49998-0e92-c330-c602-1e26a127b9df.png)

# OdinとZenjectの使い分け

以上、UnityC#においてインターフェイスを引き渡す方法を2つ紹介しました。
OdinとZenjectは、片方だけでもインターフェイスを引き渡すという目的は達成できるのですが、どちらにも得意な場面・不得意な場面があると考えています。

例えば、Odinの場合はインスペクタ上で視覚的にオブジェクトの注入ができるため、__同じインターフェイスを実装していても実体が異なる複数のインスタンスを注入したい場合__に向いています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/9fed93a6-be1b-6214-d35c-46a348c706ea.png)
▲異なる複数のインスタンスを視覚的に注入できる
これをZenjectで行うには、Installerの設定時にIDを使った注入設定を細かく行う必要があるため、設定と管理が煩雑になります。

反対にゲームのManager的なクラスなど、__ただひとつのインスタンスしか持たないオブジェクトを注入したい__場合、インスペクタ上で手動で注入するよりも、依存関係を設定してZenjectにやってもらったほうが楽です。

このように、OdinとZenjectは得意不得意があると思いますので、__結局は両方とも導入して、適材適所で使い分ける__のが一番良いのではないかと考えています。
Odinはインターフェイス注入以外にもインスペクタを使いやすくする機能が揃ってますし、DIはUnityだけではなく様々な場面で使われるデザインパターンです。どちらも勉強しておいて損はないかと思います（自分も勉強中）。

# 番外編. どうしてもOdinを買わずにインスペクタでインターフェイスを渡したい場合

昔の自分がOdinを使わずに無理矢理インスペクタでインターフェイスを（擬似的に）渡す方法を考えてました。
ご興味ある方は下記参照ください。

https://qiita.com/yutorisan/items/9e1c7cc29abd5e8f271b#自力で考えたインスペクタでinterfaceを渡す方法