【Unity】ServiceLocatorを使ってサービスクラスをまとめる

Unity
マイケル
マイケル
みなさんこんにちは!
マイケルです!
エレキベア
エレキベア
こんにちクマ〜〜〜
マイケル
マイケル
今日は サービスロケータ(Service Locator)
というデザインパターンを紹介するぜ!
エレキベア
エレキベア
なんだそれはクマ
マイケル
マイケル
簡単に言うと クラス全般から呼ばれるようなサービスクラスを一箇所に集めて
依存関係をまとめることのできるパターン
のことだね・・・。
エレキベア
エレキベア
よくわからんクマ・・・
マイケル
マイケル
便利で手軽に使えるパターンだから、Unityで使って覚えてみよう!
ただ、今はDIコンテナを使う方が推奨されることが多いようだから、その辺も踏まえて解説するね!
エレキベア
エレキベア
そこまで便利なら聞いてやるクマ
スポンサーリンク

参考書籍

マイケル
マイケル
参考書籍は以下になります!
こちらはゲーム開発に特化したデザインパターンを扱った書籍で、WEBでも公開されているから一読の価値ありです!

Game Programming Patterns ソフトウェア開発の問題解決メニュー impress top gearシリーズ


WEB版:

Game Programming Patterns

マイケル
マイケル
ちなみにデザインパターン全般については下記記事でも
まとめてあるのでこちらもよければ見てみてください!
エレキベア
エレキベア
懐かしいクマ〜〜〜

サービスロケータとは

サービスロケータの概要

マイケル
マイケル
サービスロケータはさっきも言った通り、
クラス全般から呼ばれるようなサービスクラスを一箇所に集めて
依存関係をまとめることのできるパターン
のことだ!
マイケル
マイケル
例えば、いろんなクラスから呼び出されるクラスがあったとして、
これを直接呼び出した場合には下記のように複雑な依存関係になってしまいます。
UntitledImage
↑直接呼び出した例
マイケル
マイケル
ここでServiceLocatorクラスに各サービスクラスを登録して
間に挟むことで、依存関係をまとめることができるというわけです!
UntitledImage
↑Service Locatorを挟んだ例
エレキベア
エレキベア
おお〜〜
確かにこれなら綺麗クマ〜〜〜〜
マイケル
マイケル
更に 各サービスクラスのインタフェースを登録して
呼び出し元からはインタフェースを指定
するようにすれば、
サービスクラスの差し替えも簡単に行えるようになります!
UntitledImage
↑各サービスクラスのインタフェースを登録する
エレキベア
エレキベア
差し替えることができると何うれしいクマ?
マイケル
マイケル
使うサービスを変更したくなった時に応用が効くのはもちろん、
Dummyクラスに差し替えることでテストができるようにしたり、
開発時にだけログ出力するクラスに差し替える
といったことができるようになるよ!
UntitledImage
↑サービスの差し替えが可能
エレキベア
エレキベア
なるほどクマ
それなら柔軟性があっていいクマね

DIコンテナとどちらを使用すべきか?

マイケル
マイケル
他に依存性を管理する手法としてDIコンテナが有名です。
ServiceLocatorとDIコンテナどちらを使用するべきか?については下記の記事が分かりやすくまとまっていました!

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

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

Unityでの実装

マイケル
マイケル
概要をざっと説明したところで、
実際にコードを書いてみましょう!
ServiceLocatorクラスの例としては下記のようになります!
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));
        }
    }
}
↑ServiceLocatorクラス
エレキベア
エレキベア
思ったよりシンプルクマね
マイケル
マイケル
型をkeyとしてDictionaryに登録することで、
呼び出し側からは型を指定して呼び出すことができるようになっているよ。
マイケル
マイケル
例として、下記のPlayerPrefsServiceクラスを登録してみましょう!
using UnityEngine;

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

        public int GetInt(string key)
        {
            return PlayerPrefs.GetInt(key);
        }
    }
}
↑PlayerPrefsServiceクラス

namespace Services
{
    /// <summary>
    /// PlayerPrefsService
    /// </summary>
    public interface IPlayerPrefsService
    {
        public void SetInt(string key, int value);
        public int GetInt(string key);
    }
}
↑PlayerPrefsServiceインタフェース
マイケル
マイケル
このクラスをプロジェクト初期化等のタイミングで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);
        }
    }
}
↑サービスクラスの呼び出し例
エレキベア
エレキベア
呼び出し側も簡単に呼び出せるクマね
マイケル
マイケル
ちなみにMonoBehaviorクラスを継承したサービスクラスを登録したい場合には、
下記のようにAddComponentした戻り値を登録するようにすればOKです!
using Services;
using UnityEngine;

/// <summary>
/// プロジェクト初期化クラス
/// </summary>
public static class ProjectInitializer
{
    /// <summary>
    /// 初期化処理
    /// シーンのロード前に呼ばれる
    /// </summary>
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    private static void Initialize()
    {
        // サービス登録
        var serviceLocator = new GameObject("ServiceLocator");
        ServiceLocator.Register<IXXXService>(serviceLocator.AddComponent<XXXService>());
    }
}
↑MonoBehaviorクラスを登録した例
マイケル
マイケル
とはいえ共通となるサービスクラスにMonoBehaviorクラスを継承させるのは
極力避けた方がよさそうですね・・・。
エレキベア
エレキベア
やむを得ない時もあるのだろうかクマ・・・。

おわりに

マイケル
マイケル
というわけで今日はサービスロケータの実装について紹介しました!
どうだったかな??
エレキベア
エレキベア
想像していたよりもシンプルで使いやすそうだったクマ
ただ慣れてきたらDIコンテナも使ってみたいクマね
マイケル
マイケル
シンプルで手軽に使えるサービスロケータ!
呼び出し処理が散らばっている時、テスト処理が書けない時には使ってみてはいかがでしょうか?
マイケル
マイケル
それでは今日はこの辺で!
アデュー!!!
エレキベア
エレキベア
クマ〜〜〜〜〜

コメント