はじめに
大多数の開発者は階層化アーキテクチャアプローチの価値を理解しています。階層化アーキテクチャの中核的な考え方は「責任の分割」です。各層はそれぞれ限定的な量の仕事について責任を負います。ある特定の層で遂行すべきでない仕事は、それを処理するのにふさわしい層に委任されます。
しかし残念ながら、階層化アーキテクチャを支持している人でも、階層間に必要以上の結合を持ち込んでしまうことがよくあります。階層間の結合度が高くなると、アーキテクチャの変更や拡張が難しくなり、結果として脆弱なアプリケーションになりがちです。
本稿では、堅牢性やテスト容易性(testability)という点で問題のある手法を使って構築されたプロジェクトを例にとり、アプリケーションの柔軟性とテスト容易性を高めるのに役立つ原則とテクニックとリファクタリング方法をいくつか適用します。
本稿の最初の部分では、階層化アーキテクチャをもっとスマートなやり方で利用できるようにするための設計原則について説明します。さらに、このプロジェクトに依存性注入を取り入れてプラグ可能なアプリケーションアーキテクチャを実現する方法を具体的に示します。
手近な事例
まずは手近な例で考えてみましょう。私は自分の会社の従業員情報を表示するために、図1のような画面を開発するよう頼まれました。これはごく「単純な」画面なので、私はすぐにリスト1のようなコードを書き上げました(念のため言っておきますが、これが現実の例であれば、私は決してこんなコードは書きません)。
リスト1 最初のユーザーインターフェイス
public partial class
ViewEmployeesTightlyCoupled : Page
{
private const string ConnectionString =
"data source=(local);Integrated Security=SSPI;" +
"Initital Catalog=NorthWind";
private SqlConnection connection;
protected void Page_Load(object sender, EventArgs e)
{
if (! IsPostBack)
{
PopulateGridFrom(MapFrom(GetAllEmployees()));
}
}
private DataTable GetAllEmployees()
{
using (connection = new
SqlConnection(ConnectionString))
{
SqlCommand command = connection.CreateCommand();
command.CommandText = "SELECT * FROM Employees";
command.CommandType = CommandType.Text;
connection.Open();
using (SqlDataReader reader = command.ExecuteReader(
CommandBehavior.CloseConnection))
{
DataTable results = new DataTable();
results.Load(reader);
return results;
}
}
}
private List<Employee> MapFrom(DataTable employeeData)
{
List<Employee> employees = new List<Employee>();
foreach (DataRow employeeRow in employeeData.Rows)
{
employees.Add(MapFrom(employeeRow));
}
return employees;
}
private void PopulateGridFrom(List<Employee> employees)
{
this.employeesRepeater.DataSource = employees;
this.employeesRepeater.DataBind();
}
private Employee MapFrom(DataRow row)
{
return new Employee(Convert.ToInt32(
row["EmployeeId"]), row["LastName"].ToString(),
row["FirstName"].ToString(), row["Title"].ToString(),
Convert.ToDateTime(row["BirthDate"]),
Convert.ToDateTime(row["HireDate"]));
}
}
リスト1は、私がわざと問題のある方法で記述したコードです。コードの内容をよく読んでみると、いろいろな問題が見えてくるのではないでしょうか? このコードはアプリケーションアーキテクチャのいくつかのルールに違反しています。
- 接続文字列がハードコーディングされている
- ユーザーインターフェイス側がデータベーステーブルの詳細を知らなければならない
- ユーザーインターフェイス側がデータベースデータをドメインオブジェクトにマップしなければならない
- 抽象ではなく実装にコーディングしている
最後の項目のことはしばらく忘れてください。まずは最初の3つの項目について、階層化アーキテクチャによる改善に取り組みます。
階層化アーキテクチャによる責任の分割
リスト1の主な問題点の1つは、単一責任の原則を完全に放棄していることです。単一責任の原則とは、簡単に言えば「あらゆるオブジェクトは責任を1つしか持ってはならず、したがって変更する理由は1つでなければならない」というものです。この原則に従っているコンポーネントは一般に、「凝集度の高い」コンポーネントと表現されます。
もしも質問する機会があったなら、私は「この従業員表示Webページの主な
責任は何ですか」と尋ねたことでしょう。この質問に対する答えはおそらく、「必要なのは従業員データをユーザーに表示することだけだ」となります。ところが、コードビハインド(code behind:分離コード)の内容を見ると、実態はまったく違っています。現在、このコンポーネントは従業員データをユーザーに表示するという1つの責任を負う代わりに、以下のすべての責任を負っています。
- データベースへの接続を作成する
- データベースから適切な情報を引き出すためのSQLステートメントを作成する
- 高コストのリソース(接続やリーダーなど)を処理する
- データベースの情報をデータのドメイン表現にマップする
- ユーザーに情報を表示する
この短いリストを見てもわかるように、このコンポーネントが負っている責任は多すぎます。さらに悪いことに、これらの責任のうち、従業員のリストを「表示する」ことに関係しているのは1つだけです。これでは凝集度が低いと思わざるを得ません。
大多数の開発者は、アプリケーションにn層アーキテクチャという概念を導入することが、この問題の解決に役立つということにかなり前から気づいていました。要するに、階層化アーキテクチャを導入すれば、各層が(できるだけ実際的に)単一責任の原則をまっとうできるようになります。図2は、アプリケーションを構成する分離された層の概念図です。
以下では、このアプリケーションの全面的なリファクタリングを試みることにします。Webページのコードビハインドにおける単一責任を実現するために、まずModel View Presenterデザインパターンの変形であるPassive Viewパターンを利用します。なお、Model View Presenterパターンの詳細については本稿では説明しませんので、
私が昨年書いた記事を参照してください。
最初に行うリファクタリングは、「従業員を表示する」という責任に直接関係しないコードを取り除くことです。実は、これによってコードビハインドからほぼすべてのコードを取り除くことになります。Webページの新たなコードビハインドは以下のようになります。
public partial class
ViewEmployeesWithAdditionOfPresenter : Page,
IEmployeeView
{
private ViewEmployeesPresenterLowCohesion presenter;
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
presenter = new
ViewEmployeesPresenterLowCohesion(this);
}
public IList Employees
{
set
{
this.employeesRepeater.DataSource = value;
this.employeesRepeater.DataBind();
}
}
}
リスト1と比べると、雲泥の差があるでしょう。Passive Viewパターンに不案内な方のために、ここで簡単に説明しておきます。
- ビュー実装(Webページ)は、プレゼンターによってコンシュームされるインターフェイスを実装する。
- ビューインターフェイスは、ビュー(Webページ)によって実装され、プレゼンターによってサブスクライブされるイベントを公開する。
- ビューは自分自身について発生する事象(たとえば、「ボタンがチェックされた」とか「ロードが行われている」など)への応答としてイベントを発生させる。これらのイベントはプレゼンターによって処理される。
- プレゼンターはイベントを処理する。また、ビューインターフェイスを使ってビューに情報を返すことがある。
- ビューインターフェイスを使用すると、プレゼンターは任意のUIテクノロジ(たとえば、ASP.NET)との緩やかな結合を維持できる。
リスト2に、この段階でのプレゼンタークラスを示します。
リスト2 最初のプレゼンター
public class ViewEmployeesPresenterLowCohesion
{
private IEmployeeView view;
private const string ConnectionString =
"data source=(local);Integrated Security=SSPI;" +
"Initital Catalog=NorthWind";
private SqlConnection connection;
public ViewEmployeesPresenterLowCohesion(
IEmployeeView view)
{
this.view = view;
HookupEventHandlersTo(view);
}
private void HookupEventHandlersTo(
IEmployeeView view)
{
view.Load += delegate { LoadEmployees(); };
}
private void LoadEmployees()
{
if (view.IsPostBack) return;
view.Employees = MapFrom(GetEmployees());
}
public DataTable GetEmployees()
{
using (connection = new SqlConnection(ConnectionString))
{
SqlCommand command = connection.CreateCommand();
command.CommandText = "SELECT * FROM Employees";
command.CommandType = CommandType.Text;
connection.Open();
using (SqlDataReader reader =
command.ExecuteReader(CommandBehavior.CloseConnection))
{
DataTable results = new DataTable();
results.Load(reader);
return results;
}
}
}
private IList<IEmployee>
MapFrom(DataTable employeeData)
{
List<IEmployee> employees = new List<IEmployee>();
foreach (DataRow employeeRow in employeeData.Rows)
{
employees.Add(MapFrom(employeeRow));
}
return employees;
}
private Employee MapFrom(DataRow row)
{
return new Employee(Convert. ToInt32(row["EmployeeId"]),
row["LastName"].ToString(), row["FirstName"].ToString(),
row["Title"].ToString(), Convert.ToDateTime(row["BirthDate"]),
Convert.ToDateTime(row["HireDate"]));
}
}
リスト2からわかるように、ここで行ったのは、もともとWebページのコードビハインドにあった雑多なものを別のクラスに移しただけのことです。ただ1つ注目してほしいのは、ここでビューのインターフェイスを導入することで、柔軟な階層化アーキテクチャを作成する際に重要となる「依存性逆転の原則」という概念を取り入れていることです。
依存性逆転の原則
前にも述べたとおり、今回の例ではPassive Viewパターン(具体的にはビューのインターフェイス(抽象))を導入することで、依存性逆転の原則を密かに取り入れています。ビューインターフェイスのコードは以下のようになります。
public interface IEmployeeView
{
event EventHandler Load;
bool IsPostBack { get; }
IList<IEmployee> Employees { set; }
}
とても単純ですね。なお、このビューインターフェイスにはLoadイベントが定義されていますが、Webページのコードビハインドには明示的な実装が見当たらなかったはずです。なぜなら、すべてのWebページで既に(同じデリゲートシグネチャの)Loadイベントを定義しており、これでこのインターフェイスの要件が満たされるからです。
IsPostbackプロパティについても同じことが当てはまります(こちらはPageクラスで満たされます)。それなら、なぜインターフェイスを導入するのでしょうか。その理由は、インターフェイス(または抽象)にコーディングしておけば、そのインターフェイスを実装している任意のオブジェクトにプレゼンターがアクセスできるようになるからです。WindowsフォームでもモバイルWebコントロールでもWebページでも、それがビューインターフェイスを実装してさえいればアクセス可能です。プレゼンターは常に抽象(インターフェイス)を通じてその実装にアクセスするので、実装の詳細を気にかけません。これは依存性逆転の原則の重要性をよく示しており、次のように言い表すことができます。
「高レベルのコンポーネントは低レベルのコンポーネントに依存してはならない。両方ともインターフェイスに依存すべきである」
今度は、この原則に従ってプレゼンターのコードをさらに整理することにします。
以降、Webページとそれに対応するコードビハインドに手を加えることはないので、これらは頭の隅に追いやってかまいません。これは階層化アーキテクチャのメリットであり、周囲に余計な影響を与えずにコードの大幅改良ができるという特徴をよく示しています。次はプレゼンターに目を転じ、これを「高レベル」コンポーネントとして扱うことにします。プレゼンターは現在どんな高レベルコンポーネントに依存しているのでしょうか。以下のコードから2つのヒントが得られるはずです。
using (connection = new
SqlConnection(ConnectionString))
{
SqlCommand command = connection.CreateCommand();
command.CommandText = "SELECT * FROM Employees";
command.CommandType = CommandType.Text;
connection.Open();
using (SqlDataReader
reader = command.ExecuteReader(
CommandBehavior.CloseConnection))
{
DataTable results = new DataTable();
results.Load(reader);
return results;
}
}
このコードでは、プレゼンターを
SqlConnection、
SqlCommand、
SqlDataReaderというオブジェクトに結合しています。ここで一番問題なのは、この機能はプロジェクトのプレゼンテーション層に属していないという点です。表示だけを担当すべきプレゼンテーション層を、アプリケーションへのデータ提供を行う物理データベースに結合していますが、これはやはり不必要な結合です。
どうすればこの問題に対処できるでしょうか。コンピュータサイエンスの古い格言に、「どんな問題でも間接的な層をもう1つ追加すれば解決できる」というものがあります。今回は、プロジェクトにサービス層を追加することでプレゼンターのコードを整理することにします。サービス層がどのようなものかは、
この記事に詳しく書かれています。本稿のシナリオで注目していただきたいのは、サービス層をファサードとして使用することにより、プレゼンテーション層をアプリケーション機能(本稿の例では一連の従業員データを取得すること)の実行の詳細から隔離しているという点だけです。今回の例では、依存性逆転を利用して、「すべての従業員を取得する」という機能を備えたサービス層コンポーネントに関するコントラクト(抽象)を導入します。
プレゼンターの主要な役割は、従業員のリストをビューに提供することです。プレゼンターは、このリストがどのように取得されるかは関知しません。それはサービス層の領分です(最終的にはマッパー層ですが、これについては本稿では触れません)。プレゼンターの主要な役割を確認したところで、このプレゼンターからアクセスされるサービス層コンポーネントのインターフェイスを導入します。
public interface IEmployeeTask
{
IList<IEmployee>
GetAllEmployees();
}
ご覧のように、これも単純なインターフェイスです。依存性逆転の原則に従うことで、プレゼンターはビューとサービス層に依存するようになりました。サービス層を導入する前のプレゼンターは本来の責任以上のものを抱えていましたが、この部分は最終的にサービス層へと分離されます。コントラクト(抽象)へのコーディングの利点は、プレゼンターがコントラクトにのみアクセスすればよいということです。コンパイル時にプレゼンターは、そのコントラクトが実装されているかどうかを知りません(あるいは気にかけません)。そのため、サービス層コンポーネントの実際の具象実装が存在しない段階でも、プレゼンターのコードを完成させることができます。リスト3に、下層コンポーネントへの依存を導入したリファクタリング後のプレゼンターを示します。
リスト3 サービス層インターフェイス導入後のプレゼンター
public class ViewEmployeesPresenter
{
private IEmployeeView view;
private IEmployeeTask task;
public ViewEmployeesPresenter(IEmployeeView
view, IEmployeeTask task)
{
this.view = view;
this.task = task;
HookupEventHandlersTo(view);
}
private void HookupEventHandlersTo(IEmployeeView view)
{
view.Load += delegate { LoadEmployees(); };
}
private void LoadEmployees()
{
if (view.IsPostBack) return;
view.Employees = task.GetAllEmployees();
}
}
これで見違えるほどすっきりしました。依存性逆転の原則を利用してプレゼンターをリファクタリングすることで、はるかに凝集度の高いユニットになりました。プレゼンターは、(インターフェイスを通じて)ビューにデータを提供する責任を負い、さらにコントラクト(インターフェイス)を通じてサービス層コンポーネントからデータを取得します。実は、このプレゼンターのリファクタリングにあたり、依存性逆転をさらに有効に活用するためのテクニックも密かに取り入れていました。それは
依存性注入です。
依存性注入
リスト3をよく見ると、プレゼンターのコンストラクタで非常に重要なことが行われているのに気づくでしょう。
public
ViewEmployeesPresenter(IEmployeeView
view, IEmployeeTask task)
たとえプレゼンターが自身の果たすべき凝集的な責任をいくつか持っているとしても、その依存性を把握していなければ、責任を果たすことはできません。そうした依存性は作成時にプレゼンターに与えられます。これは
コンストラクタベースの依存性注入と呼ばれるタイプの依存性注入です。
依存性注入の背後にある主要な考え方は、「オブジェクトが自らの仕事を遂行するために他の
コンポーネントに頼る場合には、そのオブジェクト自身がコンポーネントの作成責任を負うのではなく、コンポーネントを抽象という形でそのオブジェクトに
注入すべきだ」というものです。
最もよく知られたタイプの依存性注入は、
コンストラクタ注入と
セッター注入です。
コンストラクタ注入では、依存性を持つオブジェクトのインスタンスが作成されるときに、必要な依存性がすべて与えられます。そのオブジェクトインスタンスが何によって作成されるかは関係ありません。そのオブジェクトが自らの仕事を遂行するために必要なものが、インスタンス作成時にすべて与えられなければなりません。必要なものがすべて与えられなかった場合(依存性としてnullが渡された場合など)、そのオブジェクトは仕事を遂行できません。コンストラクタベースの注入の1つの短所は、オブジェクトの依存性が多いと、コンストラクタで多くの依存性を指定しなければならないので、やや扱いにくくなるかもしれないという点です(もちろん、ファクトリを使って、これをプログラマから見えなくすることも可能です)。
セッター注入では、少し違ったテクニックを使用し、ほとんど常にファクトリに頼って、オブジェクトを作成し、そこに依存性を結び付けます。セッター注入では、依存性を持つオブジェクトに特別なコンストラクタはありません。その代わり、オブジェクトのすべての依存性は、そのオブジェクトが依存性のために公開しているセッタープロパティを通じて、ファクトリによって注入されます。セッターベースの注入の短所は、ファクトリを省略した場合に、そのオブジェクトを使用するコンポーネントを、そのオブジェクトが必要とする依存性の具象実装に結合しなければならないという点です。
依存性注入の概念全体は、抽象へのプログラミングという概念にかかっています。本稿のプレゼンターは、以下のものへの依存を持っています。
- 提供された情報を表示できる「ビュー」
- プレゼンターに代わってアプリケーション機能を実行できる「サービス」
テスト容易性の観点から見ると、依存性逆転と依存性注入の両方を利用すれば、テスト容易性を高めることができ、またアプリケーションのコンポーネントどうしの結合度を下げることができます。以下のコードは、単体テストとモックオブジェクトを組み合わせて、プレゼンターが構築時にビューインターフェイスのLoadイベントをサブクスライブすることを検証する方法を示しています。
[Test]
public void ShouldSubscribeToViewEventsOnConstruction()
{
MockRepository mockery = new MockRepository();
IEmployeeView mockView = mockery.CreateMock<IEmployeeView>();
IEmployeeTask mockTask = mockery.CreateMock<IEmployeeTask>();
mockView.Load += delegate { };
LastCall.IgnoreArguments();
mockery.ReplayAll();
ViewEmployeesPresenter presenter = new
ViewEmployeesPresenter(mockView, mockTask);
mockery.VerifyAll();
}
このコードでは、Rhino MocksというモックオブジェクトフレームワークとNUnit単体テストフレームワークを使用しています(私はテスト駆動開発の実践者なので、プレゼンターの機能を試すために通常ならこのテストを先に書きます。しかし、本稿では話の焦点をはっきりさせるためにテストを後回しにしました)。このテストをパスするために必要なコードは、プレゼンタークラス自体の中にあります。
public ViewEmployeesPresenter(IEmployeeView view,
IEmployeeTask task)
{
this.view = view;
this.task = task;
HookupEventHandlersTo(view);
}
private void HookupEventHandlersTo(IEmployeeView view)
{
view.Load += delegate {
LoadEmployees(); };
}
コンストラクタが
HookupEventHandlersToメソッドを呼び出すことに注意してください。このメソッドはビューインターフェイスで定義されたLoadイベントをサブスクライブします。
メンテナンス性の観点から言えば、これでプレゼンターが構築時に必ずビューのLoadイベントをサブスクライブすることを確認できるようになりました。もちろん、実際の機能が実行されるのはビューのLoadイベントがトリガされたときです。これでプレゼンターがサービス層と通信して、全従業員のリストを要求することができます。プレゼンターは得られたものをビューに返します。依存性注入を利用することによって、このシナリオのテストを書くのが楽になりました。「実際の」オブジェクトの代わりにモックオブジェクトを使用しても、プレゼンターはやはり気がつきませんでした。というのも、プレゼンターは実装ではなく抽象に依存しているからです。
これで、プレゼンターは単に自身の依存物に基づき、依存物に対して反応を返すコンポーネントに格下げされました。つまり、プレゼンターがコンシュームするコンポーネントが、すべてのアクションが行われるべき場所になります。この後はサービス層に目を転じ、依存性逆転と依存性注入のテクニックを利用してIEmployeeTaskインターフェイスのクリーンな実装を作成する方法を示すことにします。
サービス層における依存性注入と依存性逆転
リスト4は、プレゼンターから取り除いた部分のコードです。さて、このコードはドードー鳥のように消えていく運命なのでしょうか?
リスト4 行き場のないコード
public DataTable GetEmployees()
{
using (connection = new SqlConnection(ConnectionString))
{
SqlCommand command = connection.CreateCommand();
command.CommandText = "SELECT * FROM Employees";
command.CommandType = CommandType.Text;
connection.Open();
using (SqlDataReader reader = command.ExecuteReader(
CommandBehavior.CloseConnection))
{
DataTable results = new
DataTable();
results.Load(reader);
return results;
}
}
}
private IList<IEmployee> MapFrom(DataTable employeeData)
{
List<IEmployee> employees = new List<IEmployee>();
foreach (DataRow employeeRow in employeeData.Rows)
{
employees.Add(MapFrom(employeeRow));
}
return employees;
}
private Employee MapFrom(DataRow row)
{
return new Employee(Convert.ToInt32(
row["EmployeeId"]), row["LastName"].ToString(),
row["FirstName"].ToString(), row["Title"].ToString(),
Convert.ToDateTime(row["BirthDate"]),
Convert.ToDateTime(row["HireDate"]));
}
この部分を処理するために、サービス層にIEmployeeTaskインターフェイスの実装を作成します。リスト5は私が最初に作成したサービス実装ですが、これはあまり良い方法ではありません。
リスト5 とりあえず作成したサービス層クラス
public class EmployeeTaskLowCohesionNoDependencyInversion :
IEmployeeTask
{
public const string ConnectionString =
"data source=(local);Integrated Security=SSPI;" +
"Initial Catalog=Northwind";
private SqlConnection connection;
public IList<IEmployee> GetAllEmployees()
{
return MapFrom(GetEmployees());
}
public DataTable GetEmployees()
{
using (connection = new SqlConnection(ConnectionString))
{
SqlCommand command = connection.CreateCommand();
command.CommandText = "SELECT * FROM Employees";
command.CommandType = CommandType.Text;
connection.Open();
using (SqlDataReader reader = command.ExecuteReader(
CommandBehavior.CloseConnection))
{
DataTable results = new DataTable();
results.Load(reader);
return results;
}
}
}
private IList<IEmployee> MapFrom(DataTable employeeData)
{
List<IEmployee> employees = new List<IEmployee>();
foreach (DataRow employeeRow in employeeData.Rows)
{
employees.Add(MapFrom(employeeRow));
}
return employees;
}
private Employee MapFrom(DataRow row)
{
return new Employee(Convert.ToInt32(
row["EmployeeId"]), row["LastName"].ToString(),
row["FirstName"].ToString(),row["Title"].ToString(),
Convert.ToDateTime(row["BirthDate"]),
Convert.ToDateTime(row["HireDate"]));
}
}
ユーザーインターフェイスとプレゼンターがすっきりしたのはいいのですが、問題をサービス層にシフトさせただけで、同じ過ちを繰り返したのでは、そもそも今回のプロジェクト改善に取り組み始めた意味がありません。依存性逆転と依存性注入という新しい知識を踏まえて、今度は抽象ではなく実装への依存を整理することにしましょう。まずは新しいインターフェイスを導入します。
public interface IConnectionFactory
{
IDbConnection Create();
}
IConnectionFactoryインターフェイスを作成したので、依存性注入を使用して、サービス層クラスがこのIConnectionFactoryを使って構築されるようにします。
public EmployeeTaskLowCohesion
NoDependencyInversion(
ConnectionFactory connectionFactory)
{
this.connectionFactory =
connectionFactory;
}
このようなわずかな変更により、
GetEmployeesメソッドのコードを以下のように変更することが可能になります。
public DataTable GetEmployees()
{
using (IDbConnection connection =
connectionFactory.Create())
{
using (IDbCommand command =
connection.CreateCommand())
{
command.CommandText =
"SELECT * FROM Employees";
command.CommandType = CommandType.Text;
connection.Open();
using (IDataReader reader =
command.ExecuteReader(
CommandBehavior.CloseConnection))
{
DataTable results = new DataTable();
results.Load(reader);
return results;
}
}
}
}
もはや
GetEmployeesメソッドが明示的なSqlClient実装にコーディングされていないことに注意してください。現在の
GetEmployeesメソッドは、System.Data名前空間で定義されている抽象を使用しており、ほんの少し手間をかけるだけで、このコンポーネントで任意のデータベースをシームレスに扱うことが可能になっています。違いがあるとすれば、特定のデータベース(Oracle、SQL、MySQLなど)への接続を作成するためにプラグインしなければならないIConnectionFactoryの実装だけです。このコーディングスタイルのもう1つの副産物は、実際のデータベースを指定しなくても、このクラスの動作を簡単にテストできることです。
では、IEmployeeTaskの現在の実装はもう不要になったのでしょうか? 決してそんなことはありません。私の考えでは、現在の実装には他のオブジェクト(あるいはまったく異なる層)に移すべき責任がまだ数多く残っています。本稿の残りの部分では、抽象をこれ以上取り上げるのはやめて、これらの依存物を互いに結び付けることについて論じることにします。私が実際にサービス層クラスをどのように整理したかと、さまざまな責任をどのような層に分割したかについては、本稿のサンプルソースコードを参照してください。
全体の組み立て
これらの抽象と依存があちこちに注入されるわけですが、「実際に依存を注入するのは誰か」と疑問に思う人もいるでしょう。ここでプレゼンターのコードと、対応するWebページをもう一度見てみましょう。プレゼンターのコンストラクタを思い出してください。
public ViewEmployeesPresenter(
IEmployeeView view, IEmployeeTask task)
プレゼンターを構築するには、プレゼンターが情報を返すビューと、プレゼンターが情報を要求するサービス層の両方を与える必要があります。ここで問題が起こります。なぜなら、Webページのコードビハインドで、そのページに関するプレゼンターをインスタンス化する必要があるからです(他のソリューションもありますが、本稿の趣旨から外れるので、そうしたものには触れません)。
プレゼンターがビューへの依存を持っているだけなら次の行で用が足りるのですが、あいにくそうではありません。プレゼンターは作成時にビューとサービス層の両方の依存を持たねばなりません。
presenter = new ViewEmployeesPresenter(this);
この問題を解決する簡単な方法として考えられるのは、Webプロジェクトからサービス層プロジェクトへの参照を追加し、Webページのコードビハインドで次のコードを実行することです。
presenter = new ViewEmployeesPresenter(
this, new EmployeeTask());
残念ながら、こうするとビューはプレゼンターを作成する責任を負うだけでなく、プレゼンターに必要な依存物を作成して、そこに結合するという処理も担当しなければなりません。そうなると、そもそもこの道を選んだ意味がなくなりそうです。
この問題を解決するには、非常に単純で結合度の高い方法から、もっと複雑で結合度の低い方法まで、数多くの方法が考えられます。手始めに非常に単純なアプローチを見てみましょう。
まず、プレゼンターを構築するためにWebページで行う処理は次の1行だけにしたいと思います。
presenter = new ViewEmployeesPresenter(this);
これを実現するには、コンストラクタチェーンを利用して、ビューがコンシュームできる単純なコンストラクタを提供します。この「簡易版」コンストラクタは、必要なものを提供してくれる「完全版」コンストラクタ(すべての依存を必要とするコンストラクタ)を呼び出します。
public ViewEmployeesPresenter(
IEmployeeView view):this(view,
new EmployeeTask())
{
}
public ViewEmployeesPresenter(
IEmployeeView view,
IEmployeeTask task)
これでビューは、プレゼンターが必要とする一切の依存(それ自身を除く)について何も知らなくて済みます。これは単純なソリューションであり、プレゼンターと1つの依存物の特定の実装とを結合することになります。しかし、この結合はそれほど恐れるようなものではありません。この結合が発生するのは「簡易版」コンストラクタの中だけだからです。プレゼンター内の他の部分では、常にインターフェイスを通じて自分の依存物とやり取りします。ここで結合が生じるのは、プレゼンターが自らの依存物(IEmployeeTask実装)を作成する責任を負うようになったからです。もちろん、実利的な立場から言えば、もしも読者が依存性注入と依存性逆転という概念を自分自身のアプリケーションに導入しようとするならば、このようにほどほどの結合度を持つやり方から始めるのがよいでしょう。
しかし、依存を持つクラスを依存物の実装にまったく結合しないようにしたい場合は、
サービスロケータの機能を利用します。
サービスロケータによる依存性注入
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実装の例です。
リスト6 独自のサービスロケータの実装
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によって開発されました。これを使用すると、アプリケーションの外部に格納されたコンフィグレーションを使って依存を設定することができます。
まとめ
本稿では多くの問題を論じました。初めに、よく知られている多くのベストプラクティスに反する単層アプリケーションを取り上げ、階層化アーキテクチャといくつかのリファクタリング手法、さらに依存性逆転の原則について説明しました。実装へのコーディングに比べて抽象へのコーディングを行った方が有利である理由についても触れました。さらに、抽象へのコーディングの利点をよりよく理解していただくために、依存性注入とサービスロケータという概念を紹介しました。
本稿で紹介した内容が、読者の皆さんのアプリケーションアーキテクチャでより緩やかに結合されたソリューションを実現するためのお役に立てば幸いです。
編集者注
この記事はもともと
CoDe MagazineのMay/June 2007年5-6月号に掲載されたものですが、許可を得てここに転載しました。
Jean-Paul S. Boodhoo(Jean-Paul S. Boodhoo)