Unity 向けのサービスロケーターを公開しました

はじめに

このたび、Unity でサービスロケーターを使用できるようにするパッケージを公開いたしました! 🚀

github.com

今回はこのパッケージの紹介を、サービスロケーター自体の紹介も合わせつつ、させていただければと思います。

概要

【サービスロケーター】とは、

  • どこからでもアクセスできる格納場所(サービスコンテナ)を一つだけ用意し、
  • その格納場所に任意のクラス(サービス)を登録することで、
  • 他のクラスから格納場所に登録されたクラスへどこからでもアクセスできるようにする

というデザインパターンです。

例えば、ゲームデータのセーブ・ロードを司るクラスの SaveLoadService があったとして、それをあらかじめサービスコンテナに登録しておけば、他の様々なクラスからサービスコンテナを介し SaveLoadService へアクセスしてセーブやロードが行えるようになります。

これと似たようなことを実現できるものとして、【シングルトン】【DI (Dependency Injection)】パターンというのもありますが、

  • シングルトンだと SaveLoadService などの各サービスに他の各クラスが直接アクセスする形になり、サービスの数が増えていくと依存関係がスパゲッティ化してしまう
  • DI はサービスロケーターよりも依存関係を整理できるが、仕組みがやや難解で学習コストが高い

といった特徴があり、サービスロケーターはその中間的な立ち位置で使いやすさと実用性を兼ね揃えたデザインパターンと言えます。

インストール

それでは、パッケージのインストール方法から説明いたします。

Package Manager を使用する方法

導入したい Unity プロジェクトで Package Manager を開き、「+」ボタンを押して「Add package from git URL...」を選択、以下の URL を入力して「Add」ボタンを押すとインストールできます。

https://github.com/gigacreation/ServiceLocatorForUnity.git?path=Assets/Service

手動でインストールする方法

GitHub のリポジトリからソースコードをダウンロードし、その中の Assets/Service/ ディレクトリを導入したい Unity プロジェクトにコピーします。

使い方

基本の使い方

1. サービスコンテナへ登録したいクラスに IService を実装します。

using GigaCreation.Tools.Service;
using UnityEngine;

public class SampleService : IService
{
    public void Bark()
    {
        Debug.Log("Bowwow!");
    }
}

2. 次に、そのクラスをサービスコンテナに登録します。*1 *2

using GigaCreation.Tools.Service;
using UnityEngine;

public class RegisterServiceSample : MonoBehaviour
{
    private SampleService _sampleService;

    private void Awake()
    {
        // サービスを生成し……
        _sampleService = new SampleService();

        // サービスコンテナに登録する
        ServiceLocator.Register(_sampleService);
    }

    private void OnDestroy()
    {
        // GameObject が破棄されたら、サービスコンテナへの登録を解除する
        ServiceLocator.Unregister(_sampleService);
    }
}

3. これで、どこからでもアクセスできるようになりました!

using GigaCreation.Tools.Service;
using UnityEngine;

public class UseServiceSample : MonoBehaviour
{
    private void Start()
    {
        // 型を指定してサービスを取得できる
        var sampleService = ServiceLocator.Get<SampleService>();

        sampleService.Bark();
    }
}

IDisposable とセットで使う

サービスコンテナに登録されたクラスが IDisposable を実装していた場合、登録解除時に Dispose() が呼ばれます。

using System;
using GigaCreation.Tools.Service;
using UnityEngine;

public class SampleService : IService, IDisposable
{
    public void Bark()
    {
        Debug.Log("Bowwow!");
    }

    public void Dispose()
    {
        // 登録解除時に呼ばれる
        Debug.Log("SampleService disposed.");
    }
}

自作のインターフェイスとセットで使う

クラスをサービスコンテナへ登録する際、クラスを直接登録するのではなく、インターフェイスを介して登録することもできます。これにより、他から呼ばれるメソッドを限定することが可能です。

using GigaCreation.Tools.Service;
using UnityEngine;

public class SampleService : ISampleService
{
    public void Bark()
    {
        Debug.Log("Meow!");
    }

    public void Scratch()
    {
        // このメソッドは public であるが、ISampleService を介してアクセスすることはできない
        Debug.Log("Ouch!");
    }
}

public interface ISampleService : IService
{
    void Bark();
}

こんな形でサービスに自作のインターフェイスを実装し、

// 登録
// 左辺は ISampleService であることに注意
ISampleService sampleService = new SampleService();
ServiceLocator.Register(sampleService);

// アクセス
// ISampleService を指定して取得する
var sampleService = ServiceLocator.Get<ISampleService>();
sampleService.Bark();
sampleService.Scratch(); // <- これはエラー!

という感じでインターフェイスを介して登録やアクセスができます。*3

インターフェイスをさらに細分化して、各クラスで使用できるメソッドを個別に限定することなども可能です(SaveLoadService で、あるクラスはセーブだけ、あるクラスはロードだけできるようにする、みたいな)。

API リファレンス

一応リファレンスも用意していて、英語にはなりますが こちら で読むことができます。

補遺

シングルトンとの使い分け

「あるサービスへどこからでもアクセスしたい」という用途に関して言えば、シングルトンとサービスロケーターは使用感が結構似ていて、特にサービスの数が 1 つだけの場合、サービスコンテナを介すか介さないかの違いくらいしかありません。

サービスが 1 つの場合
あまりサービスロケーターのメリットが無さそうに見える

ごく小規模なミニゲームを作るときなどで、サービスの数が 1 つ程度で済むことが確定している場合は、シングルトンを使ってもそれほど大きな問題にはならないかと思います。

ただまぁ、ゲーム開発は得てして「あれも追加しよう、これも追加しよう」となりがちなので、後々の拡張性を考えれば、最初からサービスロケーターを使うのをオススメしたいですね。サービスの数が増えていけばいくほど、サービスロケーターで依存関係を整理できるのが活きてくるでしょう。

サービスが 3 つの場合
サービスロケーターならスッキリ!

先述の「自作のインターフェイスとセットで使う」をやれば、例えば同じインターフェイスを実装したデバッグ用のサービスを別で作り、デバッグモードの時はそちらを登録するようにすれば、使用者側のほうは何もせずともデバッグ用のコードを実行できる、といったことも実現可能です。

登録や破棄のタイミングも、サービスロケーターなら自由に変更できますしね(シングルトンだと基底クラスを変更したりしないといけないのでちょっと面倒)。

サービスロケーターはアンチパターンなのか?

DI パターンを説明する文脈では、サービスロケーターはアンチパターンとして紹介されることが多いようです。

実際これは一理あって、サービスロケーターは依存関係をスッキリさせられるとはいえ、結局サービスコンテナというグローバルな存在に依存することになりますし、コンテナにアクセスできるクラスは自動的に、コンテナに登録されているすべてのサービスにアクセスできてしまいます。例えば前項の図だと、Class B は SaveLoadService にだけアクセスできれば良いのに、すべてのサービスへのアクセス権を得られてしまう……ちょっと微妙ですよね。

ですので、必要なクラスに必要な分だけ依存性を注入する DI パターンのほうが、よりスマートだと言えそうです。Unity 開発であれば、 VContainer という素晴らしい DI フレームワークが存在していますので、もし DI を使いたいという場合はぜひ一度目を通すことをオススメします。DI とは何か? というところから、日本語で詳細に説明してくれています。

一方、DI の難点としては、やはり学習コストが高いことが挙げられると思います。VContainer のマニュアルを見ていただければ分かりますが、結構覚えないといけないことが多く、サービスロケーターやシングルトンに比べて導入はかなり難しめです。

サービスロケーターはグローバルなコンテナに依存するという点がマイナスポイントですが、各サービスはローカルな存在のまま保たれるので、十分実用に耐えられるデザインパターンだと個人的には考えています。シングルトンだとサービス自体がグローバルな存在になってしまうので、個人的にはデメリットのほうが大きいかなという印象です。

まとめると、サービスロケーターは手軽さと実用性を兼ね揃えた良いデザインパターンですので、シングルトンを使うくらいならぜひこちらを使っていってほしいと思います。より堅牢な設計を求めるのであれば、DI の導入を検討したい、といった感じですね。

*1:ここでは MonoBehaviour の Awake() で登録していますが、別に他のタイミングで登録しても良いですし、MonoBehaviour ではない Pure C# クラスで登録してももちろん大丈夫です

*2:OnDestroy() で破棄することで、そのシーン内でのみサービスが有効になります。これを無くすとシーンを切り替えたりしてもサービスが生き続けるので、シーン間のデータのやり取りが可能になります

*3: (sampleService as SampleService).Scratch() って書いたら無理やりクラスへアクセスできたりもしますが……

The coloring of this site is Dracula PRO🧛🏻‍♂️
This website uses the FontAwesome icons licensed under CC BY 4.0.

2020 GIGA CREATION