サービスロケータによる依存性注入
IEmployeeTaskのどの実装を使用すべきかプレゼンターが知る必要性を完全になくすためには、新しい要素を追加しなければなりません。依存性注入の世界では、サービスロケータとは他のオブジェクトの依存物を取得する方法を知っているオブジェクトにすぎません。今回のシナリオにおけるサービスロケータの役割は、プレゼンターが扱えるIEmployeeTaskの実装を見つけることです。ここまで読んできた方は「つまり、プレゼンターはこのサービスロケータへの依存を持つことになるのか」と思ったことでしょう。そのとおりです。「それなら、サービスロケータを受け入れるためにプレゼンターのコンストラクタを変更する必要があるのだろうか」と思った方もいるでしょう。そういう方法もあります。しかしそうすると、他のクラスでもロケータのサービスを利用して依存を解決したくなった場合に、それらのクラスについてもサービスロケータへのインターフェイスを受け入れるコンストラクタが必要になるでしょう。そうなると、アーキテクチャへの侵害になりそうですし、必要以上の作業を強いられそうにも見えます。
私が考えているソリューションは、実際のプロジェクトでサービスロケータを利用してみたいと思う読者なら、すぐに採用できるものです。また、Windsor Castleのような本格的な依存性注入フレームワークを使い始めるときに応用できるソリューションでもあります(Windsor Castleは有名なオープンソースの依存性注入フレームワークです)。まずサービスロケータのインターフェイスを作成することにします。
public interface IDependencyResolver : IDisposable { void RegisterImplmentationOf<T>( T component); T GetImplementationOf<T>(); }
このコードでは、クライアントのメソッドからロケータを呼び出すときにキャストする必要がないように汎用メソッドを使用しています。前述のとおり、今回のシナリオでは、サービスロケータへの明示的なパラメータ依存を持たなくても、サービスロケータによって提供される機能を既存のクラスでコンシュームできるようにしたいと考えています。これを実現するために、すべての呼び出しをサービスロケータインターフェイスの任意の具象実装に委任する静的クラスを使用します。
public static class DependencyResolver { private static IDependencyResolver resolver; public static void RegisterResolver( IDependencyResolver resolver) { DependencyResolver.resolver = resolver; } public static T GetImplementationOf<T>() { return resolver.GetImplementationOf<T>(); } }
DependencyResolverクラスにRegisterResolverというメソッドがあることに注目してください。このメソッドには、自らの呼び出しの転送先となるIDependencyResolverの実装が渡されます。これは、プレゼンターの観点からすると、IEmployeeTaskのEmployeeTask実装の詳細を知らなくても、DependencyResolverクラスを使用してIEmployeeTaskの実装を探し出せるということを意味します。これに基づき、EmployeePresenterのコンストラクタは次のものから、
Public ViewEmployeesPresenter( IEmployeeView view):this( view,new EmployeeTask()) { } public ViewEmployeesPresenter( IEmployeeView view, IEmployeeTask task)
次のように変更されます。
public ViewEmployeesPresenter( IEmployeeView view):this(view, DependencyResolver.GetImplementationOf <IEmployeeTask>()) { } public ViewEmployeesPresenter( IEmployeeView view, IEmployeeTask task)
前述の「完全版」コンストラクタを引き続き使用していることに注目してください。このコンストラクタは、プレゼンターの動作を検証するためのテストに利用できます。Webページで使用するコンストラクタでは、プレゼンターはDependencyResolverを使用し、IEmployeeTaskの実装を取得するように要求します。これで、プレゼンターとEmployeeTaskを結合する部分は何もなくなります。
今回の例では、静的なDependencyResolverクラスを使用して、プレゼンターをサービスロケータに結合しました。「静的クラスなんてとんでもない」とか「シングルトンはだめだ」という声が聞こえてきそうですが、静的クラスもシングルトンも、テスト容易性を考慮したものならばそう問題にはなりません(従来のシングルトンや静的クラスの実装でこれを扱うのは簡単ではありません)。今回のシナリオでは、DependencyResolver静的クラスは自分の仕事を(テスト時に簡単に偽造できる)IDependencyResolver実装に委任するだけなので、テストを容易に実行できます。
IDependencyResolverインターフェイスを使用することの利点は、サービスロケータを使用する道を選んだときに、アプリケーションの起動時に設定される独自のサービスロケータを作成できることです。リスト5は、そのようなロケータのコードを示しています。以下は、アプリケーションの起動時に呼び出されるクラスで独自のサービスロケータを設定する例です。
public class ApplicationStartupTask { public static void Initialize() { IDependencyResolver resolver = new CustomDependencyResolver(); resolver.RegisterImplmentationOf<IEmployeeTask> (new EmployeeTask()); DependencyResolver.RegisterResolver(resolver); } }
IEmployeeTaskの実装にEmployeeTaskオブジェクトを返すよう求めるタイミングをサービスロケータに明確に指示していることに注目してください。また、この実装はEmployeeTaskに対して、たとえシングルトンとして明示的に指定されていなくても、このアプリケーションでシングルトンになるように強要してもいます。最後のステップでは、サービスロケータをDependencyResolverに登録して、すべてのクライアントがその機能にアクセスできるようにしています。
読者が自分のサービスロケータにもっと多くの機能を求めるとしても、自分でそれをコーディングしたくはないでしょう。IDependencyResolverインターフェイスを使用すれば、既存の依存性注入フレームワークを利用している実装へと簡単に切り替えることができます。リスト6は、Windsor Castleを使用するようにコーディングされたIDependencyResolver実装の例です。
public class CustomDependencyResolver : IDependencyResolver { private IDictionary<Type, object> components; public CustomDependencyResolver() { components = new Dictionary<Type, object>(); } public void RegisterImplmentationOf<T>(T component) { components.Add(typeof (T), component); } public T GetImplementationOf<T>() { return (T) components[typeof (T)]; } public void Dispose() { components.Clear(); } }
インターフェイスへのコーディングのメリットは、Windsorやその他の依存性注入フレームワークに切り替える場合に、クライアントコードの変更がごくわずかで済むことです。実際、Windsorを使用するために必要な変更は、ApplicationStartupTaskクラスを次のように変更することだけです。
public class ApplicationStartupTask { private static readonly string ContainerConfigurationPath = Path.Combine(AppDomain. CurrentDomain.BaseDirectory, "container.boo"); public static void Initialize() { IWindsorContainer container = new WindsorContainer(); BooReader.Read(container, ContainerConfigurationPath); DependencyResolver.RegisterResolver( new WindsorDependencyContainer(container)); } }
Windsorのようなフル装備のフレームワークを使用することの利点は、依存物の特定の実装に縛られたコードがまったく(ApplicationStartUpの中にさえ)なくなることです。依存物を必要とするオブジェクトは、それを取得するように引き続きDependencyResolverクラスに要求することができ、DependencyResolverはその要求を満たすために与えられたIDependencyResolverの実装を、それがどんな実装でも使用します。
本稿のサンプルコードでは、私はBinsorを使ってWindsorコンテナを設定しています。BinsorはXMLファイルを使わずにWindsorを設定するためのツールで、Ayende Rahienによって開発されました。これを使用すると、アプリケーションの外部に格納されたコンフィグレーションを使って依存を設定することができます。
まとめ
本稿では多くの問題を論じました。初めに、よく知られている多くのベストプラクティスに反する単層アプリケーションを取り上げ、階層化アーキテクチャといくつかのリファクタリング手法、さらに依存性逆転の原則について説明しました。実装へのコーディングに比べて抽象へのコーディングを行った方が有利である理由についても触れました。さらに、抽象へのコーディングの利点をよりよく理解していただくために、依存性注入とサービスロケータという概念を紹介しました。
本稿で紹介した内容が、読者の皆さんのアプリケーションアーキテクチャでより緩やかに結合されたソリューションを実現するためのお役に立てば幸いです。
