はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【Unity】サービスロケーターを使ってみる

はじめに

今回はServiceLocatorパターンというよくDependency Injectionパターンの前座として紹介されるデザインパターンを取り上げたいと思います。

f:id:hanaaaaaachiru:20201107004804p:plain

実装

サービスロケーターの実装の仕方は無数にありますが、私が好みかつシンプルなのはこんな感じ。

using System;
using System.Collections.Generic;

public static class ServiceLocator
{
    private static readonly Dictionary<Type, object> _container;

    static ServiceLocator()
        => _container = new Dictionary<Type, object>();

    /// <summary>
    /// インスタンスを取得する
    /// </summary>
    public static T Resolve<T>()
        => (T)_container[typeof(T)];

    /// <summary>
    /// インスタンスを登録する
    /// </summary>
    public static void Register<T>(T instance)
        => _container[typeof(T)] = instance;

    /// <summary>
    /// インスタンスを登録解除する
    /// </summary>
    public static void UnRegister<T>(T instance)
    {
        if (Equals(_container[typeof(T)], instance)) _container.Remove(typeof(T));
    }

    /// <summary>
    /// 登録しているインスタンスを削除する
    /// </summary>
    public static void Clear<T>()
        => _container.Remove(typeof(T));
}


またスレッドセーフにしたいなら以下のように変更することも考えられるでしょう。

private static readonly ConcurrentDictionary<Type, object> _container;

static ServiceLocator()
    => _container = new ConcurrentDictionary<Type, object>();

使い方

インスタンスを登録するのはこんな感じ。

public static class Initializer
{
    /// <summary>
    /// Awakeより前に呼ばれる
    /// </summary>
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    private static void Init()
    {
        ServiceLocator.Register<ISomeSystem>(new SomeSystem());
    }
}

public interface ISomeSystem
{
    void Execute();
}

public class SomeSystem : ISomeSystem
{
    public void Execute()
    {
        Debug.Log("実行されたよ");
    }
}

基本的にInterfaceもしくはabstract classを通してインスタンスを登録してください。(疎結合,テストを行うため)

次に利用する側はこちら。

public class Client
{
    public void Execute()
    {
        var system = ServiceLocator.Resolve<ISomeSystem>();

        system.Execute();
    }
}

一応言っておくとSomeSystemClientExecuteという名前に特に意味はないです。

Client -> ISomeSystemというclientISomeSystemを利用している意味合いを込めてます。

テストをやる

モックを作成してテストを行ってみましょう。

public class SomeSystemMock : ISomeSystem
{
    public void Execute()
    {
        Debug.Log("これはテストだよ");
    }
}


UnityにはTest Runnnerというテストをする機能がありますので、そちらを利用してみます。

using NUnit.Framework;

namespace Tests
{
    public class ClientTest
    {
        [OneTimeSetUp]
        public void OneTimeSetup()
        {
            ServiceLocator.Register<ISomeSystem>(new SomeSystemMock());
        }

        [Test]
        public void ClientMockTest()
        {
            var client = new Client();

            client.Execute();

            Assert.Pass();
        }
    }
}

サービスロケーターの良し悪し

サービスロケーターはアンチパターン(良くない実装)だとよく言われていて、Dependency Injectionパターンを使えと決まって言われます。

詳しいことはこちらの記事がとても参考になるはずです。
www.nuits.jp


ただサービスロケーターには

  • 理解しやすい
  • パフォーマンスが良い

という利点もあり、そこまで大規模なプロジェクトでなければ十分活用できるデザインパターンだと個人的には思っています。

さいごに

ServiceLocatorにインスタンスを登録した後、使わなくなったらちゃんと解除する癖はつけておきましょう。

参照がどこかしらに残っているとガベージコレクションが働かなかったはずなので、メモリは大切に。

ではまた。