Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9374

Unityでサービスロケーター(ServiceLocator)を活用する

$
0
0

はじめに

サムザップ #1 Advent Calendar 2019の12/2の記事です。

株式会社サムザップの尾崎です。Unityエンジニアです。

内容

Unityでサービスロケーターの活用について紹介します。

サービスロケーターとは

サービスロケーターはプログラムを特定の実装に依存させずに動作させたいときに用いる実装手法の一つです。
柔軟性のあるプログラムを作成できます。

用語

  • 本記事ではサービスをシステムと表現しています。
  • システムはいろんなクラスから呼び出される共通的なプログラムのことを表しています。サブシステムや基盤と呼ばれることもあります。 例えばゲームでは外部リソースやログを扱うクラスなどが該当します。

背景

newでオブジェクトを生成したり、staticやシングルトンでアクセスすると特定のクラスに依存することになります。
その依存したクラス内で外部環境を扱う処理を行っているとプログラムの動作確認が大変になってくることがあります。

例えば1日に1回だけ挑戦できるステージがあり、セーブデータシステムにステージクリアを記録すると翌日まで再挑戦できない状況があるとします。
開発中はこうした状況でも何度でもステージに挑戦できるようにしておきたいものです。

セーブデータクラスのメソッド内でデバッグ用の分岐を書くこともできますが、クラスが大きくなると分岐が増えて分かりにくいコードになりやすいです。

そこでセーブデータシステムのためのインターフェース定義してクラスを複数作成します。
正規の処理を行うクラスとデバッグ用の処理を行うクラスです。
サービスロケーターはこのクラスへのアクセス方法を提供します。

(セーブデータをクリアしたり日付を進めるなどデバッグ方法はいくつかありますがここでは実装を複数用意するという方向で。)

そしてもう一つ、共通システムはいろんなクラスから簡単に呼び出せるようにしておきたいものです。
staticやシングルトンが使われることが多いですが、便利な反面1つのクラスに依存してしまうのと、グローバル変数と同じ問題があるので、できるだけ使用しないようにしています。ユニットテストの妨げにもなってしまいます。

サービスロケーターの詳細

機能

  • システムのインターフェースに対するインスタンスを登録できます
  • システムのインターフェースを指定してインスタンスを取得できます
  • グローバルなアクセスを提供します

※ インターフェース以外にクラスでも大丈夫です

これらの機能によってシステム利用側のコードからシステムの具体的な実装とインスタンスの生成方法を分離します。

メリット

  • 複数の実装を切り替えることができ柔軟性のあるプログラムになります
  • 複数実装をのための分岐が初期化時の一箇所で済みます
  • インスタンスへの容易なアクセス
  • DIコンテナよりシンプルで高速
  • モック実装に切り替えることでユニットテストできるようになります

デメリット

  • ServiceLocatorクラスへの依存が増えます
  • DIコンテナよりクラスの依存関係が分かりにくくなります。一般的に柔軟性を得るための方法としてはDIコンテナの方が推奨されています。
  • シングルトンと同じようにインスタンスを単一にするためには別の対策が必要です

コード例

サービスロケーター本体

usingSystem;usingSystem.Collections.Generic;namespaceSumzap{/// <summary>/// サービスロケーター/// </summary>publicstaticclassLocator{/// <summary>/// 単一インスタンス用ディクショナリー/// </summary>privatestaticDictionary<Type,object>_instanceDict=newDictionary<Type,object>();/// <summary>/// 都度インスタンス生成用ディクショナリー/// </summary>privatestaticDictionary<Type,Type>_typeDict=newDictionary<Type,Type>();/// <summary>/// 単一インスタンスを登録する/// 呼び直すと上書き登録する/// </summary>/// <typeparam name="T">型</typeparam>/// <param name="instance">インスタンス</param>publicstaticvoidRegister<T>(objectinstance)whereT:class{_instanceDict[typeof(T)]=instance;}/// <summary>/// 型を登録する/// このメソッドで登録するとResolveしたときに都度インスタンス生成する/// </summary>/// <typeparam name="TContract">抽象型</typeparam>/// <typeparam name="TConcrete">具現型</typeparam>publicstaticvoidRegister<TContract,TConcrete>()whereTContract:class{_typeDict[typeof(TContract)]=typeof(TConcrete);}/// <summary>/// 型を指定して登録されているインスタンスを取得する/// </summary>/// <typeparam name="T">型</typeparam>/// <returns>インスタンス</returns>publicstaticTResolve<T>()whereT:class{Tinstance=default;Typetype=typeof(T);if(_instanceDict.ContainsKey(type)){// 事前に生成された単一インスタンスを返すinstance=_instanceDict[type]asT;returninstance;}if(_typeDict.ContainsKey(type)){// インスタンスを生成して返すinstance=Activator.CreateInstance(_typeDict[type])asT;returninstance;}if(instance==null){Debug.LogWarning($"Locator: {typeof(T).Name} not found.");}returninstance;}}}

システム (サービス)

usingUnityEngine;namespaceSumzap{/// <summary>/// システムのインターフェース/// </summary>publicinterfaceISomeSystem{voidSomeMethod();}/// <summary>/// 正式版のシステム/// </summary>publicclassSomeSystem:ISomeSystem{publicvoidSomeMethod(){// 正式な処理}}/// <summary>/// デバッグ版のシステム/// </summary>publicclassDebugSomeSystem:ISomeSystem{publicvoidSomeMethod(){// デバッグ用の処理}}}

システムをサービスロケーターに登録 (プロジェクトの初期化)

#define DEBUG
usingUnityEngine;namespaceSumzap{/// <summary>/// プロジェクトの初期化/// </summary>publicstaticclassProjectInitializer{[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]privatestaticvoidInitialize(){// この変数を切り替えることで生成するインスタンス切り替えます// 単純化のためクラス内の#defineで定義しています// 実際にはScripting Define Symbolsや設定ファイルを読み込んだりして切り替えますbooluseDebugSystem;#ifDEBUGuseDebugSystem=false;#endifif(useDebugSystem){// 正式な処理を行うインスタンスを登録Locator.Register<ISomeSystem>(newSomeSystem());}else{// デバッグ用処理を行うインスタンスを登録Locator.Register<ISomeSystem>(newDebugSomeSystem());}}}}

システムを利用

usingUnityEngine;namespaceSumzap{publicclassSomeScene:MonoBehaviour{publicvoidStart(){// システムの型を指定して登録されているインスタンスを取得varsystem=Locator.Resolve<ISomeSystem>();system.SomeMethod();// newの場合varsystem2=newSomeSystem();system2.SomeMethod();// staticの場合SomeSystem.SomeMethod();// シングルトンの場合SomeSystem.Instance.SomeMethod();}}}

構成

ServiceLocator

サービスロケーター実装本体です
型とインスタンスをセットで登録します。
型を指定してインスタンスを取得します。
事前にインスタンス生成しておくパターンと都度インスタンス生成するパターンに対応しています。
事前インスタンス生成パターンはシングルトンの代替になります。
Resources.LoadでPrefabを取得してMonoBehaviourを登録することもできます。

SomeSystem

何らかのシステムです。
例. セーブデータ、マスターデータ、サウンド、チュートリアルなど
ファイルやプラットフォーム周りを扱うシステムが対象になりやすいです。

ProjectInitializer

使用するクラスを決めるところです。
RuntimeInitializeOnLoadMethodによってどのシーンを実行しても最初に処理されます。Awakeより先に実行されます。

コード例ではインスタンスを生成して登録していてResolveされたときは単一インスタンスが使われます。

下記のようにするとResolveされる度にインスタンス生成されます。

Locator.Register<ISomeSystem,SomeSystem>();
SomeScene

サービスロケーターを使用してシステムを利用するところです。
比較のためnew、static、シングルトンのコードも配置しています。

活用例

アセットバンドルシステム

アセットバンドルをサーバーからロードする実装とローカルファイルをロードする実装を切り替えられるようにします。
システム利用側はアセットバンドルのロード先を意識せずに実装できます。
開発時はローカルファイルからロードすることでアセットバンドルビルドやサーバーから取得する手間をなくし効率良く開発できます。
AssetBundleManagerのSimulation ModeやAddressableのFast Modeと同じ機能です。

プラットフォームごとに異なる実装

iOSとAndroidなどプラットフォームによって実装が異なる機能を共通インターフェースで機能を提供します。
プラットフォームごとにクラスを作成します。
システム利用側はプラットフォームの違いを意識する必要がなくなり、システム実装側は1クラスにプラットフォームの分岐を多数記述する必要がなくなります。

その他活用案
  • 負荷の高いシステムを無効化する
  • システムにログを仕込む

補足

  • サムザップではDependency Injectionコンテナ(Zenject)を採用しているプロジェクトもあります。Factoryで複数の実装を切り替えています。 ただ、Zenjectはパフォーマンスの懸念と、学習コストが高い面がありサービスロケーターを採用しているプロジェクトもあります。
  • 一応シングルトンでも継承を利用することで複数の実装を切り替えることができます。
  • 実装は1つで良いと割り切ってシンプルなstaticクラスを採用することもあります。用途に合わせて選択すると良いかと思います。
  • サービスロケーターを使ってメリットが大きいクラスに使うと良いです。オブジェクトを引数で簡単に渡せる場合には渡した方が良いと思います。

最後に

明日は @tomeitouさんの記事です。


Viewing all articles
Browse latest Browse all 9374

Latest Images

Trending Articles