
マイケル
みなさんこんにちは!
マイケルです!!
マイケルです!!

エレキベア
こんにちクマ〜〜〜

マイケル
今日はUnityのDIフレームワークである VContainer について
使い方を紹介していくぜ!
使い方を紹介していくぜ!

エレキベア
最近はUnity界隈でDIが流行ってるクマね〜〜〜

マイケル
今回の記事を書くにあたって、下記の公式ページとスライドを参考にさせていただきました!
リファリンスも分かりやすくまとまっているのでおすすめです。
リファリンスも分かりやすくまとまっているのでおすすめです。
About | VContainer
↑VContainerの公式ページ
Unity専用最速DIコンテナVContainer と、UnityにおけるDIの勘所
↑作者本人の説明スライド

エレキベア
シンプルで使いやすそうクマね〜〜〜

マイケル
いきなりDIと言われても分からない方もいると思うので、
最初に 依存関係やDIについて説明した後、
ライブラリの使い方を説明 していこうかと思います!
最初に 依存関係やDIについて説明した後、
ライブラリの使い方を説明 していこうかと思います!
DI(依存性の注入)とは

マイケル
まずはDIとは何か?という部分について解説していきます。
以前、SOLID原則について紹介しましたが、この中で出てくる
「DIP(依存性逆転の原則)」を実現するための手段としてDIが用いられます。
以前、SOLID原則について紹介しましたが、この中で出てくる
「DIP(依存性逆転の原則)」を実現するための手段としてDIが用いられます。

エレキベア
確か 依存関係を逆転させて、抽象に依存させるべき
という原則だったクマね
という原則だったクマね


マイケル
その通り!
依存関係を制御するための手段として、「DI」「DIコンテナ」というものが使われているんだ。
まずはイメージを掴むために簡単なDIの例を見てみよう!
依存関係を制御するための手段として、「DI」「DIコンテナ」というものが使われているんだ。
まずはイメージを掴むために簡単なDIの例を見てみよう!
依存関係とDI

マイケル
まず、下記のようなマイケルクラスがあったとします。
この中では所持モンスターを変数として持っています。
この中では所持モンスターを変数として持っています。
/// <summary>
/// マイケルクラス
/// </summary>
public class Michael
{
/// <summary>
/// 所持モンスター
/// </summary>
private ElekiBear _haveMonster;
public Michael(ElekiBear monster)
{
_haveMonster = monster;
}
}
↑マイケルクラスの実装
マイケル
この実装はよく見ると、エレキベアクラスをそのまま使っているため、
エレキベア以外のモンスターを所持することはできません。
エレキベア以外のモンスターを所持することはできません。

エレキベア
誰が所持モンスターになんてなるかクマ

マイケル
この状態のことをマイケルクラスはエレキベアクラスに依存していると言われます。
UML図で書くと下記のようになります。依存の向きが矢印の向きですね。
UML図で書くと下記のようになります。依存の向きが矢印の向きですね。

↑マイケルクラスがエレキベアクラスに依存している

エレキベア
エレキベア以外のモンスターはダメということクマね

マイケル
これを解決するにはインターフェイスを使用します。
下記のようにIMonster型で定義し直すと、IMonsterをimplementしているクラスであればどれでも所持モンスターとして設定できるようになります。
下記のようにIMonster型で定義し直すと、IMonsterをimplementしているクラスであればどれでも所持モンスターとして設定できるようになります。
/// <summary>
/// マイケルクラス
/// </summary>
public class Michael
{
/// <summary>
/// 所持モンスター
/// </summary>
private IMonster _haveMonster;
public Michael(IMonster monster)
{
_haveMonster = monster;
}
}
↑インターフェイスで定義しなおした状態
マイケル
これをUML図で表すとこんな感じ!
どのクラスもインターフェイスに依存しており、向きが逆転していることが分かります!
どのクラスもインターフェイスに依存しており、向きが逆転していることが分かります!

↑矢印の向きが逆転している!

エレキベア
浮気クマか・・・

ゴロヤン
ゴロ〜〜〜(モンスター呼ばわりは心外です)

マイケル
これが冒頭で話した 抽象に依存している状態になります!
設定する側はこんな感じです!
設定する側はこんな感じです!
/// <summary>
/// マイケル管理クラス
/// </summary>
public class MichaelManager
{
public void Initialize()
{
var michael = new Michael(new ElekiBear());
michael = new Michael(new Goloyan()); // 別モンスターにも設定できる
}
}
↑注入している例
マイケル
依存性(オブジェクト)を引き渡していますが、
このことを依存性の注入(DI)と言います。
このことを依存性の注入(DI)と言います。

エレキベア
注入する依存性というのは渡すオブジェクトへの依存のことだったクマね

マイケル
ただ見ても分かる通り、今度は注入するクラス(MichaelManager)の方が
各モンスタークラスに依存するようになっています。
これを解決するために、依存関係を定義するクラスをDIコンテナという形で分けることで
DIを実現しやすくする、というわけです。
各モンスタークラスに依存するようになっています。
これを解決するために、依存関係を定義するクラスをDIコンテナという形で分けることで
DIを実現しやすくする、というわけです。
public class SampleLifetimeScope : LifetimeScope
{
protected override void Configure(IContainerBuilder builder)
{
// インターフェースを登録
builder.Register<IMonster, ElekiBear>(Lifetime.Singleton);
}
}
↑DIコンテナの例(VContainerでの実装。IMonsterとElekiBearを紐づけている)
エレキベア
なるほどクマ
DIコンテナは依存性の注入(DI)をしやすくするためのものだったクマね
DIコンテナは依存性の注入(DI)をしやすくするためのものだったクマね

マイケル
その通り!
各用語の意味をまとめると下記のようになります!
各用語の意味をまとめると下記のようになります!
- DIP(依存性逆転の原則)
依存の向きを逆転させて抽象に依存すべきである、という原則。 - DI(依存性の注入)
外側からオブジェクトの参照を渡すことで依存関係を取り除く手法。 - DIコンテナ
依存関係を別途定義するコンテナを作成し、DIを行いやすくするもの。

エレキベア
用語がややこしいクマが、なんとなくイメージは分かったクマね

マイケル
DIやDIコンテナのイメージが湧いてきたところで、
VContainerの使い方について見ていこう!
VContainerの使い方について見ていこう!
VContainerの使い方

マイケル
VContainerは下記の特徴を持つUnityのDIフレームワークです!
- Unityに最適化されたDIフレームワーク
- MonoBehaviourを制御することを思想としている
- 類似ライブラリのZenjectと比べてシンプルで高速

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

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

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

基本の使い方

マイケル
まずは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]を指定することで登録したクラスが渡されるようになります!
コンストラクタに[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クラスを見て分かる通り、何にも依存していないクリーンなコードになっていることが分かります。
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クラスをオブジェクトにアタッチして実行すれば、
正常に実行できることが確認できます。
正常に実行できることが確認できます。



エレキベア
シンプルに使えるクマね
MVPの実装

マイケル
次は簡単なMVPの実装をしてみます。
MonoBehaviourクラスはRegisterComponent関数で登録します。
また、エントリーポイントは複数登録することもできます。
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クラスを受け取る形になっていることが確認できます。
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;
}
}

マイケル
処理を実行すると、正常に表示されることが確認できました!


エレキベア
MVP実装もスマートに実装できるクマね〜〜
その他の使い方

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

マイケル
先ほどはコンストラクタに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する方がよいと思います。
基本的にコンストラクタで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に設定することで親に指定できます。
LifetimeScopeオブジェクトのParentに設定することで親に指定できます。


マイケル
また、VContainerの設定ファイルを作成して Root Lifetime Scope に指定することで、
全てのLifetimeScopeの親に指定することもできます。
全てのLifetimeScopeの親に指定することもできます。



エレキベア
階層構造を定義することで
依存関係を更に整理することができるクマね
依存関係を更に整理することができるクマね
Container API

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

マイケル
使い方は下記のようになります。
コンテナ経由でInstantiateすることで動的に生成したオブジェクトにInjectしたり、サービスロケータ的な使い方をすることもできます。
コンテナ経由で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の使い方について 〜完〜
コメント