はじめに
お手軽に作れてサービスを集約させることのできる「ServiceLocator」ですが、登録と取得、削除のみでは使いづらいと思って関数を追加する人も多いと思います。その時に起きがちなミスを紹介します。
通常のServiceLocator
一番スタンダードなServiceLocatorの組み方はこのようになると思います。
public static class ServiceLocator
{
private static Dictionary<Type, object> _dic = new();
/// <summary>
/// サービスロケーターに登録する
/// </summary>
public static void Register<T>(T instance)
{
if (_dic.ContainsKey(typeof(T)))
{
_dic[typeof(T)] = instance;
}
else
{
_dic.Add(typeof(T), instance);
}
}
/// <summary>
/// サービスロケーターからインスタンスを取得する
/// </summary>
public static T Get<T>()
{
return (T)_dic[typeof(T)];
}
/// <summary>
/// サービスロケーターから登録解除する
/// </summary>
public static void Unregister<T>()
{
if (_dic.ContainsKey(typeof(T)))
{
_dic.Remove(typeof(T));
}
}
}
Register<T>
でインスタンスを登録し、Get<T>
で取得、Unregister<T>
で登録解除するシンプルな構造です。
Getで取得できない場合に明示的なエラーを出すかnullを返すかすべきですが今回は省略します。
設計
そもそもServiceLocator自体がアンチパターンと言われることが多いです。
コードからクラスの依存関係を判断しづらいことや、依存関係の検証が実行時に行われるためnull参照が起こりやすいなど、依存関係にかなりの難があるからです。
プロジェクトの規模によってはこれを使う判断をしてもいいと思います。ただし、汎用性を上げようとして、Getメソッドの中に生成を含めたコードを書くのはアウトです。
設計の知識はそこまで豊富なわけじゃないのでこんなこと書くつもりなかったのですが生成を含んだServiceLocatorを見てしまったので書いておきます。
どういうことかというと、下のコードのようにGetメソッドを実行したときにインスタンスが登録されていなければ生成するというもの。
public static T Get<T>() where T : class
{
if (_dic.ContainsKey(typeof(T)))
{
return (T)_dic[typeof(T)];
}
return Activator.CreateInstance(typeof(T)) as T;
}
おそらく目的は取得したいときにエラーが出てしまったから出ないように生成するというものでしょう。
やってはいけない理由は次のことが挙げられます。
- 単一責任の原則に反する
- 登録の順序によってはインスタンスが異なるため処理の内容が変わってしまう
- 引数付きコンストラクタの生成ができない
後ろ二つは特に、設計関係なくバグが発生する原因にもなる理由なのでServiceLocatorに生成を含めるのは控えましょう
メモリ
過去にサービスロケーターを作ったとき、クラスにジェネリック引数をつけていました
public static class ServiceLocator<T> // ここで登録する型を書いてしまう
{
private static Dictionary<Type, T> _dic = new();
// ・・・省略
使うときは次のような感じ。
ServiceLocator<ClassA>.Register(new ClassA());
これは一見何も問題ないように見えますが、メモリに無駄が生じます。
静的クラスは参照が発生するとアプリの終了まで常にメモリに存在し続けます。
これがジェネリック引数をつけるとその引数に指定した型の数だけ別々のメモリアドレスが割り当てられるそうです。
つまり、ClassA
を登録した場合ServiceLocator<ClassA>
として、ClassB
を登録した場合ServiceLocator<ClassB>
としてメモリに保存されます。
また、サービスロケーター内で定義している_dic
は常に一つのクラスしか保存されないです(_dic.Countが常に1になる)。
パフォーマンス
サービスロケーターを作る側は対策できないことですが、サービスロケーターに値型を登録するとボックス化され、取得する際にもアンボックス化されます。これはメモリ確保のオーバーヘッドになります。
用語
値型
対となる参照型は値への参照が保存されるのに対し、値型はメモリ内に直接保存されます。
intやfloatなどがこれに当たります。
ボックス化
値型が参照型へ変換されることをボックス化といいます。
特にobject型にしたりinterfaceにしたりすると発生します。
今回ではRegister<T>(T instance)
で登録する際、ここにstruct(構造体)で定義したMyStruct
を登録しようとした場合、サービスロケーター内のDictionaryが<Type, object>
で定義されているため、object型にボックス化されると表現できます。
アンボックス化
ボックス化の逆の操作で、先ほどの例を使うと登録されたMyStruct
を取り出す際、object型からMyStruct型に変換するとき参照型から値型に変換されます。
まとめ
今回はこれまでの経験をもとにServiceLocatorを作る際の注意点を「設計」「メモリ」「パフォーマンス」の項目に分けて説明しましたが、このように拡張しようとしているということは現在のサービスロケーターが不便に感じているのだと思います。
少し学習コストは高くなりますが「DependencyInjection」通称DI
について学んでみるのがいいと思います。