nuits.jp blog

C#, Xamarin, WPFを中心に書いています。Microsoft MVP for Development Technologies。

Service LocatorとDependency InjectionパターンとDI Container

本エントリーでは次の点を整理したいと思います。

  • ServiceLocatorパターンとは何か?
  • Dependency Injection(以降DI)パターンとは何か?
  • DI Containerとは何か?
  • これらを使うと何がうれしいのか?
  • ServiceLocatorとDI何が違うのか?
  • ServiceLocatorとDIどちらを使うべきか?

はじめに

さて現代において、一定規模以上のアプリケーションを構築しようとした場合、モノリシックな一枚岩のようなソフトウェアとして構築しようとする人は稀でしょう。
アプリケーションを何らかの関連性に則って分割・開発し、それらを結合することで実現しようとするはずです。

たとえば、現在地から周辺のレストランを検索するようなアプリケーションを想定しましょう。 この場合、二つのステップで解決しますよね?

  1. 現在地を特定する
  2. 特定された地点周辺のレストランを検索する

これらを、レストラン検索を利用するアプリケーションも含めてクラスに分割した場合、大体こんな感じになるはずです。

https://raw.githubusercontent.com/nuitsjp/BlogAssets/master/01.ServiceLocator%20vs%20Dependency%20Injection/ClassModel01.png

実際の利用シーケンスはこんな流れでしょう。

https://raw.githubusercontent.com/nuitsjp/BlogAssets/master/01.ServiceLocator%20vs%20Dependency%20Injection/Sequence01.png

そして実装したコードはこのようになります。

RestaurantService restaurantService = new RestaurantService();
var restaurants = restaurantService.GetRestaurants();
...
public class RestaurantService
{
    public IList<Restaurant> GetRestaurants()
    {
        GeolocationService geolocationService = new GeolocationService();
        return GetRestaurants(geolocationService.GetCurrentLocation());
    }

    public IList<Restaurant> GetRestaurants(Location location)
    {
        ...
    }
}

これをベースに考えていきます。

問題点

さて先ほどの現在地周辺のレストランを検索するAPI「GetRestaurants」に対してUnitTestを書くとしましょう。
この時問題になるのが、RestaurantServiceがGeolocationServiceと密結合しているということです。

https://raw.githubusercontent.com/nuitsjp/BlogAssets/master/01.ServiceLocator%20vs%20Dependency%20Injection/ClassModel02.png

この部分のインスタンスの生成と利用それぞれが、GeolocationServiceの実装クラスに直接依存しており、取り換えが効かない状態となっています。
この状態を密結合された状態と呼びます。

GeolocationServiceが仮にどこでも正しく動くクラスだとしても、UnitTestを実施する立地によってGetRestaurantが返すレストラン情報が変わってしまうのではテストがしにくくて仕方がありません。
そこでGeolocationServiceをモックに差し替えてテスタビリティを確保したい、という発想になるかと思います。
それが簡便に実現可能な状態が、疎結合な状態です。

利用箇所の結合度をさげる

まずはServiceLocatorもDIも関係ない領域です。
GeolocationServiceからIGeolocationServiceインターフェースを抽出して利用箇所の結合度を下げます。

https://raw.githubusercontent.com/nuitsjp/BlogAssets/master/01.ServiceLocator%20vs%20Dependency%20Injection/ClassModel03.png

コードを見てみましょう。

public class RestaurantService
{
    public IList<Restaurant> GetRestaurants()
    {
        // GeolocationService geolocationService = new GeolocationService();
        IGeolocationService geolocationService = new GeolocationService();
        return GetRestaurants(geolocationService.GetCurrentLocation());
    }

    ...
}

geolocationServiceの型がIGeolocationServiceに変更されています。
これで利用箇所、つまりGetCurrentLocationを呼び出す箇所はGeolocationServiceに依存しなくて済むようになりました。

しかし依然として生成箇所は密結合したままとなっています。
次はその部分を解決しましょう。

生成箇所の結合度をさげる

さて、ここからServiceLocatorとDIでアプローチが異なります。
まずはServiceLocatorから解説します。

ServiceLocatorパターン

ServiceLocatorとは、依存先のオブジェクトを解決するための役割もったオブジェクトの事です。
Factory系のパターンと似通っていますが、ServiceLocatorで解決する依存先オブジェクトは、必ずしも毎回インスタンスを生成するとは限りません。
システムでたった一つしか存在しないインスタンスかも知れません。その点がFactory系のパターンとは異なります。

さて、ServiceLocatorを導入した具体的なクラス図は、つぎのとおりです。

https://raw.githubusercontent.com/nuitsjp/BlogAssets/master/01.ServiceLocator%20vs%20Dependency%20Injection/ClassModel04.png

利用例はつぎのとおりです。

public class RestaurantService
{
    public IList<Restaurant> GetRestaurants()
    {
        IGeolocationService geolocationService = ServiceLocator.Resolve<IGeolocationService>();
        return GetRestaurants(geolocationService.GetCurrentLocation());
    }
}

ServiceLocatorの実装は本題から外れますが、例えばつぎのような実装が考えられます。

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

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

    public static TKey Resolve<TKey>()
    {
        return (TKey) Activator.CreateInstance(_typeDictionary[typeof(TKey)]);
    }
}

これでRestaurantServiceとGeolocationServiceが完全に疎結合な状態となりました。

Dependency Injectionパターンを使う

つづいてDIパターンを使った疎結合の実現方法を解説します。
厳密にいうと「DIパターン」を利用することと、「DI Container」を利用する事は異なります。
ここではまず「DIパターン」の例を示します。

クラスの依存関係はつぎのようになります。

https://raw.githubusercontent.com/nuitsjp/BlogAssets/master/01.ServiceLocator%20vs%20Dependency%20Injection/ClassModel05.png

変更点はつぎの二つです。

  1. GeolocationServiceのインスタンス化をApplicationが行うように変更
  2. RestaurantServiceのコンストラクタにGeolocationServiceを引数に取るよう変更

コードを見た方が早いかもしれません。

var geolocationService = new GeolocationService();
var restaurantService = new RestaurantService(geolocationService);
var result = restaurantService.GetRestaurants();
public class RestaurantService
{
    private readonly IGeolocationService _geolocationService;

    public RestaurantService(IGeolocationService geolocationService)
    {
        _geolocationService = geolocationService;
    }

    public IList<Restaurant> GetRestaurants()
    {
        return GetRestaurants(_geolocationService.GetCurrentLocation());
    }
}

依存(Dependency)オブジェクトであるIGeolocationServiceを外部から挿入(Injection)しているのが見て取れるでしょう。
この様な設計パターンがDependency Injectionパターンです。

これでRestaurantServiceとGeolocationServiceは疎結合な状態となり、RestaurantServiceのテスタビリティも確保できました。
RestaurantServiceをテストする際には、IGeolocationServiceを実装したテストに都合の良いMockをインジェクションすると良いでしょう。
たとえばつぎの例では、東京の座標を常に返すTokyoGeolocationServiceをインジェクションしています。

//var geolocationService = new GeolocationService();
var geolocationService = new TokyoGeolocationService();
var restaurantService = new RestaurantService(geolocationService);
var result = restaurantService.GetRestaurants();

ただクラス関係がシンプルなうちは良いのですが、クラス数が増えてきたり、関連が複雑になると利用者側の負担が上がります(今回のコードではApplicationの役割)。
そこで登場するのがDI Containerです。

Dependency Injection Container

DI ContainerはDIパターンをサポートするライブラリです。
先ほどのコードではRestaurantServiceの利用者が、必要なオブジェクトを用意して組み立てていましたが、これを代替してくれるものになります。
クラス図を見てみましょう。

https://raw.githubusercontent.com/nuitsjp/BlogAssets/master/01.ServiceLocator%20vs%20Dependency%20Injection/ClassModel06.png

RestaurantServiceとGeolocationServiceの生成と組み上げはDIコンテナが行います。
利用者は、Containerから必要なオブジェクトを取得して利用するだけです。
処理の流れはつぎのような形になるでしょう。

https://raw.githubusercontent.com/nuitsjp/BlogAssets/master/01.ServiceLocator%20vs%20Dependency%20Injection/Sequence03.png

さて、鋭い人はもうお気づきかも知れません。
DI Containerは利用者からみるとServiceLocatorそのものだということです。

DI Containerを利用したアプリケーションにおいて、ほとんどのオブジェクトはコンテナが生成しオブジェクトツリーをインジェクションしながら構築します。
しかしオブジェクトツリーを上へ上へ辿っていくと、DI Containerから明示的にオブジェクトを取得している、つまりServiceLocatorパターンを利用している箇所に行き着くはずです(高レベルのフレームワークではその箇所が隠ぺいされていることもあります)。

MVVMパターンを採用したアプリケーションなどでは、ViewModelの取得時にDI Containerから明示的なオブジェクトの取得を行っているケースも多いでしょう。

ServiceLocatorとDependency Injection いずれを利用すべきか?

さて、ServiceLocatorとDIはいずれも複数のクラスを疎結合に保つためのデザインパターンです。それではServiceLocatorとDI、(トップレベルのオブジェクトの取得を除き)何れを利用すべきでしょうか?

一般的にはDIの利用を推奨されることが多いようです。

ServiceLocatorが推奨されない場合の理由は主に三つあります。

  1. 本来不要であるServiceLocatorへの依存が発生してしまう
  2. 依存関係が分かりにくくなる
  3. テストが困難になる

本来不要であるServiceLocatorへの依存が発生してしまう

例えば先のRestaurantServiceはGeolocationServiceだけあれば十分なはずでした。
ServiceLocatorパターンを採用すると、RestaurantServiceの利用者は本来ServiceLocatorの利用を強制されます。

DIコンテナを利用する場合は、同様のジレンマが発生しますが、DIコンテナを利用せずにDIパターンだけを利用する事も出来ます。
つまりDIを採用した場合、選択権は利用者にありますがServiceLocatorでは利用者に選択権がありません。

依存関係が分かりにくくなる

Serviceが他の何に依存しているのか、ServiceLocatorを利用した場合は外から把握することができません。
自作のクラスであればコードを見れば良いかもしれませんが、バイナリとして提供されていた場合は適切なドキュメントの存在が必要不可欠でしょう。
DIであれば必要なものは全てシグニチャから判断することができます。

テストが困難になる

ServiceLocatorを利用した場合と、DIの場合のテストコードを比較してみましょう。

public void ServiceLocatorPattern()
{
    ServiceLocator.Register<IGeolocationService>(
        () => new GeolocationServiceMock{ Location = new Location(35.681167, 139.767052) });
    var restaurantService = new RestaurantService();
    restaurantService.GetRestaurants();
}

public void DependencyInjectionPattern()
{
    var restaurantService = 
        new RestaurantService(
            new GeolocationServiceMock { Location = new Location(35.681167, 139.767052) });
    restaurantService.GetRestaurants();
}

どちらもテスト用のMockを利用する事は可能ですが、ServiceLocatorのセットアップが発生する分どうしてもServiceLocatorパターンの方が煩雑なコードになりがちです。
DIパターンの方が直観的でもあります(もっとも好みの差はあるかもしれません)。

またテストケースがマルチスレッドで実行される場合は、さらなる課題が発生する事もあります。
これはServiceLocatorはグローバル変数的なふるまいをする事が多いので、UnitTestの並行実行時にすれ違いで意図しないMockが設定されてしまう可能性があるためです。

つぎの例をみてください。

  • UnitTest Aは、東京のレストランを取得するテストを実行します
  • UnitTest Bは、大阪のレストランを取得するテストを実行します

https://raw.githubusercontent.com/nuitsjp/BlogAssets/master/01.ServiceLocator%20vs%20Dependency%20Injection/Sequence04.png

UnitTestAではServiceLocatorに東京の座標を固定で返すTokyoGeolocationServiceを登録し、UnitTest Bは大阪の座標を固定で返すOsakaGeolocationServiceを登録します。
上の図ではUnitTest Aが、ServiceLocatorからRestaurantServiceを取得する前に、UnitTest BがOsakaGeolocationServiceを登録してしまっているため、結果的に大阪のレストランが取得され、テストはエラーとなってしまうわけです。
しかもタイミング依存なので、必ずしもそうなるとは限りませんし、逆にUnitTest B側がエラーになるかもしれません。

これに対して、DIパターンでは、テスト対象のオブジェクトをUnitTestのコードから手動で組み上げられます。
DI Containerを利用する必要もありません。

こういった側面から、UnitTestを想定するとDependency Injectionパターンがより良いケースであることが多いと言えます。

以上です。