Unity_Lesson

サービスロケータについて

サービスロケーターパターン(Service Locator Pattern)は、クラスが必要とするサービス(インターフェースの実装)を取得するためのデザインパターンです。
サービスロケータは、アプリケーション全体で使用される依存関係を集中管理し、クライアントコードが具体的なサービスのインスタンスを直接生成することを防ぎます。


サービスロケーターパターンの構造


サービスロケータの概要

例えば、いろんなクラスから呼び出されるクラスがあったとして、 これを直接呼び出した場合には下記のように複雑な依存関係になってしまいます。(Opsを直接呼び出している例)


そこで、ServiceLocatorクラスに各サービスクラスを登録。間を挟むことで、依存関係をまとめる



各サービスクラスのインタフェースを登録して、呼び出し元からはインタフェースを指定するようにすれば、サービスクラスの差し替えも簡単に行えるようになります


差し替えることができると、 使うサービスを変更したくなった時に応用が効くのはもちろん、 Dummyクラスに差し替えることでテストができるようになったり、 開発時にだけログ出力するクラスに差し替える といったことができるようになります。


ServiceLocator以外について

他に依存性を管理する手法としてDIコンテナが有名です。 ServiceLocatorとDIコンテナ、どちらを使用するべきか?についてはネット上の記事等を参考にしてみてください。

Service Locator と Dependency Injectionパターン と DI Container

本来不要なServiceLocatorクラスへの依存が発生してしまうこと、呼び出し側のコードが少し複雑になることなどから、DIコンテナの使用が推奨されることがあります。
ただ、UnityでDIコンテナを利用するには、ZenjectやVContainerといったDIライブラリを使用することが多く、学習コストと導入コストもそれなりに高いです。
そのため、個人レベルや規模が小さい場合にはServiceLocatorを使用し、慣れてきたら上記のようなDIライブラリを使うことに挑戦するというのも一つの手かと思います。


利点


欠点


サービスロケーターパターンの使用例1


型をkeyとしてDictionaryに登録することで、 呼び出し側からは型を指定して呼び出すことができるようになる。

using System;
using System.Collections.Generic;

namespace Services
{
    /// <summary> サービスロケータ </summary>
    public static class ServiceLocator
    {
        /// <summary> コンテナ </summary>
        private static readonly Dictionary<Type, object> Container;

        /// <summary> コンストラクタ </summary>
        static ServiceLocator()
        {
            Container = new Dictionary<Type, object>();
        }

        /// <summary> サービス取得 </summary>
        public static T Resolve<T>()
        {
            return (T) Container[typeof(T)];
        }

        /// <summary> サービス登録 </summary>
        public static void Register<T>(T instance)
        {
            Container[typeof(T)] = instance;
        }

        /// <summary> サービス登録解除 </summary>
        public static void UnRegister<T>()
        {
            Container.Remove(typeof(T));
        }
    }
}


例として、 PlayerPrefsServiceクラスを登録してみる

namespace Services
{
    public interface IPlayerPrefsService
    {
        public void SetInt(string key, int value);
        public int GetInt(string key);
    }
}
using UnityEngine;

namespace Services
{
    public class PlayerPrefsService : IPlayerPrefsService
    {
        public void SetInt(string key, int value)
        {
            PlayerPrefs.SetInt(key, value);
        }

        public int GetInt(string key)
        {
            return PlayerPrefs.GetInt(key);
        }
    }
}


プロジェクト初期化時、ServiceLocatorに登録

using Services;
using UnityEngine;

/// <summary>プロジェクト初期化クラス </summary>
public static class ProjectInitializer
{
    /// <summary> 初期化処理(シーンのロード前に呼ばれる)</summary>
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    private static void Initialize()
    {
        // サービス登録
        ServiceLocator.Register<IPlayerPrefsService>(new PlayerPrefsService());
    }
}


サービスクラスの呼び出し例(あとは各クラスでServiceLocator.Resolveを呼び出すことで、 各サービスを使用することができます)


using Services;
using UnityEngine;
using Utils;

namespace Scenes.Common
{
    /// <summary> PlayerPrefs管理クラス </summary>
    public static class SamplePlayerPrefs
    {
        /// <summary> スコア </summary>
        public static int Score
        {
            get => GetPlayerPrefsIntValue(KeyScore);
            set => SetPlayerPrefsIntValue(KeyScore, value);
        }
        private const string KeyScore = "Score";
        
        private static void SetPlayerPrefsIntValue(string key, int value)
        {
            ServiceLocator.Resolve<IPlayerPrefsService>().SetInt(key, value);
        }

        private static int GetPlayerPrefsIntValue(string key)
        {
            return ServiceLocator.Resolve<IPlayerPrefsService>().GetInt(key);
        }
    }
}

↓だけ適当なGameObjectにアタッチしてみて(他はフォルダにあれさえすればいい)


using Scenes.Common;
using UnityEngine;

public class ExampleUsage : MonoBehaviour
{
    void Start()
    {
        // スコアの設定
        SamplePlayerPrefs.Score = 42;

        // スコアの取得
        int score = SamplePlayerPrefs.Score;
        Debug.Log($"Current Score: {score}");
    }
}




サービスロケーターパターンの使用例2

サービスロケーターパターンのシンプルな例。


サービスインターフェース。 指定された soundName を再生するためのメソッドです。

public interface IAudioService {
    void PlaySound(string soundName);
}


具体的なサービスクラス

using UnityEngine;

public class AudioService : IAudioService {
    public void PlaySound(string soundName) {
        Debug.Log("Playing sound: " + soundName);
        // 実際のサウンド再生ロジックを書く

    }
}


サービスロケーター。静的クラスとして定義され、サービスの登録と取得を行います。

using System;
using System.Collections.Generic;

public static class ServiceLocator {
    private static Dictionary<Type, object> services = new Dictionary<Type, object>();

    public static void RegisterService<T>(T service) {
        var type = typeof(T);
        if (!services.ContainsKey(type)) {
            services[type] = service;
        }
    }

    public static T GetService<T>() {
        var type = typeof(T);
        if (services.ContainsKey(type)) {
            return (T)services[type];
        }
        throw new Exception("Service not found: " + type);
    }
}

services は Type をキーとし、サービスのインスタンスを値とする辞書。
RegisterService<T> メソッドは、サービスインスタンスがすでに登録されていない場合のみ登録します。 GetService<T> メソッドは、登録されたサービスインスタンスを取得します。サービスが見つからない場合は例外をスローします。


サービスロケーターの使用例(MonoBehaviour を継承し、Unity のゲームオブジェクトとして機能します。)
下のスクリプトだけ適当なGameObjectにアタッチしてください。

using UnityEngine;

public class GameManager : MonoBehaviour {
    void Start() {
        // サービスの登録
        ServiceLocator.RegisterService<IAudioService>(new AudioService());

        // サービスの取得と使用
        var audioService = ServiceLocator.GetService<IAudioService>();
        audioService.PlaySound("BackgroundMusic");
    }
}

Start メソッド内で、AudioService のインスタンスを ServiceLocator に登録します。 登録後、ServiceLocator.GetService() を使用して IAudioService のインスタンスを取得し、PlaySound メソッドを呼び出してサウンドを再生します