Service Locator

フレームワークやゲームのあらゆるサービスを取得するために呼び出す、唯一の static クラス。

あらゆるサービスを取得するために呼び出す唯一のクラス · フレームワークとゲームの全サービスのグローバルレジストリ · 常に初期化済み — 差し替え不要

概要

Service Locator は、「X サービスはどこにある?」というただ 1 つの問いに答える小さな static レジストリです。Bootstrap が起動時に、23 のフレームワークサービスをそれぞれここに登録します。あなたのコードはどこからでも ServiceLocator.Resolve<IMyService>() を呼び出してインスタンスを受け取れます — シーン参照も、シングルトンも、コンパイル時の配線も不要です。実装を差し替えたい場合(たとえばデフォルトの代わりに自作のセーブシステムを使う)は、Replace<T>() を一度呼ぶだけで、他には何も変更する必要がありません。

簡単な使用例

using CommonGameSystem.Core;
using UnityEngine;

[DefaultExecutionOrder(100)]  // Bootstrap registers services first — class-level attribute
public class GameController : MonoBehaviour
{
    [SerializeField] private AudioClip _clickSound;

    // Cache in a field; never call Resolve in Update().
    private IAudioService _audio;
    private ISaveService _save;

    void Awake()
    {
        // Bootstrap has already called ServiceLocator.Register<T>() for each service.
        _audio = ServiceLocator.Resolve<IAudioService>();
        _save = ServiceLocator.Resolve<ISaveService>();
    }

    public void OnPlayButtonPressed()
    {
        _audio.PlaySfx(_clickSound);
    }
}

実際のセーブシステムなしで動かしたい場合 — テストのため、あるいは機能をオフにするため — は、組み込みの no-op 実装に差し替えます:

// In a test setup, or anywhere before the code under test resolves it:
ServiceLocator.Replace<ISaveService>(new NullSaveService());

// New Resolve calls now return the replacement.
// Code that cached the old instance earlier keeps using that old instance —
// the locator never updates references you already hold.

API の全体像

すべてのメソッドは ServiceLocator クラスの static メソッドです。そしてすべてメインスレッド専用です(挙動とエッジケースを参照)。

コア操作

  • void Register<T>(T instance) — キー T の下に新しいサービスを追加します。T に対してすでにサービスが登録されている場合は ServiceAlreadyRegisteredException を、T が具象クラスの場合は ArgumentException をスローします(キーはインターフェースまたは抽象クラスでなければなりません)。自作のゲームサービスにはこれを使ってください。フレームワーク自身のサービスは Bootstrap が登録します。

  • T Resolve<T>() — 登録済みのサービスを取得します。T に何も登録されていなければ ServiceNotRegisteredException をスローします。これは呼び出しのたびに辞書検索が走ります — 毎フレーム呼ばないでくださいAwake/Start で一度解決し、結果をフィールドにキャッシュしてください。

  • void Replace<T>(T instance) — 登録を上書き、または新規作成します(既存の登録があってもエラーになりません)。押し出した側のインスタンスは破棄しません — あなたがまだ参照を保持しているかどうかを知りようがないからです。退場するサービスが内部 GameObject を所有している場合(Time、Object Pool、Audio、UI Framework、Scheduler、Tween、Tween Sequencing はそれぞれ [CGS] … GameObject を作成します)、自分で破棄しないと、その GameObject はセッションの残りの間ずっと生き続けて tick し続けます:

    var previous = ServiceLocator.Resolve<ITimeService>() as System.IDisposable;
    ServiceLocator.Replace<ITimeService>(new NullTimeService());
    previous?.Dispose();   // after the swap, so nothing resolves a disposed instance
    
  • bool TryResolve<T>(out T service) — ソフトな検索。何も登録されていなければ false を返し、servicenull を設定します。例外はスローしません。オプショナルなサービスに使ってください。

  • bool IsRegistered<T>() — インスタンスを取得せずに存在を確認します。

  • void Unregister<T>() — 登録を削除します。何も登録されていなくても安全に呼び出せます(2 回呼んでも問題ありません)。

デバッグ・テスト用ヘルパー

  • void Reset() — すべての登録をクリアします。エディタと Development ビルドでのみ利用可能で、Release ビルドではコンパイル時に除去されるため、プロダクションのコードパスからは呼び出せません。
  • IReadOnlyDictionary<Type, object> GetRegistrySnapshot() — 登録済み全サービスの読み取り専用コピー。エディタ専用。
  • int RegisteredServiceCount — 登録エントリのライブカウント。エディタ専用。

例外

  • ServiceLocatorException — すべての Service Locator エラーの基底型。これを catch すれば、どのエラーも処理できます。
  • ServiceAlreadyRegisteredException — キーが既存のため Register が失敗。.ServiceType プロパティを公開します。
  • ServiceNotRegisteredExceptionResolve が失敗。.ServiceType と、最も一般的な原因を列挙したメッセージを公開します。

未登録のサービスとオプショナルな検索

Service Locator 自体には no-op の差し替えはありません — レジストリは、Play 開始のたびに、そしてエディタのアセンブリリロード後に、必ず初期化されます。サービスが見つからない場合、Resolve<T>() は即座に ServiceNotRegisteredException をスローします。これは意図的な設計です: サービスを必要としたまさにその行で気づけるのであって、後から静かに壊れるのではありません。

オプショナルなサービスを穏やかに扱いたい場合は、代わりに TryResolve<T> を使ってください:

if (ServiceLocator.TryResolve<IAudioService>(out var audio))
    Debug.Log("Audio service is available");
else
    Debug.LogWarning("Audio service not registered");

挙動とエッジケース

  • 解決したものはキャッシュする。 Resolve<T>() は毎回辞書検索です。Update() で呼ぶと毎フレームその時間を浪費します。AwakeStart で一度解決し、参照をフィールドに保持してください。

  • メインスレッド専用。 すべての public メソッドは、Debug ビルドで Unity のメインスレッド上での実行をアサートします。Task.Run、スレッドプール、その他のバックグラウンドスレッドから呼ぶと「[ServiceLocator] Main thread only.」で失敗します。バックグラウンドスレッドがサービスを必要とする場合は、先にメインスレッドで解決して参照を渡してください。

  • キーはインターフェースまたは抽象クラスのみ。 Register<ConcreteClass>(impl)ArgumentException をスローします。T には常にインターフェースか抽象クラスを使ってください。これにより、呼び出し側に知られることなく、あらゆるサービスを差し替え可能に保てます。

  • インターフェースの継承はたどりません。 仮に ISaveService が何らかの IPersistenceService を継承していて、ISaveService しか登録していなければ、Resolve<IPersistenceService>() はスローします。レジストリは正確な型の辞書です。同じインスタンスを両方のキーで登録するか、登録した正確なキーで解決してください。

  • Replace は、すでに保持している参照をリターゲットしません。 Replace<T>() の後、新しい Resolve<T>() 呼び出しは新しいインスタンスを返しますが、古いインスタンスをキャッシュしていたコードはそれを使い続けます。実装の差し替えは、消費側が解決するに行ってください — 典型的には、より早く実行される Awake の中か、テストのセットアップです。

  • 早すぎる解決。 Bootstrap は最初のシーンのロード前にすべてのフレームワークサービスを登録するため、フレームワークサービスはシーンコードから常に利用可能です。しかし、あなた自身のコードが後から追加のサービスを登録する場合 — たとえば MonoBehaviour の Awake から — その時点より前に実行されるコードは、まだそれらを解決できません。ServiceNotRegisteredException のメッセージが、よくある順序起因の原因を案内します。

  • Domain Reload と IL2CPP。 レジストリは、エディタで Domain Reload が有効かどうかにかかわらず、Play 開始のたびにワイプされ再初期化されます。IL2CPP(Unity の AOT コンパイルバックエンド)でビルドする場合、サービス呼び出しに渡すカスタムデータ型は、ビルドのコードストリッピングに除去されないよう、あなた自身のプロジェクトに link.xml エントリが必要です。フレームワーク自身の型はすでに保護されています。

関連ページ

  • Bootstrap — 起動順序と 23 サービスの全リスト
  • Logger — 最初に登録されるサービスと、セッション途中の差し替えに関する 1 つの注意点
  • Save/Load — 典型的な「解決してキャッシュする」消費者と、その NullSaveService オフスイッチ
  • はじめに — 最初の「解決してキャッシュする」スクリプト
  • マニュアル: トラブルシューティングServiceNotRegisteredException の診断