サービスロケータ、手動 DI、そして DI コンテナ

サービスロケータ、手動 DI、そして DI コンテナ

August 30, 2022

本記事に登場するコードの言語は C# です。

最悪なパターン #

サービスロケータや DI の話に入る前に、まずは何もできていない状態です。

public class Model
{
  private readonly IRepository _repository;

  public Model()
  {
    this._repository = new Repository();
  }
}
Model model = new Model();
  • 悪い点
    • Model クラスの中で Repository を直接インスタンス化している。
    • Repository を差し替えることができないので最悪。

サービスロケータ(Serivce Locator)パターン #

サービスロケータパターンとは要するに以下です。

  • サービスロケータという一元管理クラスが存在します。
  • その他のクラスはサービスロケータに対してインスタンスを要求します。
  • そのインスタンス生成にかかる処理はすべてサービスロケータの内部で行われます。
  • そのため、各クラスが個別にインスタンス生成処理を実施する必要はなくなります。

同じ内容にはなりますが Wikipedia からの引用も載せておきます。

The service locator pattern is a design pattern used in software development to encapsulate the processes involved in obtaining a service with a strong abstraction layer. This pattern uses a central registry known as the “service locator”, which on request returns the information necessary to perform a certain task. Proponents of the pattern say the approach simplifies component-based applications where all dependencies are cleanly listed at the beginning of the whole application design, consequently making traditional dependency injection a more complex way of connecting objects. Critics of the pattern argue that it is an anti-pattern which obscures dependencies and makes software harder to test.

翻訳:サービスロケータパターンは、ソフトウェア開発において、サービス取得に関わるプロセスを抽象化レイヤでカプセル化するために用いられるデザインパターンである。このパターンは「サービスロケータ」と呼ばれる中央レジストリを使用し、要求に応じて特定のタスクを実行するために必要な情報を返す。このパターンの支持者は、サービスロケータを用いない従来の依存性注入がより複雑なものであるのと比較して、サービスロケータを用いるとアプリケーション全体のすべての依存関係が一箇所にリストされることでアプリケーションをシンプルにできると言っている。このパターンの批評家は、依存関係を不明瞭にし、ソフトウェアのテストを困難にするアンチパターンであると主張している

https://en.wikipedia.org/wiki/Service_locator_pattern

// 何かしらのキーをもとに特定のインスタンスを返す、という動きができれば良く、
// サービスロケータの具体的な実装方法はどんなものでも良いです。これは一例。

public static class ServiceLocator
{
  private static readonly IDictionary<Type, Type> _typeDictionary = new Dictionary<Type, Type>();

  public static void Register<TKey, TValue>()
  {
    this._typeDictionary[typeof(TKey)] = typeof(TValue);
  }

  public static TKey Resolve<TKey>()
  {
    return (TKey) Activator.CreateInstance(_typeDictionary[typeof(TKey)]);
  }
}
public class Model
{
  private readonly IRepository _repository;

  public Model()
  {
    // サービスロケータ経由でインスタンスを取得
    this._repository = ServiceLocator.Resolve<Repository>();
  }
}
// アプリケーションの起動処理時などで一元的にすべてをサービスロケータに登録しておく
ServiceLocator.Register<IRepository, Repository>();
Model model = new Model();
  • 良い点
    • 各クラスは自分が必要とするインスタンスをサービスロケータ経由で取得しているため、ServiceLocator.Register(); にて登録するインスタンスを書き換えれば、すべての実体の差し替えが完了する。
    • そのため例えば、プロダクション用とテスト用でアプリの起動処理を書き分け、別のものを Register するようにしておけば、それだけで実体の差し替えができる。
  • 悪い点
    • 当然ながら Register をせずにクラスをインスタンス化しようとするとエラーになる。しかし、そのクラスを見ただけではその仕組みに気づくことができない。
    • また、サービスロケータ経由で取得していることで、そのクラスが他のどのインスタンスに依存しているのかが、かえって見えづらくなってしまった。
    • 各クラスは新たにサービスロケータに依存してしまうことで依存先が増える。
      • 具体的には、サービスロケータ導入前では ModelIRepository にのみ依存していた、しかしサービスロケータ導入後だと ModelIRepository に加えて新たにサービスロケータそのものにも依存してしまうようになった。

一般的にはアンチパターンと言われることが多いようです。サービスロケータパターンは採用すべきではないでしょう。

手動 DI パターン #

使用者側で手動で DI します。

public class Model
{
  private readonly IRepository _repository;

  public Model(IRepository repository)
  {
    this._repository = repository;
  }
}
Model model = new Model(new Repository());
  • 良い点
    • そのクラスが何に依存しているかがコードからわかりやすい。
    • コンストラクタに渡すインスタンスを変えれば差し替えができる。
  • 悪い点
    • シンプルなうちは良いが、クラス数が増えたり、依存関係が複雑になってくると使用者側の負荷があがる。
    • インスタンス化の処理をあちこちで記述することになる。つまり DI するインスタンスを変えたくなった時、インスタンス化している箇所が多くなると修正作業が大変になる。
// 使用者側の負荷があがる例:
Model model = new Model(new ServiceAAA(new ServiceBBB(new ServiceCCC(new ServiceDDD(new ServiceEEE())))));

// 修正作業が大変な例:
// - DI するインスタンスを Repository() から NewRepository() に変えたいとして、
//   それをインスタンス化しているすべての箇所、ファイルを修正しなければいけない。
Model model1 = new Model(new NewRepository());
Model model2 = new Model(new NewRepository());
Model model3 = new Model(new NewRepository());

総じて、規模が大きくなると問題になる場合もありますが、そうでないならばこのパターンでも問題はないと思います。

DI コンテナ(DI Container)パターン #

「DI コンテナパターン」は「サービスロケータパターン」と「手動 DI パターン」を組み合わせたパターンといって差し支えないでしょう。

サービスロケータパターンにはデメリットもあったものの、使用者側からするとメソッドを呼ぶだけで完結する簡単さが良い点です。これを利用して、手動 DI パターンが抱える悪い点、つまり使用者側に負荷がかかる、というのをカバーします。

// アプリケーションの起動処理時などで一元的にすべてをコンテナに登録しておく

// DI コンテナ
var serviceCollection = new ServiceCollection();

// 依存性の登録
serviceCollection.AddTransient<IRepository, Repository>;
serviceCollection.AddTransient<Model>;
// コンテナから必要なインスタンスを取得
var provider = serviceCollection.BuildServiceProvider();
Model model = provider.GetService<Model>();

サービスロケータパターンの際は Model クラスの中で直接 ServiceLocator.Resolve<Repository>(); をしていました。

しかしこれだとサービスロケータパターンの悪い点で記載したような問題があったことから、Resolve にあたる処理をクラスの外に出したのが「DI コンテナパターン」と言えるのではないでしょうか。

また DI コンテナパターンだと、Model クラスの記述は「手動 DI パターン」と同じになります。そのため、クラスのコードの明瞭さは保たれたままです。

その他参考 #