Edited at

Zenject入門その1 疎結合とDI Container

More than 1 year has passed since last update.


はじめに

最近、Zenjectについて導入を検討する人が増えてきました。今回はそのZenjectがそもそも何のためのライブラリなのかを解説します。


Zenjectとは

Zenject Dependency Injection IOC」は依存性の注入のためのフレームワークと言われています。


よくある勘違い

Zenjectを導入すると、次のようなことができるようになると思っている人が多いですが、それは間違いです


  • Zenjectを入れると疎結合になる!

  • Zenjectを入れるとテストが書きやすくなる!

  • 何かよくわからないけど入れるとプログラムが書きやすくなる!

繰り返しますが、上記の認識は間違いです。


Zenjectの正しい説明

Zenjectは疎結合な設計やテストを書きやすくするためのライブラリではありません。

順序が逆で、疎結合やテストのことを考えて設計したときに発生してしまう問題を解決するものがZenjectです。

つまり、あらかじめ疎結合な設計やテストを考慮したプログラムになってないとZenjectは導入しても意味がないということになります。

そのため、Zenjectのメリットを説明するためにはどうしても「疎結合な設計」に対する理解が必要になります。

そういった点を考慮すると、Zenjectは初心者向けのライブラリではありません。ある程度プログラミングに慣れて、設計について意識し始めたときに導入を検討するくらいでちょうど良いくらいです。


「疎結合」と「依存性の注入」

さきほどから何度も「疎結合」というワードが登場しています。これについて詳しく説明しましょう。

まずは「疎結合」の反対の概念にあたる「密結合」から解説します。


密結合

密結合とは、名前のとおり「密な結合状態」を表します。

設計においては、「あるクラスが特定のクラスにべったり依存している」という状態を表します。

たとえばUnity開発において、よく発生する密結合な場面は「Input」周りでしょう。


密結合なInput

たとえば、次のようなコードは密結合な設計になります。

class Mover : MonoBehaviour

{
void Update()
{
if (Input.GetButton("Jump"))
{
Jump();
}

var inputVector = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
var isDash = Input.GetButton("Dash");

// 移動
Move(inputVector, isDash);
}

/// <summary>
/// ジャンプする
/// </summary>
void Jump()
{
/*
* 省略
*/

}

/// <summary>
/// 移動する
/// </summary>
/// <param name="direction">移動方向</param>
/// <param name="isDash">ダッシュするか</param>
void Move(Vector3 direction, bool isDash)
{
/*
* 省略
*/

}
}

何が密結合かと言うと、「UnityEngine.Input」を直接参照して利用しているという点です。

image.png

つまり、このスクリプトはUnityEngine.Input以外の要素から入力値を取得することができなくなっています。


「密結合なInput」では何がダメなのか?

これ以上の機能拡張やテストを書かないというのであればこれで問題ありません。

逆に言うと、機能拡張やテストを書きたくなったときに問題が出てきます。

たとえば、


  • UnityEngine.Inputではなく、RewirdのInputに差し替えたい

  • テストするときに任意のタイミングでInputイベントを差し込みたい

といったときにこのままでは対応ができません。


疎結合

疎結合とは、密結合の逆で、「特定のクラスに依存しない状態」になっていることを指します。

つまり、具体的なクラスには紐付かず、それを抽象化したインタフェースを参照する設計を指します。


密結合から疎結合へ

では、さきほどのInputの例をもとに、密結合から疎結合へと作り直してみます。

やるべき作業としては、「Moverが直接UnityEngine.Inputを触らない」ようにしてしまいます。

つまり、インタフェースを介してMoverはInputの状態を取得するという設計にします。

image.png


InputProvider

using UnityEngine;

namespace Player
{
interface IInputProvider
{
bool GetDash();
bool GetJump();
Vector3 GetMoveDirection();
}
}



UnityEngine.Inputを使うInputProvider

namespace InputProviders

{
public class UnityInputProvider : IInputProvider
{
public bool GetDash()
{
return Input.GetButton("Dash");
}

public bool GetJump()
{
return Input.GetButton("Jump");
}

public Vector3 GetMoveDirection()
{
return new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
}
}
}



疎結合化したMover

using UnityEngine;

namespace Player
{
class Mover : MonoBehaviour
{
private IInputProvider inputProvider;

public void SetInputProvider(IInputProvider input)
{
inputProvider = input;
}

void Update()
{
if (inputProvider.GetJump())
{
Jump();
}

var inputVector = inputProvider.GetMoveDirection();
var isDash = inputProvider.GetDash();

//移動
Move(inputVector, isDash);

}

/// <summary>
/// ジャンプする
/// </summary>
void Jump()
{
/*
* 省略
*/

}

/// <summary>
/// 移動する
/// </summary>
/// <param name="direction">移動方向</param>
/// <param name="isDash">ダッシュするか</param>
void Move(Vector3 direction, bool isDash)
{
/*
* 省略
*/

}
}
}


こうすることで、Moverはいまどの実装を使っているのかを意識することなく入力状態を取得することができるようになりました。

これが疎結合な設計という状態です。


疎結合にすることのメリット

疎結合化することにより、モジュールの差し替えが簡単にできるようになります。

モジュールを差し替えることで、「最初はUnityEngine.Inputで実装して、途中でRewirdに差し替える」「テスト用のモジュールに差し替えてテストを実行する」といったことができるようになります。

たとえば、さきほどのMoverを例にテストを書いてみます。


Moverのテストを書く


ちゃんと移動するように作ったMover

さきほどまでのMoverは説明のために実装を省略していたので、ちゃんと移動するように実装を追加します。

using UnityEngine;

namespace Player
{
public class Mover : MonoBehaviour
{
[SerializeField]
private float jumpPower = 5f;

[SerializeField]
private float defaultMoveSpeed = 10f;

private CharacterController characterController;

private IInputProvider inputProvider;

private Vector3 moveDirection;

public void SetInputProvider(IInputProvider input)
{
inputProvider = input;
}

void Start()
{
characterController = GetComponent<CharacterController>();
}

void Update()
{
moveDirection = Vector3.zero;

if (inputProvider == null) return;

if (inputProvider.GetJump())
{
Jump();
}

var inputVector = inputProvider.GetMoveDirection();
var isDash = inputProvider.GetDash();

//移動
Move(inputVector, isDash);

//重力加速度
moveDirection = new Vector3(
moveDirection.x,
moveDirection.y + Physics.gravity.y * Time.deltaTime + characterController.velocity.y,
moveDirection.z);

//移動
characterController.Move(moveDirection * Time.deltaTime);
}

/// <summary>
/// ジャンプする
/// </summary>
void Jump()
{
if (!characterController.isGrounded) return;
moveDirection = new Vector3(moveDirection.x, moveDirection.y + jumpPower, moveDirection.z);
}

/// <summary>
/// 移動する
/// </summary>
/// <param name="direction">移動方向</param>
/// <param name="isDash">ダッシュするか</param>
void Move(Vector3 direction, bool isDash)
{
var speed = isDash ? 3 : 1; //ダッシュすると3倍速い
moveDirection += (direction * speed * defaultMoveSpeed);
}
}
}

そして、これに対応するテスト(PlayModeTest)がこちらです。


MoverTest.cs

using UnityEngine;

using UnityEngine.TestTools;
using NUnit.Framework;
using System.Collections;
using InputProviders;
using Player;
using UnityEngine.SceneManagement;

public class MoverTest
{
private GameObject targetGameObject;
private TestInputProvider testInputProvider;

[SetUp]
public void Init()
{
testInputProvider = new TestInputProvider();
SceneManager.LoadScene("TestScene");
}

[TearDown]
public void Finish()
{
testInputProvider.Reset();
targetGameObject.transform.position = Vector3.zero;
}

private void InitLazy()
{
if (targetGameObject == null)
{
targetGameObject = GameObject.Find("Player");
var mover = targetGameObject.GetComponent<Mover>();
mover.SetInputProvider(testInputProvider);

testInputProvider.Reset();
targetGameObject.transform.position = Vector3.zero;
}
}

[UnityTest]
public IEnumerator 移動できる()
{
InitLazy();

// 前に進む
testInputProvider.MoveDirection = Vector3.forward;

yield return new WaitForSeconds(1);

// 前進している
Assert.True(targetGameObject.transform.position.z > 0);
}

[UnityTest]
public IEnumerator ダッシュできる()
{
InitLazy();

// 前に進む
testInputProvider.MoveDirection = Vector3.forward;

yield return new WaitForSeconds(1);

// 通常移動で進んだ距離
var normal = targetGameObject.transform.position.z;

testInputProvider.Reset();
targetGameObject.transform.position = Vector3.zero;
yield return null;

// ダッシュする
testInputProvider.IsDash = true;
testInputProvider.MoveDirection = Vector3.forward;

yield return new WaitForSeconds(1);

// ダッシュした方が遠くに移動できている
Assert.True(targetGameObject.transform.position.z > normal);
}

[UnityTest]
public IEnumerator ジャンプできる()
{
InitLazy();

// ジャンプする
testInputProvider.IsJump = true;

yield return new WaitForSeconds(1);

// ジャンプできている
Assert.True(targetGameObject.transform.position.y > 0);
}
}


重要なのはInitLazy()内のこのあたりです。

targetGameObject = GameObject.Find("Player");

var mover = targetGameObject.GetComponent<Mover>();
mover.SetInputProvider(testInputProvider);

SetInputProvider()を使って、テスト用のInputProviderMoverに登録しています。

このように、通常時はUnityInputProviderを渡し、テスト時はTestInputProviderに差し替える、みたいなことが疎結合化していると簡単に行えるようになります。


疎結合化することの問題点

疎結合化することによるデメリットはいくつかあるのですが、もっとも大きな問題は「利用するインスタンスの決定をどうやって行うのか?」というものです。

さきほどのMoverの説明ではしれっと「SetInputProvider()UnityInputProviderを与えればよい」みたいな説明をしましたが、じゃあ誰がどのタイミングでSetInputProvider()を呼び出すのか?という問題が起きることを無視していました。

この問題を回避する方法として、手法として「ServiceLocatorパターン」と「Dependency Injectionパターン」というものがあります。


ServiceLocator パターン

ServiceLocatorパターンは、簡単に言えば「ServiceLocatorというオブジェクトに対して依存関係を解決しにいく」という手法です。

たとえば次のような実装はServiceLocatorパターンになります。


依存関係の登録と取得ができるServiceLocator

public class ServiceLocator

{
private Dictionary<Type, object> container = new Dictionary<Type, object>();

public T Resolve<T>()
{
return (T)container[typeof(T)];
}

public void Register<T>(T instance)
{
container.Add(typeof(T), instance);
}
}



ServiceLocatorを取得できるシングルトン

public class ServiceLocatorProvider : SingletonMonoBehaviour<ServiceLocatorProvider>

{
public ServiceLocator Current { get; private set; }

private void Awake()
{
Current = new ServiceLocator();
// 依存関係を登録
Current.Register<IInputProvider>(new UnityInputProvider());
}
}



Mover(ServiceLocatorを使って依存性を解決する)


namespace Player
{
public class Mover : MonoBehaviour
{
//関係ない部分は省略

private IInputProvider inputProvider;

void Start()
{
// サービスロケータから利用するインスタンスを取得する
inputProvider = ServiceLocatorProvider.Instance.Current.Resolve<IInputProvider>();
}

void Update()
{
// 以下略
}


このように、依存関係の解決をServiceLocatorというオブジェクトに集約し、static変数経由でアクセスして解決する手法をServiceLocatorパターンと呼びます。

この手法は実装がシンプルで使い方も単純というメリットがあるのですが、一方で開発規模が大きくなって依存関係が複雑化すると途端にリファクタリングが困難になるというデメリットも持っています。(ServiceLocatorへの密結合がリファクタリング時に邪魔になってしまう。)

そのため、「規模が小さいプロジェクトで依存関係を解決する」といった場面や、「アプリケーションのグローバル設定を参照するのに使う」といったような限定的な場面で使うなど、できるだけ狭い範囲でのみServiceLocatorパターンは利用することを推奨します。

そして、規模が大きくなってきた場合は「Dependency Injectionパターン」を利用すると良いでしょう。


Dependency InjectionとDI Container

あらためて、「依存性の注入(Dependency Injection)」について解説します。

Dependency InjectionパターンではDI Containerと呼ばれるオブジェクトが登場します。

「依存性の注入」は日本語訳がわかりにくいのですが、意味としては「依存オブジェクトの注入」が近いです。

つまり、「状況に応じて適切なオブジェクトをインスタンスに設定していくパターン」みたいな意味です。

(依存性の注入、は長いので以降はDIと呼称します。)


DI Container

DIを実行するにあたり、DI Containerという概念が登場します。

DI Containerは簡単に言えば「環境にあらかじめ依存関係を設定しておくと、それに応じて自動的にインスタンスを設定してまわってくれる便利な存在」です。

ServiceLocatorパターンではオブジェトが自ら必要なインスタンスを取りに行くという仕組みでしたが、DIではその逆を行います。

環境に存在するDI Containerが、状況に応じて自動的にオブジェクトに対してインスタンスを設定してくれます。(仕組みはともかくとして、そういう便利な存在です。)

そして、このDI Containerの機能を提供してくれるライブラリがZenjectになります。


ZenjectのDI Containerを使ってみる

それでは、実際にZenjectのDI Containerを設定し、実際に使ってみましょう。

さきほどのMoverの例をもとに行っています。


1. Zenjectをインストールする

Asset Storeから導入すれば終わりです。


2. Installerを記述する

Installerとは、ZenjectのDI Containerに対して依存関係を記述してあげる場所になります。

いくつか種類があるのですが、今回はMonoBehaviourとして扱うことができるMonoInstallerを利用します。

ProjectView -> Create -> Zenject -> Mono Installer から作成ができます。

image.png

スクリプトが生成されたら、そこに次のような内容を記述します。


UnityInputProviderを使うInstaller

using InputProviders;

using Player;
using Zenject;

namespace ZenjectSample
{
public class UnityInputInstaller : MonoInstaller<UnityInputInstaller>
{
public override void InstallBindings()
{
Container
.Bind<IInputProvider>() // IInputProviderが要求されたら
.To<UnityInputProvider>() // UnityInputProviderを生成して注入する
.AsCached(); // ただし、UnityInputProviderが生成済みなら使い回す
}
}
}


DI Containerに対して、どのインタフェースが要求されたら、どのインスタンスを注入するか、そのときにインスタンスをどう扱うか、設定することができます。

今回は「IInputProviderが要求されたら、UnityInputProviderを注入する、そのときにUnityInputProviderがキャッシュされているならそれを使う」という設定をしています。


3. Contextを用意する

ContextInstallerの影響範囲を設定するものです。主に次の2種類があります。


  • Project Context : そのUnityProject全体に影響を及ぼす

  • Scene Context : そのContextが配置されたシーンにのみ影響を及ぼす

今回はScene Contextを使います。

Installerを配置したいシーンのHierarchyView上で、

右クリック -> Zenject -> Scene Context でSceneContextが配置されます

image.png


4. ContextにInstallerを設定する

生成したScene Contextに、さきほどのUnityInputInstallerを登録します。

image.png

場所はどこでもいいので、UnityInputInstallerをどこかのGameObjectに貼り付けます(今回はそのままScene ContextのGameObjectに配置)。

そして、Scene Context上のInstallers欄にこれを登録します。

これでこのシーン上でDI Continerが有効になりました。


5. DI ContainerからのInjectを受け入れられるようにする

Moverスクリプトを修正し、DIを受けれいられるようにします。

記法はいくつかありますが、MonoBehaviorを継承したオブジェクトの場合は「Field Injection」「Property Injection」「Method Injection」のどれかしか利用できません。

今回はどの記法で書いても挙動は同じになります。


Field InjectionでDI

フィールド変数に[Inject]属性をつけることで、そこにDIContainerがオブジェクトを注入してくれます。

using UnityEngine;

using Zenject;

namespace Player
{
public class Mover : MonoBehaviour
{
[Inject]
private IInputProvider inputProvider;

// 以下略


Property InjectionでDI

プロパティに[Inject]属性をつけることで、そこにDIContainerがオブジェクトを注入してくれます。

using UnityEngine;

using Zenject;

namespace Player
{
public class Mover : MonoBehaviour
{
[Inject]
private IInputProvider inputProvider { get; set; }

// 以下略


Method InjectionでDI

メソッドに[Inject]属性をつけることで、その引数に応じてDIContainerがオブジェクトを注入してくれます。

using UnityEngine;

using Zenject;

namespace Player
{
public class Mover : MonoBehaviour
{
private IInputProvider inputProvider;

[Inject]
private void Injection(IInputProvider inputProvider)
{
this.inputProvider = inputProvider;
}

// 以下略


6. 実行する

あとはこのままゲームを実行すると、シーンがロードされたタイミングでDI Containerがオブジェクトを探してまわり、注入を自動的に実行してくれます。

今回の場合は、シーンをロードするとMoverUnityInputProviderが自動的に注入されることになります。


7. GameObject.Instantiate するときは

GameObject.Instantiateを直接実行した場合、そのオブジェクトはDI Containerが認識してくれないためDIが実行されません。

DIを実行しながらGameObjectを生成する場合は次の方法を取る必要があります。


Factoryを使う場合

指定したPrefabからGameObjectを生成するFactoryを用意し、そのFactory経由でオブジェクトを生成することでDIを実行することができます。


MoverFactory

class MoverFactory : PlaceholderFactory<Mover>

{
// 中身は空で良い
}


Installer

public class UnityInputInstaller : MonoInstaller<UnityInputInstaller>

{
[SerializeField] private GameObject MoverPrefab;

public override void InstallBindings()
{
Container
.Bind<IInputProvider>() // IInputProviderが要求されたら
.To<UnityInputProvider>() // UnityInputProviderを生成して注入する
.AsCached(); // ただし、UnityInputProviderが生成済みなら使い回す

Container
.BindFactory<Mover, MoverFactory>() //MoverのFactoryはMoverFactoryであると紐付ける
.FromComponentInNewPrefab(MoverPrefab); //生成はPrefabをベースに行う
}
}



使い方

public class MoverGenerator: MonoBehaviour

{
[Inject] private MoverFactory factory;

void Start()
{
// 3つ作ってみる
factory.Create();
factory.Create();
factory.Create();
}
}



DI Containerを直接使う場合

別の方法として、DI Containerを直接DIして使うこともできます。ただしこちらのパターンはService Locatorパターンとやってることは同じになります。


Containerを直接使う場合

public class MoverGenerator: MonoBehaviour

{
[SerializeField] private GameObject MoverPrefab;

[Inject] private DiContainer container;

void Start()
{
// Containerから直接生成する
container.InstantiatePrefab(MoverPrefab);
}
}


なお、DI Containerを直接操作することで、AddComponent相当の処理を実行することもできます。


AddComponentをContainer経由で行う

public class MoverGenerator: MonoBehaviour

{
[Inject] private DiContainer container;

[SerializeField] private GameObject EmptySpherePrefab;

void Start()
{
// 何もついてないSphereをPrefabから作る
var go = Instantiate(EmptySpherePrefab);

// DIが不要なコンポーネントはそのままAddComponent
go.AddComponent<CharacterController>();

// DIが必要なコンポーネントはContainer経由で行う
container.InstantiateComponent<Mover>(go);
}
}



8. Container経由でInstantiateできない場合では

PhotonNetwork.Instantiateを使っているなど、Container経由でInstantiateできないシチュエーションなどがあると思います。

その場合はZenAutoInjectorコンポーネントをPrefabにあらかじめ貼り付けて置くことで、生成時に後追いでそのオブジェクトに対してDIが実行されるようになります。

ただし、この場合はStart()よりあとのタイミングでDIが実行されることになります。そのためDIが確実に完了していることを検知した上でUpdate()を実行するなどの工夫が必要になってきます。

たとえばMethodInjectionを使ってDIのタイミングでフラグを立てる、などするとよいでしょう。

using UnityEngine;

using Zenject;

namespace Player
{
public class Mover : MonoBehaviour
{
private bool isInitialized;

private IInputProvider inputProvider;

[Inject]
private void Injection(IInputProvider inputProvider)
{
this.inputProvider = inputProvider;

// DIが実行されたらフラグを立てる
isInitialized = true;
}

void Update()
{
if(!isInitialized) return; //初期化が終わってないならUpdateを実行しない


9. ZenjectBinding

ZenjectBindingは、DI Containerの機能を応用したもので、シーンロード時にMonoBehaviourの参照解決を行ってくれる機能です。


次のようなMonoBehaviourがあったときに、参照をZenjectBindを使って解決してみます。

using UnityEngine;

/// <summary>
/// 何かのマネージャ
/// </summary>
public class HogeManager : MonoBehaviour
{

}

using UnityEngine;

using Zenject;

public class Fuga : MonoBehaviour
{
// HogeManagerがほしい
[Inject] private HogeManager hogeManager;
}

続いて、Zenject Bindingというコンポーネントをどこでもいいので配置します。

ここのComponent欄に、DIしたいコンポーネントを登録します。

image.png

以上です。これで自動的にHogeManagerが要求されている場所に注入されるようになりました。


Bind Type

image.png

ついでに覚えておくとよいのが、このBind Typeです。それぞれ次のような意味です。



  • Self : 指定コンポーネントと型が完全に一致した場合のみDIする


  • AllInterfaces : 指定コンポーネントが実装するインタフェースが要求されている場合のみDIする


  • AllInterfacesAndSelf : Self + AllInterfacesになる


  • BaseType : 指定コンポーネントのインタフェースではなく、基底クラスが要求されている場合のみDIする


仕組み

ZenjectBindingは、次のようなMonoInstallerを自動生成しContextに登録してくれる機能にすぎません。


ZenjectBindingが生成するInstallerと同等の内容

public class GameInstaller : MonoInstaller

{
public HogeManager hogeManager;

public override void InstallBindings()
{
Container.Bind<HogeManager>().FromInstance(hogeManager);
}
}



Zenjectを使ってテストを実行する

ZenjectUnitTestFixture / ZenjectIntegrationTestFixtureを使うと、テスト実行時にContainerを設定することができるようになります。

なお、 ZenjectIntegrationTestFixture はAssetStoreからZenjectを導入した場合はデフォルトでは使えないようになっています。

Assets/Plugins/Zenject/OptionalExtras/ZenjectIntegrationTestFixture.zip を解凍して中のファイルを手動で配置することで利用可能になります。


MoverTestのZenject版

PreInstall()PostInstall()の間にDI Containerに対して操作することができるようになっています。

このタイミングでDI Containerにテスト用のオブジェクトを注入するように設定してあげることで、テストモジュールが差し込まれてテストができるようになります。

using Zenject;

using System.Collections;
using InputProviders;
using NUnit.Framework;
using Player;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;

public class MoverTestZenject : ZenjectIntegrationTestFixture
{
private TestInputProvider testInputProvider;
private GameObject targetGameObject;

[SetUp]
public void Init()
{
SceneManager.LoadScene("TestScene");
testInputProvider = new TestInputProvider();
}

void CommonInstall()
{
PreInstall();

Container.Bind<IInputProvider>().FromInstance(testInputProvider);

PostInstall();
}

[TearDown]
public void Finish()
{
testInputProvider.Reset();
targetGameObject.transform.position = Vector3.zero;
}

private void InitLazy()
{
if (targetGameObject == null)
{
targetGameObject = GameObject.Find("Player");

testInputProvider.Reset();
targetGameObject.transform.position = Vector3.zero;
}
}

[UnityTest]
public IEnumerator MoveTest()
{
CommonInstall();

InitLazy();

// 前に進む
testInputProvider.MoveDirection = Vector3.forward;

yield return new WaitForSeconds(1);

// 前進している
Assert.True(targetGameObject.transform.position.z > 0);
}

[UnityTest]
public IEnumerator DashTest()
{
CommonInstall();

InitLazy();

// 前に進む
testInputProvider.MoveDirection = Vector3.forward;

yield return new WaitForSeconds(1);

// 通常移動で進んだ距離
var normal = targetGameObject.transform.position.z;

testInputProvider.Reset();
targetGameObject.transform.position = Vector3.zero;
yield return null;

// ダッシュする
testInputProvider.IsDash = true;
testInputProvider.MoveDirection = Vector3.forward;

yield return new WaitForSeconds(1);

// ダッシュした方が遠くに移動できている
Assert.True(targetGameObject.transform.position.z > normal);
}

[UnityTest]
public IEnumerator JumpTest()
{
CommonInstall();

InitLazy();

// ジャンプする
testInputProvider.IsJump = true;

yield return new WaitForSeconds(1);

// ジャンプできている
Assert.True(targetGameObject.transform.position.y > 0);
}
}


まとめ


  • Zenjectを使うためには、あらかじめ疎結合な設計にしておく必要がある

  • Zenjectは依存性の注入を行うためのフレームワークである

  • DI Containerの扱いが非常に重要になる

  • 設計の勉強がしたいならこの資料をみましょう