【Unity】Sub-Containers And Facades 日本語訳【Zenject】

# はじめに

なんだかすごいと噂の [Zenject](https://github.com/modesttree/Zenject) というアセットがあります。このアセットの"GameObjectContext"という機能に関して日本語の解説がどこにもなかったので、これについて説明しているドキュメントのページを日本語訳してみました。

Zenjectマスターに、俺はなる！

---
以下、 [Sub-Containers And Facades](https://github.com/modesttree/Zenject/blob/master/Documentation/SubContainers.md) の日本語訳

# サブコンテナとFacades

同一のアプリケーション内で複数のコンテナを使うのが便利な場合があります。例えばあなたがワードプロセッサを作っているとしたら、別々のドキュメントを表すタブごとにそれぞれのサブコンテナを持つのが便利かもしれません。この場合、サブコンテナ内に`AsSingle()`なクラス群をバインドでき、それらはまるでシングルトンクラスのように容易に互いを参照し合うことが可能です。サブコンテナはそれぞれのドキュメントごとにインスタンス化可能で、ドキュメントを扱うための全てのクラスのユニークなインスタンスを持ちます。

別の例として、オープンスペースの宇宙船ゲームを設計している場合、各宇宙船ごとに独自のコンテナ（その宇宙船を動かすためのすべてのクラスインスタンスを含んでいる）を持たせることができます。

ここで、`ProjectContext`の実際の動作は次のようになっています。プロジェクト全体で一つのコンテナがあり、Unityのシーンが開始したときに各`SceneContext`は`ProjectContext`の下に生成されます。シーンの`MonoInstaller`に追加されたすべてのバインディングは`SceneContext`コンテナにバインドされます。これにより、サブコンテナは親（および祖先）コンテナ内のすべてのバインディングを自動的に継承するため、シーンの依存関係に`ProjectContext`バインディングが自動的に挿入されます。

これはコード内で他のモジュールから利用されるときに、関連した依存関係のグループを高いレベルで抽象化するのに用いられます。これが関係してくるのは、アプリケーションでサブコンテナを定義するときに、サブコンテナ全体と相互作用するために使用されるFacadeクラスを定義することが非常に便利な場合が多いためです。したがって、上記の宇宙船の例に適用するには、"Start Engine","Take Damage","Fly to destination"などの宇宙船での非常に高いレベルの操作を表す`SpaceshipFacade`クラスを持つかもしれません。そして、`SpaceshipFacade`クラスはこれらのリクエスト全てに対して、内部的にその処理をサブコンテナ内に存在する依存関係に委譲することができます。

以下のセクションでいくつか例を挙げてみましょう。


## 例）サブコンテナ/Facadeを用いた Hello World

```csharp
public class Greeter
{
    readonly string _message;

    public Greeter(string message)
    {
        _message = message;
    }

    public void DisplayGreeting()
    {
        Debug.Log(_message);
    }
}

public class GameController : IInitializable
{
    readonly Greeter _greeter;

    public GameController(Greeter greeter)
    {
        _greeter = greeter;
    }

    public void Initialize()
    {
        _greeter.DisplayGreeting();
    }
}

public class TestInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container.BindInterfacesTo<GameController>().AsSingle();
        Container.Bind<Greeter>().FromSubContainerResolve().ByMethod(InstallGreeter).AsSingle();
    }

    void InstallGreeter(DiContainer subContainer)
    {
        subContainer.Bind<Greeter>().AsSingle();
        subContainer.BindInstance("Hello World!");
    }
}
```

ここで大事なのは、`InstallGreeter`メソッド内で追加されるすべてのバインディングはこのサブコンテナ内でだけアクセス可能であるということです。唯一の例外はFacadeクラス（この場合は`Greeter`）で、これは`FromSubContainerResolve()`バインディングを用いて親コンテナにバインドされています。言い換えれば、この例における"Hello World"という文字列は`Greeter`クラスにしか見えません。

`FromSubContainerResolve()`に与えられたクラスはサブコンテナ内のインストールメソッド内でバインドされなければならないことに注意してください。そうしないと例外が発生し、バリデーションは失敗します。

また、通常は上記の例のように`ByMethod()`を使用するよりも、`ByInstaller()`を使用したほうが良いことに注意してください。これは`ByMethod()`を使用すると`subContainer`ではなく`Container`を誤って使用する可能性が高くなるためです。また、`ByInstaller()`を使用するとインストーラ自体に引数を渡すこともできます。


# サブコンテナ内での IInitializable / ITickable / IDisposable の使用

上記のHello Worldの例には問題があります。`ITickable`または`IInitializable`または`IDisposable`をサブコンテナに追加したい場合、それは機能しません。例えば次のような場合：

```csharp
public class GoodbyeHandler : IDisposable
{
    public void Dispose()
    {
        Debug.Log("Goodbye World!");
    }
}

public class HelloHandler : IInitializable
{
    public void Initialize()
    {
        Debug.Log("Hello World!");
    }
}

public class Greeter
{
    public Greeter()
    {
        Debug.Log("Created Greeter!");
    }
}

public class TestInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container.Bind<Greeter>().FromSubContainerResolve().ByMethod(InstallGreeter).AsSingle().NonLazy();
    }

    void InstallGreeter(DiContainer subContainer)
    {
        subContainer.Bind<Greeter>().AsSingle();

        subContainer.BindInterfacesTo<GoodbyeHandler>().AsSingle();
        subContainer.BindInterfacesTo<HelloHandler>().AsSingle();
    }
}
```

`NonLazy()`を使用しているため`Greeter`クラスが作成されて"Created Greeter!"と表示されますが、"Hello"と"Goodbye"は表示されません。これが動くようにするには次のように変更する必要があります。

```csharp
public class Greeter : Kernel
{
    public Greeter()
    {
        Debug.Log("Created Greeter");
    }
}

public class TestInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container.BindInterfacesAndSelfTo<Greeter>()
            .FromSubContainerResolve().ByMethod(InstallGreeter).AsSingle().NonLazy();
    }

    void InstallGreeter(DiContainer subContainer)
    {
        subContainer.Bind<Greeter>().AsSingle();

        subContainer.BindInterfacesTo<GoodbyeHandler>().AsSingle();
        subContainer.BindInterfacesTo<HelloHandler>().AsSingle();
    }
}
```

これで実行時に"Hello World!"が表示され、停止したときに"Goodbye World!"が表示されるようになりました。

これが動作する理由は、`Greeter`クラスを`Container.BindInterfacesAndSelfTo<Greeter>()`を使用してルートコンテナの`IInitializable`,`IDisposable`,`ITickable`にバインドしたからです。`Greeter`は`Kernel`を継承しています。`Kernel`はこれらすべてのインターフェイスを継承し、これらの呼び出しを`IInitializable`,`ITickable`,`IDisposable`のサブコンテナに転送します。ここでは`AsSingle()`を使用することに注意してください。そうしないと、すべてのインターフェイスごとに新しいサブコンテナが作成されてしまいます。


# GameObjectContextを使用してGameObjectのサブコンテナを作成する

上記のサブコンテナを用いた"Hello World"サンプルのもう一つの問題点は、`MonoBehaviour`クラスではうまく動かないということです。`FromCoponentInNewPrefab()`や`FromNewComponentOnNewGameObject`などを使用して`MonoBehaviour`バインディングをサブコンテナに追加することは可能ですが、そうするとこれらの`GameObject`はシーンのヒエラルキーのrootに追加されてしまうため、Facadeが`Destroy()`されたときにこれらの`GameObject`を自分で追跡して`GameObject.Destroy()`で破棄するなどの寿命管理を行う必要があります。また、サブコンテナに含まれかつシーンの開始時に存在するような`GameObject`は作れません。これらの問題は`GameObjectContext`を使用することで解決できます。

この例では、サブコンテナの紹介で説明したオープンワールドの宇宙船のようなものを実際に実装してみます。

- 新しいシーンを作成する
- プロジェクトに次のファイルを追加する

```csharp
using Zenject;
using UnityEngine;

public class Ship : MonoBehaviour
{
    ShipHealthHandler _healthHandler;

    [Inject]
    public void Construct(ShipHealthHandler healthHandler)
    {
        _healthHandler = healthHandler;
    }

    public void TakeDamage(float damage)
    {
        _healthHandler.TakeDamage(damage);
    }
}
```
```csharp
using UnityEngine;
using Zenject;

public class GameRunner : ITickable
{
    readonly Ship _ship;

    public GameRunner(Ship ship)
    {
        _ship = ship;
    }

    public void Tick()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            _ship.TakeDamage(10);
        }
    }
}
```
```csharp
public class GameInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container.BindInterfacesTo<GameRunner>().AsSingle();
    }
}
```
```csharp
using Zenject;
using UnityEngine;

public class ShipHealthHandler : MonoBehaviour
{
    float _health = 100;

    public void OnGUI()
    {
        GUI.Label(new Rect(Screen.width / 2, Screen.height / 2, 200, 100), "Health: " + _health);
    }

    public void TakeDamage(float damage)
    {
        _health -= damage;
    }
}
```
```csharp
using UnityEngine;
using System.Collections;

public class ShipInputHandler : MonoBehaviour
{
    [SerializeField]
    float _speed = 2;

    public void Update()
    {
        if (Input.GetKey(KeyCode.UpArrow))
        {
            this.transform.position += Vector3.forward * _speed * Time.deltaTime;
        }

        if (Input.GetKey(KeyCode.DownArrow))
        {
            this.transform.position -= Vector3.forward * _speed * Time.deltaTime;
        }
    }
}
```

- ヒエラルキーを右クリックして`Zenject -> Scene Context`を選択する
- `SceneContext`の`GameObject`に`GameInstaller`をドラッグする
- `SceneContext`の`Installers`プロパティに新しい行を追加する
- `Installers`の下の新しい行に`GameInstaller`コンポーネントをドラッグする
- ヒエラルキーを再び右クリックして`Zenject -> Game Object Context`を選択する
- 新しくできた`GameObject`である`GameObjectContext`を`Ship`にリネームする
- `Ship`という`GameObject`に`Ship`というコンポーネントをドラッグする
  - この`Ship`クラスは船を高レベルで抽象化したFacadeクラスとして他のクラスから使用される
- `Ship`の`GameObject`に`ShipInputHandler`コンポーネントをつける
- `Ship`の`GameObject`を右クリックして`3D Object -> Cube`を選択する（船の3Dモデルの代わり）
- `Ship`の`GameObject`の下に`HealthHandler`という`GameObject`を作成し、`ShipHealthHandler`コンポーネントをつける

これでシーンは次のようになります。

![img](https://github.com/modesttree/Zenject/raw/master/UnityProject/Assets/Plugins/Zenject/Documentation/ShipFacadeExample1.png?raw=true)

- ここでのアイデアは、船のゲームオブジェクト下にあるすべてがそれ自身のサブコンテナ内にあるとみなされるということです。 作業が終わったら、`ShipHealthHandler`,`ShipInputHandler`など独自のコンポーネントを持つ複数の船をシーンに追加して、シングルトンとして扱うことができます。
- CTRL+SHIFT+Vを押してシーンのバリデーションを行ってみてください。次のようなエラーが表示されます：`Unable to resolve type 'ShipHealthHandler' while building object with type 'Ship'.`
- これは`ShipHealthHandler`コンポーネントがサブコンテナに追加されていないからです。次のように対処します：
  - `HealthHandler`の`GameObject`をクリックして`ZenjectBinding`コンポーネントを追加
  - `HealthHandler`コンポーネントを`ZenjectBinding`のフィールドにドラッグ
- CTRL+SHIFT+Vで再びバリデーションを行ってみてください。別のエラーが表示されるようになります：`Unable to resolve type 'Ship' while building object with type 'GameRunner'.`
- `Ship`コンポーネントもコンテナに追加される必要があります。これも同じように対処します：
  - `Ship`の`GameObject`をクリックして`ZenjectBinding`コンポーネントを追加
  - `Ship`コンポーネントを`ZenjectBinding`のフィールドにドラッグ
- 再びバリデーションを行うと、同じエラーが出ることに注意しましょう。これはデフォルトで`ZenjectBinding`は最も近いコンテナ（この場合は`Ship`）にのみコンポーネントを追加するからです。これは求めていた動作ではありません。`Ship`をサブコンテナのFacadeとしてシーンコンテナに追加したかったのです。これに対処するには`ZenjectBinding`に「バインディングがどのコンテキストに適用されるべきか」を明示的に教える必要があります。これは`SceneContext`の`GameObject`を`ZenjectBinding`の`Context`プロパティにドラッグすることで可能です。
- これでバリデーションが成功するようになりました。
- スクリーンの中央に体力が表示され、スペースキーを押すとダメージが適用され、上下キーで船が動くようになりました。

また、`SceneContext`にインストーラを追加する方法と同じやり方でサブコンテナにもインストーラを追加することができます。`GameObjectContext`の`Installers`プロパティにドラッグするだけです。この例ではすべて`MonoBehaviour`を使用していますが、`ZenjectBinding`を使用して`MonoBehaviour`の場合と同じように、ここで必要なプレーンなC＃クラスをいくつも追加して、サブコンテナ内のあらゆる場所で使用できます。

# `GameObjectContext`を動的に作成する

上記の船の例に引き続き、ゲームが開始された後で船を動的に作ってみます。

- まず最初に、上記で作成した`Ship`の`GameObject`をプレハブ化し、シーンから削除します。
- 次に、以下の変更を加えてください：

```csharp
public class GameRunner : ITickable
{
    readonly Ship.Factory _shipFactory;

    Vector3 lastShipPosition;

    public GameRunner(Ship.Factory shipFactory)
    {
        _shipFactory = shipFactory;
    }

    public void Tick()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            var ship = _shipFactory.Create();
            ship.transform.position = lastShipPosition;

            lastShipPosition += Vector3.forward * 2;
        }
    }
}
```
```csharp
public class GameInstaller : MonoInstaller
{
    [SerializeField]
    GameObject ShipPrefab;

    public override void InstallBindings()
    {
        Container.BindInterfacesTo<GameRunner>().AsSingle();

        Container.BindFactory<Ship, Ship.Factory>().FromSubContainerResolve().ByNewPrefab(ShipPrefab);
    }
}
```

- さらに、インスペクタの`GameInstaller`の`ShipPrefab`プロパティに、新しく作成したShipプレハブをドラッグ&ドロップしてください。
- シーンを実行してスペースキーを押しと複数のShipをシーンに追加できます。


# GameObjectContextをパラメータを伴って動的に作成する

`Ship`のFacadeにパラメータを渡し、もっと面白くしてみましょう。船の速度を`GameRunner`クラス内から設定できるようにします。

クラスを次のように変更してください：

```csharp
public class GameRunner : ITickable
{
    readonly Ship.Factory _shipFactory;

    Vector3 lastShipPosition;

    public GameRunner(Ship.Factory shipFactory)
    {
        _shipFactory = shipFactory;
    }

    public void Tick()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            var ship = _shipFactory.Create(Random.RandomRange(2, 20));
            ship.transform.position = lastShipPosition;

            lastShipPosition += Vector3.forward * 2;
        }
    }
}
```

```csharp
public class GameInstaller : MonoInstaller
{
    [SerializeField]
    GameObject ShipPrefab;

    public override void InstallBindings()
    {
        Container.BindInterfacesTo<GameRunner>().AsSingle();

        Container.BindFactory<float, Ship, Ship.Factory>().FromSubContainerResolve().ByNewPrefab<ShipInstaller>(ShipPrefab);
    }
}
```

```csharp
using Zenject;
using UnityEngine;

public class Ship : MonoBehaviour
{
    ShipHealthHandler _healthHandler;

    [Inject]
    public void Construct(ShipHealthHandler healthHandler)
    {
        _healthHandler = healthHandler;
    }

    public void TakeDamage(float damage)
    {
        _healthHandler.TakeDamage(damage);
    }

    public class Factory : Factory<float, Ship>
    {
    }
}
```

```csharp
using UnityEngine;
using System.Collections;
using Zenject;

public class ShipInputHandler : MonoBehaviour
{
    [Inject]
    float _speed;

    public void Update()
    {
        if (Input.GetKey(KeyCode.UpArrow))
        {
            this.transform.position += Vector3.forward * _speed * Time.deltaTime;
        }

        if (Input.GetKey(KeyCode.DownArrow))
        {
            this.transform.position -= Vector3.forward * _speed * Time.deltaTime;
        }
    }
}
```

また、以下の新しいファイルを追加します：

```csharp
using System;
using Zenject;

public class ShipInstaller : MonoInstaller
{
    [Inject]
    float _speed;

    public override void InstallBindings()
    {
        Container.BindInstance(_speed).WhenInjectedInto<ShipInputHandler>();
    }
}
```

コンパイル後、`Ship`プレハブに`ShipInstaller`を追加して`GameObjectContext`の`Installers`フィールドにドラッグします。

変更点は以下のようになります：

- `ShipInputHandler`の`speed`はUnityの`SerializeField`を使う代わりにinjectされるようになった
- `Ship`内のネストされたFactoryクラスは`float`のパラメータを持つようになった
- `GameInstaller`内のFactoryのバインディングが異なる
- `GameRunner`内でFactoryのcreateメソッドに`float`のパラメータを渡す必要がある

Factoryを使用してサブコンテナを作成する際の重要な違いの1つは、Factoryに提供するパラメータが必ずしもFacadeクラスに転送されるとは限らないということです。この例におけるパラメータは速度を表すfloat値で、`Ship`クラスの代わりに`ShipInputHandler`クラスに転送されます。そのため、これらのパラメータはサブコンテナのインストーラに常に転送されるため、インストール時にパラメータの処理方法を自分で決めることができます。このようにするもう一つの理由は、パラメータが異なるバインディングを選択するために使用される場合があるからです。

デザイン時に`Ship`プレハブをシーンに設置したい場合があるかもしれません。`Ship`をシーンの開始から設置し、かつ動的に作成できるようにするには`ShipInstaller`を次のように変更します。

```csharp
using System;
using Zenject;
using UnityEngine;

public class ShipInstaller : MonoInstaller
{
    [SerializeField]
    [InjectOptional]
    float _speed;

    public override void InstallBindings()
    {
        Container.BindInstance(_speed).WhenInjectedInto<ShipInputHandler>();
    }
}
```

こうすることで`Ship`プレハブをシーンに置いてインスペクタで`_speed`を制御でき、また動的に作成してパラメータとしてfactoryに渡すこともできます。

より現実的な例として、`GameObjectContext`を大量に使用しているSpaceFighterサンプルプロジェクトを参照してください。