【Unity】DI(依存性の注入)

2023年3月22日

VContainerは、Unityに特化したDIコンテナライブラリで、UnityのGameObjectやScriptableObject、MonoBehaviourなどのコンポーネントに対しても依存性の注入が可能です。また、コンポーネントのライフサイクル管理やエントリーポイントの自動検出などの機能も備えています。VContainerは、C# 8.0以降の新機能を活用した宣言的な構成を提供し、簡潔なコードで依存性の管理ができるように設計されています。VContainerはオープンソースで、GitHub上で公開されています。

サンプルについて

Unity向けDIコンテナ作者のページのサンプルをベースに実際に動作を見ていきましょう

実行結果

サンプルを実行した結果です
コンソール画面を確認してください

Collapseをオンにしておきます
表示カウンターを確認してください

上段のHelloWorld表示は、UnityでのUpdateと同じようにカウントアップされています
下段のHelloWorld表示は、ボタンをクリックするたびに表示されます

シーンの構成

クラス図

このクラス図では、3つのクラスが定義されています。

  • GamePresenter: は、制御フローのみに責任をもちます。どの入力に反応してなにを実行すべきかをつなぎ合わせます。
  • HelloWorldService: は、Hello World って出力することそれだけに責任を持ちます。
  • HelloScreen: は、Viewコンポーネントです。画面に表示する内容だけに責任を持ちます。

ドメインロジック / 制御フロー / Viewコンポーネント をそれぞれ分割し、PresenterへのDIによって参照関係をつなげることができました。一般にこういうコンセプトは MVP(Model-View-Presenter) と呼ばれています。

GamePresenterは、IStartableとITickableという2つのインターフェイスを実装しています。これらのインターフェイスは、プログラムが実行される間に開始および更新のタイミングでメソッドを呼び出すために使用されます。

GamePresenterは、HelloWorldServiceとHelloScreenという2つの依存関係を持っています。これらの依存関係は、GamePresenterのコンストラクタで注入されます。

HelloScreenは、MonoBehaviourを継承しており、HelloButtonというボタンを持っています。

最後に、GameLifetimeScopeはLifetimeScopeを継承し、Configureメソッドをオーバーライドしています。これは、依存関係注入のために使用されるDIコンテナの構成を行うためのものです。

HelloWorldServiceは、Helloメソッドを公開しています。これらのメソッドは、GamePresenterから呼び出されます。

コンストラクタインジェクションは、オブジェクト指向プログラミングにおける依存性注入の一つの方法です。依存性注入とは、あるクラスが別のクラスやオブジェクトに依存している場合、その依存関係を外部から注入することで、柔軟性やテスト容易性を向上させる手法です。

コンストラクタインジェクションは、依存するオブジェクトや値を、そのクラスのコンストラクタの引数として渡すことで、依存性を注入します。これにより、クラスの利用者が依存するオブジェクトの具体的な実装や生成方法を意識することなく、依存性を注入することができます。

例えば、以下のようなJavaのコードでは、コンストラクタインジェクションを使用して、UserRepositoryオブジェクトを依存性として注入しています。

public class UserService {
  private UserRepository userRepository;

  public UserService(UserRepository userRepository) {
    this.userRepository = userRepository;
  }

  // ユーザーに関するビジネスロジックの実装
}

このようにコンストラクタインジェクションを使用することで、UserServiceクラスが依存するUserRepositoryクラスの実装詳細を意識する必要がなく、別の実装に差し替えることも容易になります。また、テスト時にもモックオブジェクトを注入することで、独立したユニットテストを実行することができます。

サンプルコード

HelloWorldService

Hello World って出力することそれだけに責任を持ちます

// UnityEngineを使用するための宣言
using UnityEngine;

// HelloWorldServiceというクラスを定義
public class HelloWorldService
{
    // Helloというpublicなメソッドを定義
    public void Hello()
    {
        // "Hello world"という文字列をUnityのログに出力する
        Debug.Log("Hello world");
    }
}

上記のコードは、UnityのゲームオブジェクトにHAアタッされません。HelloWorldServiceというクラスを定義し、その中にHelloメソッドを作成しています。Helloメソッドは、"Hello world"という文字列をUnityのログに出力するだけの簡単な機能を持ちます。

GamePresenter

制御フローのみに責任をもちます。どの入力に反応してなにを実行すべきかをつなぎ合わせます

// VContainerライブラリを使用するためのusingディレクティブをインポートする。
using VContainer;

// VContainer.Unityライブラリを使用するためのusingディレクティブをインポートする。
using VContainer.Unity;

// GamePresenterクラスの定義。
public class GamePresenter : IStartable, ITickable
{
    // HelloWorldServiceクラスのインスタンス。
    HelloWorldService helloWorldService;

    // HelloScreenクラスのインスタンス。
    HelloScreen helloScreen;

    // GamePresenterクラスのコンストラクタ。
    [Inject]
    public GamePresenter(HelloWorldService helloWorldService, HelloScreen helloScreen)
    {
        // HelloWorldServiceクラスのインスタンスを保持する。
        this.helloWorldService = helloWorldService;
        
        // HelloScreenクラスのインスタンスを保持する。
        this.helloScreen = helloScreen;
    }

    // IStartableインターフェースのメソッド。
    void IStartable.Start()
    {
        // HelloScreenクラスのHelloButtonにリスナーを追加し、ボタンが押されたときにHelloWorldServiceクラスのHelloメソッドを呼び出す。
        helloScreen.HelloButton.onClick.AddListener(() => helloWorldService.Hello());
    }

    // ITickableインターフェースのメソッド。
    public void Tick()
    {
        // HelloWorldServiceクラスのHelloメソッドを呼び出す。
        helloWorldService.Hello();
    }
}

このコードは、VContainerライブラリを使用して依存性注入を行い、Unity上で動作するGamePresenterクラスを定義しています。

GamePresenterクラスは、IStartableおよびITickableインターフェースを実装しており、StartメソッドおよびTickメソッドを持ちます。

GamePresenterクラスのコンストラクタには、HelloWorldServiceクラスのインスタンスとHelloScreenクラスのインスタンスがInject属性によって注入されています。

Startメソッドでは、HelloWorldServiceクラスのStartメソッドを呼び出すことができますが、コメントアウトされており、実行されません。また、HelloScreenクラスのHelloButtonにリスナーを追加し、ボタンが押されたときにHelloWorldServiceクラスのHelloメソッドを呼び出します。

Tickメソッドでは、HelloWorldServiceクラスのHelloメソッドを呼び出します。

HelloScreen

Viewコンポーネントです。画面に表示する内容だけに責任を持ちます。

// UnityEngine 名前空間を使用する
using UnityEngine;

// UnityEngine.UI 名前空間を使用する
using UnityEngine.UI;

// HelloScreen クラスを定義する
public class HelloScreen : MonoBehaviour
{
    // public な Button 型の変数 HelloButton を定義する
    public Button HelloButton;
}

上記のコードは、Unityのスクリプトであり、HelloScreenというクラスを定義しています。HelloButtonという公開されたButton型の変数があります。usingステートメントによって、UnityEngineとUnityEngine.UIの名前空間が使用されています。名前空間を使用することで、クラス名が重複する場合に区別できるようになります。ButtonクラスはUnityEngine.UI名前空間に存在しているため、この名前空間が必要です。

GameLifetimeScope

 DIの設定を施す(Composition Root)

クラスをDIコンテナに登録すると、必要な箇所へ自動的に参照関係をつくることができるようになります。(Auto-Wiring)

// Unityのエンジンを使うために必要な名前空間をインポートする
using UnityEngine;
// DIコンテナのVContainerを利用するために必要な名前空間をインポートする
using VContainer;
using VContainer.Unity;

// LifetimeScopeを継承するGameLifetimeScopeクラスを定義する
public class GameLifetimeScope : LifetimeScope
{
    // Unity Editor上でInspectorから設定可能なHelloScreenの参照を宣言する
    [SerializeField]
    HelloScreen helloScreen;

    // DIコンテナの構成を設定するメソッドをオーバーライドする
    protected override void Configure(IContainerBuilder builder)
    {
        // HelloWorldServiceクラスをSingletonで登録する
        builder.Register<HelloWorldService>(Lifetime.Singleton);
        
        // GamePresenterクラスをエントリーポイントとして登録する
        builder.RegisterEntryPoint<GamePresenter>();

        // Inspectorから設定したHelloScreenのコンポーネントを登録する
        builder.RegisterComponent(helloScreen);
    }
}

このコードは、VContainerというDIコンテナを使ってUnityアプリケーションを構築する際の、VContainerの設定を行うクラスです。LifetimeScopeクラスを継承しています。Configureメソッド内で、DIコンテナに対して、HelloWorldServiceクラスをSingletonで登録したり、GamePresenterクラスをエントリーポイントとして登録したり、HelloScreenコンポーネントを登録しています。このような設定を行うことで、DIコンテナを利用して依存性の注入ができるようになります。