【Unity】DIコンテナの概要とVContainerの使い方について

Unity
マイケル
マイケル
みなさんこんにちは!
マイケルです!!
エレキベア
エレキベア
こんにちクマ〜〜〜
マイケル
マイケル
今日はUnityのDIフレームワークである VContainer について
使い方を紹介していくぜ!
エレキベア
エレキベア
最近はUnity界隈でDIが流行ってるクマね〜〜〜
マイケル
マイケル
今回の記事を書くにあたって、下記の公式ページとスライドを参考にさせていただきました!
リファリンスも分かりやすくまとまっているのでおすすめです。

About | VContainer
↑VContainerの公式ページ

Unity専用最速DIコンテナVContainer と、UnityにおけるDIの勘所
↑作者本人の説明スライド

エレキベア
エレキベア
シンプルで使いやすそうクマね〜〜〜
マイケル
マイケル
いきなりDIと言われても分からない方もいると思うので、
最初に 依存関係やDIについて説明した後、
ライブラリの使い方を説明
していこうかと思います!
スポンサーリンク

DI(依存性の注入)とは

マイケル
マイケル
まずはDIとは何か?という部分について解説していきます。
以前、SOLID原則について紹介しましたが、この中で出てくる
「DIP(依存性逆転の原則)」を実現するための手段としてDIが用いられます。
エレキベア
エレキベア
確か 依存関係を逆転させて、抽象に依存させるべき
という原則だったクマね
ScreenShot 2022 09 11 18 33 39↑抽象に依存している状態
マイケル
マイケル
その通り!
依存関係を制御するための手段として、「DI」「DIコンテナ」というものが使われているんだ。
まずはイメージを掴むために簡単なDIの例を見てみよう!

依存関係とDI

マイケル
マイケル
まず、下記のようなマイケルクラスがあったとします。
この中では所持モンスターを変数として持っています。
/// <summary>
/// マイケルクラス
/// </summary>
public class Michael
{
    /// <summary>
    /// 所持モンスター
    /// </summary>
    private ElekiBear _haveMonster;
    public Michael(ElekiBear monster)
    {
        _haveMonster = monster;
    }
}
↑マイケルクラスの実装
マイケル
マイケル
この実装はよく見ると、エレキベアクラスをそのまま使っているため、
エレキベア以外のモンスターを所持することはできません
エレキベア
エレキベア
誰が所持モンスターになんてなるかクマ
マイケル
マイケル
この状態のことをマイケルクラスはエレキベアクラスに依存していると言われます。
UML図で書くと下記のようになります。依存の向きが矢印の向きですね。
UntitledImage
↑マイケルクラスがエレキベアクラスに依存している
エレキベア
エレキベア
エレキベア以外のモンスターはダメということクマね
マイケル
マイケル
これを解決するにはインターフェイスを使用します。
下記のようにIMonster型で定義し直すと、IMonsterをimplementしているクラスであればどれでも所持モンスターとして設定できるようになります。
/// <summary>
/// マイケルクラス
/// </summary>
public class Michael
{
    /// <summary>
    /// 所持モンスター
    /// </summary>
    private IMonster _haveMonster;
    public Michael(IMonster monster)
    {
        _haveMonster = monster;
    }
}
↑インターフェイスで定義しなおした状態
マイケル
マイケル
これをUML図で表すとこんな感じ!
どのクラスもインターフェイスに依存しており、向きが逆転していることが分かります!
UntitledImage
↑矢印の向きが逆転している!
エレキベア
エレキベア
浮気クマか・・・
ゴロヤン
ゴロヤン
ゴロ〜〜〜(モンスター呼ばわりは心外です)
マイケル
マイケル
これが冒頭で話した 抽象に依存している状態になります!
設定する側はこんな感じです!
/// <summary>
/// マイケル管理クラス
/// </summary>
public class MichaelManager
{
    public void Initialize()
    {
        var michael = new Michael(new ElekiBear());
        michael = new Michael(new Goloyan()); // 別モンスターにも設定できる
    }
}
↑注入している例
マイケル
マイケル
依存性(オブジェクト)を引き渡していますが、
このことを依存性の注入(DI)と言います。
エレキベア
エレキベア
注入する依存性というのは渡すオブジェクトへの依存のことだったクマね
マイケル
マイケル
ただ見ても分かる通り、今度は注入するクラス(MichaelManager)の方が
各モンスタークラスに依存
するようになっています。
これを解決するために、依存関係を定義するクラスをDIコンテナという形で分けることで
DIを実現しやすくする
、というわけです。
public class SampleLifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        // インターフェースを登録
        builder.Register<IMonster, ElekiBear>(Lifetime.Singleton);
    }
}
↑DIコンテナの例(VContainerでの実装。IMonsterとElekiBearを紐づけている)
エレキベア
エレキベア
なるほどクマ
DIコンテナは依存性の注入(DI)をしやすくするためのものだったクマね
マイケル
マイケル
その通り!
各用語の意味をまとめると下記のようになります!

  • DIP(依存性逆転の原則)
    依存の向きを逆転させて抽象に依存すべきである、という原則。
  • DI(依存性の注入)
    外側からオブジェクトの参照を渡すことで依存関係を取り除く手法。
  • DIコンテナ
    依存関係を別途定義するコンテナを作成し、DIを行いやすくするもの。

エレキベア
エレキベア
用語がややこしいクマが、なんとなくイメージは分かったクマね
マイケル
マイケル
DIやDIコンテナのイメージが湧いてきたところで、
VContainerの使い方について見ていこう!

VContainerの使い方

マイケル
マイケル
VContainerは下記の特徴を持つUnityのDIフレームワークです!

  • Unityに最適化されたDIフレームワーク
  • MonoBehaviourを制御することを思想としている
  • 類似ライブラリのZenjectと比べてシンプルで高速

マイケル
マイケル
DIについてやZenjectとの比較については、
下記にまとまっているのでご参照ください!

Comparing to Zenject | VContainer

DIって何? | VContainer

エレキベア
エレキベア
リファレンスも分かりやすいクマ〜〜〜

導入方法

マイケル
マイケル
導入はunitypackageかUPM経由でインストールしましょう!

インストール | VContainer

ScreenShot 2022 09 11 16 27 18↑PackageManagerからインストール

基本の使い方

マイケル
マイケル
まずはDIコンテナとなるLifetimeScopeを定義します。
ここでは処理の起点となるエントリーポイントとシングルトンのクラスを登録してみます。
using VContainer;
using VContainer.Unity;
public class GameLifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        // シングルトン登録
        builder.Register<GameManager>(Lifetime.Singleton);
        // エントリーポイント登録
        builder.RegisterEntryPoint<GameEntryPoint>();
    }
}
↑エントリーポイントとシングルトンを登録
マイケル
マイケル
それぞれの処理は下記の通り!
コンストラクタに[Inject]を指定することで登録したクラスが渡されるようになります!
using VContainer.Unity;
/// <summary>
/// エントリーポイント
/// </summary>
public class GameEntryPoint : IStartable, ITickable
{
    private readonly GameManager _gameManager;
    [Inject]
    public GameEntryPoint(GameManager gameManager)
    {
        _gameManager = gameManager;
    }
    public void Start()
    {
        _gameManager.OnStart();
    }
    public void Tick()
    {
        _gameManager.OnUpdate();
    }
}
↑エントリーポイント
/// <summary>
/// ゲーム管理クラス
/// </summary>
public class GameManager
{
    public void OnStart()
    {
        // TODO
    }
    public void OnUpdate()
    {
        // TODO
    }
}
↑シングルトン
マイケル
マイケル
VContainerに用意されているIStartable、ITickableといったライフサイクルのインターフェイスを使用することで、MonoBehaviourと同様の挙動を行うことができます。
GameManagerクラスを見て分かる通り、何にも依存していないクリーンなコードになっていることが分かります。
エレキベア
エレキベア
MonoBehaviour以外を起点にすることができるクマね
マイケル
マイケル
次はインタフェースと紐づけて登録してみます。
using VContainer;
using VContainer.Unity;

public class GameLifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
・・・略・・・

        // サービス登録
        builder.Register<IHelloService, HelloService>(Lifetime.Singleton);

・・・略・・・
    }
}
マイケル
マイケル
こうすることで下記のようにコンストラクタに注入されます。
using UnityEngine;

/// <summary>
/// ゲーム管理クラス
/// </summary>
public class GameManager
{
    private readonly IHelloService _helloService;

    [Inject]
    public GameManager(IHelloService helloService)
    {
        _helloService = helloService;
    }

    public void OnStart()
    {
        var message = _helloService.GetHelloMessage();
        Debug.Log(message);
    }

    public void OnUpdate()
    {
    }
}
エレキベア
エレキベア
最初に見ていたDIの形クマね
マイケル
マイケル
あとはLifetimeScopeクラスをオブジェクトにアタッチして実行すれば、
正常に実行できることが確認できます。
ScreenShot 2022 09 11 17 25 15↑LifetimeScopeクラスをアタッチ
ScreenShot 2022 09 11 17 27 22↑ログが出力された
エレキベア
エレキベア
シンプルに使えるクマね

MVPの実装

マイケル
マイケル
次は簡単なMVPの実装をしてみます。
MonoBehaviourクラスはRegisterComponent関数で登録します。
また、エントリーポイントは複数登録することもできます。
using UnityEngine;
using VContainer;
using VContainer.Unity;
public class GameLifetimeScope : LifetimeScope
{
    [SerializeField] private GameView gameView;
    protected override void Configure(IContainerBuilder builder)
    {
        // シングルトン登録
        builder.Register<GameManager>(Lifetime.Singleton);
        // サービス登録
        builder.Register<IHelloService, HelloService>(Lifetime.Singleton);
        // コンポーネント登録
        builder.RegisterComponent(gameView); // UIを登録
        // エントリーポイント登録
        builder.RegisterEntryPoint<GameEntryPoint>();
        builder.RegisterEntryPoint<GamePresenter>(); // 複数登録できる
    }
}
↑ViewとPresenterの登録
マイケル
マイケル
それぞれの実装は下記の通り!
PresenterがViewクラスを受け取る形になっていることが確認できます。
using VContainer;
using VContainer.Unity;

public class GamePresenter : IStartable
{
    private readonly IHelloService _helloService;
    private readonly GameView _gameView;

    [Inject]
    public GamePresenter(IHelloService helloService, GameView gameView)
    {
        _helloService = helloService;
        _gameView = gameView;
    }

    public void Start()
    {
        var message = _helloService.GetHelloMessage();
        _gameView.SetSampleText(message);
    }
}
using UnityEngine;
using UnityEngine.UI;

public class GameView : MonoBehaviour
{
    [SerializeField] private Text sampleText;
    public void SetSampleText(string text)
    {
        sampleText.text = text;
    }
}
マイケル
マイケル
処理を実行すると、正常に表示されることが確認できました!
ScreenShot 2022 09 11 17 40 04
エレキベア
エレキベア
MVP実装もスマートに実装できるクマね〜〜

その他の使い方

マイケル
マイケル
基本的な使い方は以上になりますが、
それ以外の機能や使い方についてもいくつか紹介します!

Injectの方法

マイケル
マイケル
先ほどはコンストラクタにInjectしましたが、
関数、変数に対してもInjectすることができます。
public class MonsterBehaviour : MonoBehaviour
{
    // 変数にインジェクションする方法
    [Inject] private IHelloService _helloService;

    // 関数にインジェクションする方法
    private IHelloService2 _helloService2;
    [Inject]
    private void Construct(IHelloService2 helloService2)
    {
        _helloService2 = helloService2;
    }
    
}
マイケル
マイケル
ただし、下記のような理由からコンストラクタ経由でのInjectが推奨されています。
  • インスタンス化された時点で依存オブジェクトが揃っていることが保証される
  • 依存関係の見通しがよくなり、責務の把握がしやすくなる
マイケル
マイケル
そのため、MonoBehaviourクラス以外は
基本的にコンストラクタでInjectする方がよいと思います。
エレキベア
エレキベア
責務が把握できてるとクラス分割の目安にもなるクマね〜〜

LifetimeScopeでの登録方法

マイケル
マイケル
LifetimeScopeでの登録方法には他にもいくつかあります。
public class GameLifetimeScope : LifetimeScope
{
    [SerializeField] private MonsterData monsterData; // ScriptableObject
    [SerializeField] private GameView gameView;       // MonoBehaviour

    protected override void Configure(IContainerBuilder builder)
    {
        // スコープの種類
        builder.Register<GameManager>(Lifetime.Singleton); // 親か自身のコンテナがオブジェクトを生成して保持する
        builder.Register<GameManager>(Lifetime.Scoped);    // 自身のコンテナがオブジェクトを生成して保持する
        builder.Register<GameManager>(Lifetime.Transient); // 毎回インスタンスを作成する

        // クラスの登録
        // 使用された時にインスタンス化する
        builder.Register<GameManager>(Lifetime.Singleton); // 具象クラス登録
        builder.Register<IHelloService, HelloService>(Lifetime.Singleton); // インターフェイスと紐づけて登録

        // インスタンス登録
        // 既に存在するインスタンスを登録する(ScriptableObjectなど)
        builder.RegisterInstance(monsterData);

        // コンポーネント登録
        // MonoBehaviourなど
        builder.RegisterComponent(gameView); // UIを登録

        // エントリーポイント登録
        builder.RegisterEntryPoint<GameEntryPoint>();
        builder.RegisterEntryPoint<GamePresenter>();
    }
}
マイケル
マイケル
詳細は公式のリファレンスをご参照ください!

Lifetime Overview | VContainer

エレキベア
エレキベア
オブジェクトのスコープ定義やインスタンス登録もできるクマね

LifetimeScopeの親子階層定義

マイケル
マイケル
また、LifetimeScopeは階層構造を定義することもできます。
LifetimeScopeオブジェクトのParentに設定することで親に指定できます。
ScreenShot 2022 09 11 18 31 00
マイケル
マイケル
また、VContainerの設定ファイルを作成して Root Lifetime Scope に指定することで、
全てのLifetimeScopeの親に指定することもできます。
ScreenShot 2022 09 11 18 32 12↑VContainerの設定ファイルを作成
ScreenShot 2022 09 11 18 33 38↑RootとなるLifetimeScopeを定義
エレキベア
エレキベア
階層構造を定義することで
依存関係を更に整理することができるクマね

Container API

マイケル
マイケル
最後はCotaninerAPIの使い方についてです!
IObjectResolver型で受け取ることでコンテナに直接アクセスすることができます。

Container API | VContainer

マイケル
マイケル
使い方は下記のようになります。
コンテナ経由でInstantiateすることで動的に生成したオブジェクトにInjectしたり、サービスロケータ的な使い方をすることもできます。
using UnityEngine;
using VContainer;
using VContainer.Unity;

/// <summary>
/// ゲーム管理クラス
/// </summary>
public class GameManager
{
    private readonly IObjectResolver _objectResolver;
    private readonly MonsterBehaviour _monsterBehaviour;
    private readonly IHelloService _helloService;
    public GameManager(IObjectResolver objectResolver, MonsterBehaviour monsterBehaviour, IHelloService helloService)
    {
        _objectResolver = objectResolver;
        _monsterBehaviour = monsterBehaviour;
        _helloService = helloService;
    }

    public void OnStart()
    {
        // コンテナ経由でInstantiateする
        var monster = _objectResolver.Instantiate(_monsterBehaviour);

        // サービスロケータ的な使い方もできる
        var service = _objectResolver.Resolve<IHelloService>();
        var message = service.GetHelloMessage();
        Debug.Log(message);
    }
}
エレキベア
エレキベア
これで動的なオブジェクト生成も安心クマね

おわりに

マイケル
マイケル
というわけで今回はDIコンテナの概要とVContainerの使い方についてでした!
どうだったかな??
エレキベア
エレキベア
DIコンテナと聞くと難しそうだったクマが、
シンプルで使いやすかったクマね
マイケル
マイケル
使い方は簡単だけど、うやむやに依存関係を定義すると逆に複雑になってしまうから
なるべくシンプルな依存関係になるよう整理しながら使用するように気をつけよう!
マイケル
マイケル
それでは今日はこの辺で!
アデューー!!
エレキベア
エレキベア
クマ〜〜〜

【Unity】DIコンテナの概要とVContainerの使い方について 〜完〜

コメント