VContainer入門(6) - LifetimeScopeの親子関係

# この記事について
[前回](https://qiita.com/sakano/items/872a9e4cdf14be0f0c61)に引き続きVContainerの説明をしていきます。

今回はLifetimeScopeの親子関係を制御する方法を紹介します。
親子関係を通して依存関係の範囲を階層構造で管理できます。

目次ページ: [VContainer入門](https://qiita.com/sakano/items/b91e01f7fc0a946090ac)




# 親子関係を試してみる
とりあえず使ってみます。

サンプルとして`Logger`、`Calculator`クラスと、その2つに依存する`CalculatorTest`クラスを用意します。

```cs
using UnityEngine;
using VContainer;
using VContainer.Unity;

// 先頭に[Logger]を付けてログ出力するクラス
public sealed class Logger
{
    public void Log(string message) => Debug.Log("[Logger] " + message);
}

// 足し算するだけのクラス
public sealed class Calculator
{
    public int Add(int a, int b) => a + b;
}

// LoggerとCalculatorに依存するクラス
public sealed class CalculatorTest : IInitializable
{
    private Logger logger;
    private Calculator calculator;

    [Inject]
    public CalculatorTest(Logger logger, Calculator calculator)
    {
        this.logger = logger;
        this.calculator = calculator;
    }

    // クラスがインスタンス化された直後に呼ばれる
    public void Initialize()
    {
        Calculate(1, 2);
    }

    private void Calculate(int a, int b)
    {
        int result = calculator.Add(a, b);
        logger.Log($"{a} + {b} = {result}");
    }
}
```

このCalculatorTestの依存関係を解決するために、次の`ParentLifetimeScope`と`ChildLifetimeScope`を用意します。

```cs:ParentLifetimeScope.cs
public class ParentLifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        builder.Register<Logger>(Lifetime.Singleton);
    }
}
```

```cs:ChildLifetimeScope.cs
public class ChildLifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        builder.Register<Calculator>(Lifetime.Singleton);
        builder.RegisterEntryPoint<CalculatorTest>();
    }
}
```

実際の親子関係はインスペクタ上で設定できます。

下の画像のようにParentLifetimeScopeとChildLifetimeScopeを配置して、ChildLifetimeScopeのParentの部分にParentLifetimeScopeを設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31952/310ea6fe-9d8d-6ab5-a0f3-88540fcefd3d.png)

実行すると「1 + 2 = 3」と表示されてCalculatorTestが生成されていることを確認できます。

## 親のLifetimeScopeの探索
インスペクタのParentで指定すると、ロード中のシーン全体からそのLifetimeScopeを探索します。
なのでParentのLifetimeScopeはシーンのどこかに配置しておく必要があります。




# 親子関係があるLifetimeScopeの依存性解決
LifetimeScopeの親子関係は依存解決の順番に影響します。

具体的には、子のLifetimeScopeから親に向かって見つかるまで順番に探索されます。

例えば、先ほどのCalculatorTestの場合はCalculatorとLoggerに依存しています。
CalculatorはChildLifetimeScopeの中でRegisterされているのが見つかります。
LoggerはChildLifetimeScopeの中では見つからないので、親のParentLifetimeScopeでRegisterされているのが見つかります。

もし一番上の親まで見つからなければ例外が発生します。




# RootLifetimeScope
全てのLifetimeScopeの親となる、ルートのLifetimeScopeを設定できます。

次の手順で設定できます。

1. Unityメニューの「Assets > Create > VContainer > VContainer Settings」でVContainerSettingsを作る
1. 空のPrefabを作る
1. PrefabにルートにしたいLifetimeScopeをアタッチする
1. 作成したPrefabをVContainerSettingsのRootLifetimeScopeに設定する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31952/c36d184e-4e2c-f33c-e5cd-8f7fc9868bfe.png)




# 親子関係を設定する
インスペクタのParentに設定する以外でも親子関係を設定できます。

## LifetimeScopeのスクリプトで親を設定する
LifetimeScopeのAwakeで`parentReference.Object`に親LifetimeScopeを直接設定できます。

```cs
public class ChildLifetimeScope : LifetimeScope
{
    protected override void Awake()
    {
        var parentLifetimeScope = gameObject.AddComponent<ParentLifetimeScope>();

        // 親にしたいLifetimeScopeを渡す
        parentReference.Object = parentLifetimeScope;

        // 必ずbase.Awake()を呼び出す
        base.Awake();
    }
}
```

### 親を設定してLifetimeScopeを生成する
`LifetimeScope.EnqueueParent`を使うと、これから生成するLifetimeScopeの親を設定できます。

```cs
var parentLifetimeScope = gameObject.AddComponent<ParentLifetimeScope>();

using (LifetimeScope.EnqueueParent(parentLifetimeScope))
{
    // ここで生成されたLifetimeScopeの親はparentLifetimeScopeになる
    gameObject.AddComponent<ChildLifetimeScope>();
}
```

ここではChildLifetimeScopeの親をparentLifetimeScopeにするのに使っています。
より実践的な使い道としては、usingのブロックの中でシーン読み込みやPrefab生成をすれば、そのシーンやPrefabの中の全てのLifetimeScopeの親を設定できます。

ただしLifetimeScope.EnqueueParentのネストは対応されていません。
LifetimeScope.EnqueueParentの中でLifetimeScope.EnqueueParentを使おうとするとおかしな動作になります。

### 親を設定してPrefabを生成する
`CreateChildFromPrefab`を使うと、親を設定してPrefabを生成できます。

```cs
public sealed class TestMonoBehaviour : MonoBehaviour, ITestMonoBehaviour
{
    [SerializeField] private LifetimeScope prefab;

    public void Start()
    {
        var parentLifetimeScope = gameObject.AddComponent<ParentLifetimeScope>();

        LifetimeScope childLifetimeScope = parentLifetimeScope.CreateChildFromPrefab(prefab);
    }
}
```

CreateChildFromPrefabの引数にはPrefab内のLifetimeScopeを渡します。
渡したLifetimeScopeの親が設定された上でそのPrefabが生成されます。




# Lifetime.SingletonとLifetime.Scoped
Registerするときに指定するLifetime.Singleton/Scoped/Transientについて説明します。

```cs
protected override void Configure(IContainerBuilder builder)
{
    builder.Register<A>(Lifetime.Transient);
    builder.Register<B>(Lifetime.Singleton);
    builder.Register<C>(Lifetime.Scoped);
}
```

`Lifetime.Transient`はResolveするたびに毎回新しいインスタンスが生成されます。

`Lifetime.Scoped`はLifetimeScopeごとに別々のインスタンスが生成され、その中だけで使い回されます。
つまりLifetimeScopeの親子関係があるときは、親と子で別のインスタンスが生成されます。

`Lifetime.Singleton`は親子間でも同じインスタンスが使い回されます。