疎結合
ソフトウェアの依存関係を緩和してアプリケーションの柔軟性を高める
James Kovacs
この記事の内容 : :
- 密結合アーキテクチャの短所
- テストと依存関係の問題
- 依存関係の逆転
- 依存関係の注入
|
この記事は次のテクノロジを使用しています:
.NET Framework
|
コードのダウンロード : :
DependencyInjection2008_03.exe
(5408 KB)
Browse the Code Online

コンテンツ
疎結合設計を目指すのは好ましくないということに異論を挟む人は少ないと思います。しかしながら、一般に、設計は意図したよりはるかに密接に結合されます。設計が密結合しているかどうかはどうやって確認したらよいでしょうか。NDepend などの静的分析ツールを使用して依存関係を分析することもできますが、アプリケーションの結合を感知する最も簡単な方法は、クラスのうちの 1 つを単独でインスタンス化してみることです。
ビジネス層から InvoiceService などのクラスを選択し、そのコードを新しいコンソール プロジェクトにコピーするだけです。これをコンパイルしてみてください。Invoice や InvoiceValidator などの一部の依存関係が失われる可能性があります。これらのクラスをコンソール プロジェクトにコピーして再度コンパイルを試行してください。他にも不足しているクラスが見つかる可能性があります。最終的にコンパイルが成功すると、コードベースの大部分が新しいプロジェクトに含まれている可能性もあります。それは、まるでセーターのほつれた糸がほどけていくのを眺めているようなものです。設計上のすべてのクラスが他のすべてのクラスに直接または間接に結合されています。このようなシステムの変更は、1 つのクラスの変更がさざなみのようにシステムのその他の全体に影響する可能性があるため、非常に困難です。
重要なことは、結合を完全に回避することではありません。そんなことは不可能です。たとえば、次のような場合を考えてみましょう。
string name = "James Kovacs";
これは、自作のコードを Microsoft® .NET Framework の System.String クラスに結合したものです。何か問題があるでしょうか。筆者は問題ないと考えています。System.String が好ましくない方法で変更されたり、System.String の操作方法の変更が必要になるような要件の変更が発生したりする可能性はきわめて低いと考えられます。したがって、この結合に不満はありません。重要なのは、結合をなくすことではなく、結合を意識して賢明に選択することです。
もう 1 つの例として、多くのアプリケーションのデータ層で見られる次のようなコードを検討してみましょう。
SqlConnection conn = new SqlConnection(connectionString);
また、次のようなコードも考えられます。
XmlDocument settings = new XmlDocument();
settings.Load("Settings.xml");
データ層で SQL Server® のみにアクセスするか、それとも常に XML ドキュメント Settings.xml からアプリケーション設定を読み込むようにするかについてどの程度自信を持っていますか。ここでの目的は、無限に拡張可能できわめて複雑な、実用に耐えない汎用的なフレームワークを構築することではありません。重要なことは可逆性です。設計上の決定をどの程度簡単に変更できるでしょうか。アプリケーション アーキテクチャは変更への対応力に優れていますか。
変更にこだわる理由とはなんでしょうか。それは、このシステム産業で唯一不変なものが変化だからです。要件、テクノロジ、開発者、ビジネスはいずれも変化します。これらの変化に対応する準備はできていますか。疎結合設計により、ソフトウェアは不可避で予測のつかないことが多い変化に対応しやすくなります。
内部依存関係の問題
まず、平均的な階層型アプリケーション アーキテクチャに見られる一般的な、堅く結合された設計を検討していきます (図 1 を参照)。単純な階層化スキームでは UI 層がサービス (またはビジネス) 層にアクセスし、サービス (またはビジネス) 層がリポジトリ (またはデータ) 層にアクセスします。これらの階層間の依存関係は同様の方法で下位に移動します。リポジトリ層はサービス層を意識せず、サービス層は UI 層を意識しません。
Figure 1 一般的な階層型アーキテクチャ
前述の階層以外にプレゼンテーション層やワークフロー層などの階層が存在する場合もありますが、下位層のみを意識するという階層パターンはかなり一般的です。階層を一貫した役割分担のクラスタとして扱う設計手法は優れています。ただし、上位層から下位層に直接結合すると結合の数が増え、アプリケーションのテストが難しくなります。
テストの容易性に注目する理由はなんでしょうか。それは、結合の優れたバロメーターになるためです。テスト時にクラスを簡単にインスタンス化できない場合は結合の問題が存在します。たとえば、サービス層はリポジトリ層と密接に関連し、リポジトリ層に依存します。サービス層をリポジトリ層から切り離してテストすることはできません。そのため、実用上、多くのテストでは基盤となるデータベース、ファイル システム、またはネットワークにアクセスします。これにより、テストに時間がかかる、保守コストが高くつくなどのさまざまな問題が生じます。
テストに時間がかかる テストを厳密にメモリ内で実行できれば、1 テストあたりミリ秒の範囲で完了可能です。テストでデータベース、ファイル システム、ネットワークなどの外部リソースにアクセスすると、多くの場合、1 テストあたり 100 ミリ秒以上かかります。テスト カバレッジが十分なプロジェクトであれば、一般にテスト項目は数百~数千件に及ぶということを考えると、テストの実行時間は数秒で終わるか、数分ないし数時間かかるかの違いになります。
エラーの切り離しが不十分 データ層コンポーネントでエラーが発生すると、多くの場合、上位層コンポーネントのテストもエラーになります。数件のテスト エラーで済めば問題を即座に切り離すことが可能ですが、エラーが数百件も発生すれば問題の特定が難しくなり、時間もかかります。
保守コストが高い 多くのテストには初期データが必要です。テストでデータベースにアクセスする場合は、各テストの前にデータベースが既知の状態になっていることを確認する必要があります。また、各テストの初期データが他のテストの初期データから独立していることも確認する必要があります。そうしないと、順序が変わると一定のテストが失敗するというテスト順序の問題に直面することになります。データベースを既知の適切な状態に保つ操作は手間がかかり、間違いやすい作業です。
また、下位層の実装を変更する必要がある場合、上位層が下位層に暗黙的または明示的に依存していることが多いため、上位層も変更せざるを得なくなります。アプリケーションを階層化しても、疎結合は実現していません。
具体的な例として、請求書を受領するサービスを見ていきましょう (図 2 を参照)。InvoiceService.Submit は、請求書の提出を受け入れることができるようにするため、クラスのコンストラクタで作成された AuthorizationService、InvoiceValidator、および InvoiceRepository に依存しています。InvoiceService の単体テストを行うには、具象的な依存関係が必要です。そのため、単体テストを実行する前にデータベースの状態を確認し、InvoiceRepository が新しい請求書を挿入したときに主キーまたは一意キーの違反が発生しないこと、および InvoiceValidator で検証エラーが通知されないことを確かめる必要があります。また、単体テストを実行するユーザーに適切なアクセス許可があり、AuthorizationService が "送信" 操作を許可するようになっていることも確認する必要があります。

Figure 2 Invoice Service
public class InvoiceService {
private readonly AuthorizationService authoriazationService;
private readonly InvoiceValidator invoiceValidator;
private readonly InvoiceRepository invoiceRepository;
public InvoiceService() {
authoriazationService = new AuthorizationService();
invoiceValidator = new InvoiceValidator();
invoiceRepository = new InvoiceRepository();
}
public ValidationResults Submit(Invoice invoice) {
ValidationResults results;
CheckPermissions(invoice, InvoiceAction.Submit);
results = ValidateInvoice(invoice);
SaveInvoice(invoice);
return results;
}
private void CheckPermissions(Invoice invoice, InvoiceAction action) {
if(authoriazationService.IsActionAllowed(invoice, action) == false) {
throw new SecurityException(
"Insufficient permissions to submit this invoice");
}
}
private ValidationResults ValidateInvoice(Invoice invoice) {
return invoiceValidator.Validate(invoice);
}
private void SaveInvoice(Invoice invoice) {
invoiceRepository.Save(invoice);
}
}
これは無理難題です。依存関係があるこれらのコンポーネントのいずれかにコード エラーまたはデータ エラーを引き起こすような問題があれば、InvoiceService のテストで予期しない失敗が起こります。テストが成功しても、データベースに適切なデータを設定し、テストを実行し、テストによって作成されたデータをクリーンアップするまでの総実行時間は数百ミリ秒かかります。テストをバッチにグループ化し、バッチの前後にスクリプトを実行するようにして設定およびクリーンアップにかかる時間を削減しても、メモリ内でテストを実行した場合より実行時間ははるかに長くなります。
また、さらに微妙な問題もあります。それは、InvoiceRepository の監査サポートを追加する場合に AuditingInvoiceRepository を作成するか、InvoiceRepository 自体を変更せざるを得なくなるということです。InvoiceService とその子コンポーネントが結合しているため、新しい機能をシステムに導入するためのオプションは限られます。
依存関係の逆転
上位コンポーネント InvoiceService を下位の依存関係から切り離すには、具象クラスではなく、インターフェイスを使用して依存関係を操作します。
public class InvoiceService : IInvoiceService {
private readonly IAuthorizationService authService;
private readonly IInvoiceValidator invoiceValidator;
private readonly IInvoiceRepository invoiceRepository;
...
}
インターフェイス (または抽象基本クラス) を使用するようにしたこの単純な変更により、任意の依存関係の代替実装を置き換えることができます。InvoiceRepository を作成する代わりに AuditingInvoiceRepository を作成することができます (AuditingInvoiceRepository が IInvoiceRepository を実装していることが前提です)。また、テスト時にフェイクやモックを置き換えることもできます。この設計手法をコントラクトに対するプログラミングと呼びます。
この記事で上位コンポーネントと下位コンポーネントの切り離しに適用している原則を依存関係の逆転原則と呼びます。Robert C. Martin もこのトピックに関する記事 (
objectmentor.com/resources/articles/dip.pdf) の中でこう指摘しています。"上位モジュールが下位モジュールに依存することは好ましくありません。上位モジュールも下位モジュールも抽象化に依存するようにする必要があります"。
この例の場合、InvoiceService および InvoiceRepository は IInvoiceRepository による抽象化に依存するようになりました。ただし、これで問題が完全に解決したわけではありません。まだ問題を移行しただけです。具象実装がインターフェイスに依存するようになっただけで、具象クラスがどのような方法で相互を "認識" するかという問題が残っています。
InvoiceService にはまだ依存関係の具象的な実装が必要です。単純に InvoiceService のコンストラクタでこれらの依存関係をインスタンス化することもできますが、それでは以前の状態から改善されません。AuditingInvoiceRepository を使用するとしても、相変わらず InvoiceService を変更して AuditingInvoiceRepository をインスタンス化する必要があります。また、IInvoiceRepository に依存するすべてのクラスを変更して、AuditingInvoiceRepository をインスタンス化する必要があります。InvoiceRepository と AuditingInvoiceRepository をグローバルに置き換える簡単な方法はありません。
1 つの解決策として、IInvoiceRepository インスタンスの作成にファクトリを使用する方法があります。これにより、ファクトリ メソッドを変更するだけで、1 か所で AuditingInvoiceRepository に切り替えることができます。この方法をサービス ロケーションとも呼び、インスタンスを管理するファクトリ クラスをサービス ロケータと呼びます。
public InvoiceService() {
this.authorizationService =
ServiceLocator.Find<IAuthorizationService>();
this.invoiceValidator = ServiceLocator.Find<IInvoiceValidator>();
this.invoiceRepository = ServiceLocator.Find<IInvoiceRepository>();
}
ServiceLocator 内の機能には、構成ファイルまたはデータベースから読み取ったデータを使用するか、コードに直接接続することができます。どちらの場合にも依存関係のオブジェクトは集中的に作成されます。
切り離されたコンポーネントを単体テストするには、実際の実装ではなく、フェイクまたはモック オブジェクトを使用してサービス ロケータを構成します。そこで、たとえば、テスト時に ServiceLocator.Find<IInvoiceRepository> が FakeInvoiceRepository を返す場合があります。FakeInvoiceRepository は、請求書の保存時に既知の主キーを割り当てますが、請求書を実際にデータベースに保存するわけではありません。複雑な設定やデータベースの破棄を不要にし、フェイク依存関係から既知のデータを返すことができます (補足記事「依存関係のフェイク化は有効か」を参照)。
ただし、サービス ロケーションにはいくつかの短所があります。まず、依存関係が上位クラスに隠蔽されています。InvoiceService が AuthorizationService、InvoiceValidator、または InvoiceRepository に依存していることは公開署名からはわかりません。コードを確認する必要があります。
同じインターフェイスの別の具象タイプを指定する必要がある場合、オーバーロードされた Find メソッドを使用する必要があります。この場合、ファクトリ クラスの実装時に代替型を必須にするかどうかの選択が必要です。たとえば、特定の IInvoiceRepository の要求の展開時に ServiceLocator を再構成して AuditingInvoiceRepository を置き換えることはできません。このような短所はありますが、サービス ロケーションはわかりやすく、依存関係をハードコーディングするよりは優れた方法です。
依存関係の注入
上位コンポーネントの単体テストを行う場合に、依存関係のフェイクまたはモック実装を使用する必要があります。このとき、サービス ロケータにフェイクまたはモックを構成して上位コンポーネントで検索するのではなく、パラメータ化したコンストラクタで依存関係を上位コンポーネントに直接渡すだけで済みます。この方法を依存関係の注入と呼びます。図 3 に例を示します。

Figure 3 Dependency Injection
[Test]
public void CanSubmitNewInvoice() {
Invoice invoice = new Invoice();
ValidationResults validationResults = new ValidationResults();
IAuthorizationService authorizationService =
mockery.CreateMock<IAuthorizationService>();
IInvoiceValidator invoiceValidator =
mockery.CreateMock<IInvoiceValidator>();
IInvoiceRepository invoiceRepository =
mockery.CreateMock<IInvoiceRepository>();
using(mockery.Record()) {
Expect.Call(authorizationService.IsActionAllowed(
invoice, InvoiceAction.Submit)).Return(true);
Expect.Call(invoiceValidator.Validate(invoice))
.Return(validationResults);
invoiceRepository.Save(invoice);
}
using(mockery.Playback()) {
IInvoiceService service = new InvoiceService(authorizationService,
invoiceValidator, invoiceRepository);
service.Submit(invoice);
}
}
この例では、InvoiceService の依存関係のモック オブジェクトを作成し、InvoiceService コンストラクタに渡します。モック オブジェクト フレームワークの詳細については、Mark Seemann の「単体テスト : テスト代替の連続性について検討する」(
msdn.microsoft.com/msdnmag/issues/07/09/MockTesting) を参照してください。要するに、テストの実行後に InvoiceService の状態を確認するのではなく、モックの操作方法を定義することによって InvoiceService の動作を指定します。
依存関係の注入を使用することにより、単体テストで上位コンポーネントに簡単に依存関係を指定できます。ただし、アプリケーションの実行時や統合テスト時に、単体テストの外部でクラスの依存関係をどうやって見つけるかという問題が残ります。UI 層がサービス層に依存関係を指定したり、サービス層がリポジトリ層に依存関係を指定することを期待するのは不合理です。そんなことをしたら、問題をさらに悪化させる結果になります。それでも、UI 層がサービス層に依存関係を指定すると仮定してみましょう。
// Somewhere in UI Layer
InvoiceSubmissionPresenter presenter =
new InvoiceSubmissionPresenter(
new InvoiceService(
new AuthorizationService(),
new InvoiceValidator(),
new InvoiceRepository()));
おわかりのように、UI は固有の依存関係だけでなく、その依存関係に対する依存関係もデータ層に至るまで無限に認識する必要があります。これは明らかに望ましい状態ではありません。この問題を解決する一番簡単な方法は、"貧者のための" 依存関係の注入を呼び出すことです。
貧者のための依存関係の注入では、次のように、上位コンポーネントの既定のコンストラクタを使用して依存関係を指定します。
public InvoiceService() :
this(new AuthorizationService(),
new InvoiceValidator(),
new InvoiceRepository()) { }
最もオーバーロードされたコンストラクタにデリゲートする方法に注目してください。これにより、インスタンスの作成にどのコンストラクタを使用しても、クラスの初期化ロジックが同一になります。クラスを具象的な依存関係に結合する処理は、既定のコンストラクタ 1 か所で行われます。単体テスト時にクラスの依存関係を指定可能なオーバーロードされたコンストラクタがまだ存在するため、クラスは引き続きテストできます。
コンテナ
次に、依存関係を集中管理する場所を提供する、制御の反転 (IoC: inversion of control) コンテナについて説明します。コンテナは、実際にはインターフェイスの実装タイプに対する便利な辞書にすぎません。最も単純な形式では、IoC コンテナはサービス ロケータの単なる別称です。コンテナが単なるサービス ロケーションと比べてどれほど多くの機能を持っているかについては後述します。
当面の問題に戻って、InvoiceService をその依存関係の具象実装から完全に切り離すとします。ソフトウェアのあらゆる問題と同様、間接的な層を追加することで問題を解決できます。依存関係リゾルバの概念を導入し、インターフェイスを具象実装にマップします。次に、インターフェイス T を受け取る汎用メソッドを使用して、そのインターフェイスを実装する型を取得します。
public interface IDependencyResolver {
T Resolve<T>();
}
では、辞書を使用してインターフェイスとそのインターフェイスを実装するオブジェクトとのマッピング情報を保存する SimpleDependencyResolver を実装してみましょう。最初に辞書を設定する手段が必要です。その処理は Register<T>(object obj) メソッドで行います (図 4 を参照)。依存関係を登録するのは SimpleDependencyResolver の作成者のみであるため、Register メソッドは IDependencyResolver インターフェイス上に存在する必要はありません。通常、この処理はアプリケーションの起動時に Main メソッド内で呼び出されるヘルパ クラスで行います。

Figure 4 SimpleDependencyResolver
public class SimpleDependencyResolver : IDependencyResolver
{
private readonly Dictionary<Type, object> m_Types =
new Dictionary<Type, object>();
public T Resolve<T>() {
return (T)m_Types[typeof(T)];
}
public void Register<T>(object obj) {
if(obj is T == false) {
throw new InvalidOperationException(
string.Format("The supplied instance does not implement {0}",
typeof(T).FullName));
}
m_Types.Add(typeof(T), obj);
}
}
CompanyService で SimpleDependencyResolver を検索してその依存関係を見つけるにはどうしたらよいでしょうか。IDependencyResolver を必要とする各クラスに渡すことも可能ですが、この方法はすぐに煩雑になります。最も簡単な解決法は、構成済みの SimpleDependencyResolver インスタンスをグローバルにアクセス可能な場所に配置することです。静的ゲートウェイ パターンを使用すればそれが可能です (シングルトン パターンを使用することも可能ですが、シングルトンはテストが難しいことで知られています。シングルトンは見かけ上のグローバル変数程度の役割しか果たさないため、密結合コードのテストが難しい最大の原因の 1 つです。なるべく使用を避けてください)。
では、静的ゲートウェイの説明に移ります。この静的ゲートウェイを IoC と呼びます。DependencyResolver という名前でもよいのですが、IoC の方が簡単です。IoC 上の静的メソッドは IDependencyResolver 上のメソッドと一致します (静的クラスでインターフェイスを実装することはできないため、IoC は IDependencyResolver を実装しません)。実際の IDependencyResolver を受け取る Initialize メソッドもあります。IoC 静的ゲートウェイは単純にすべての Resolve<T> 要求を構成済み IDependencyResolver に転送します。
public class IoC {
private static IDependencyResolver s_Inner;
public static void Initialize(IDependencyResolver resolver) {
s_Inner = resolver;
}
public static T Resolve<T>() {
return s_Inner.Resolve<T>();
}
}
アプリケーションの起動時に、IoC を構成済みの SimpleDependencyResolver で初期化します。これで、既定のコンストラクタ内で貧者のための依存性の注入を IoC.Resolve に置き換えることができます。
public InvoiceService() :
this(IoC.Resolve<IAuthorizationService>(),
IoC.Resolve<IInvoiceValidator>(),
IoC.Resolve<IInvoiceRepository>()) { }
アプリケーションの起動後は読み取り専用で、更新されることはないため、内部 IDependencyResolver へのアクセスを同期する必要はありません。
IoC クラスには、アプリケーション内で破壊防止層の役割を果たすという利点もあります。異なる IoC コンテナを使用する場合、IDependencyResolver を実装するアダプタを実装する必要があります。IoC をアプリケーション全体で広範囲に使用する場合でも、特定のコンテナには結合していません。
完全な IoC コンテナ
SimpleDependencyResolver などの単純な IoC コンテナでは、疎結合コンポーネントを連結できます。ただし、完全な IoC コンテナには以下をはじめとする多くの機能が不足しています。
- XML、コード、スクリプトなどのより多様な構成オプション
- シングルトン、一時的、スレッド単位、プール済みなどの有効期間管理
- 依存関係の自動接続
- 新しい機能に接続する機能
これらの機能の詳細について説明します。具体的な例として、一般的なオープン ソース IoC コンテナ、Castle Windsor を使用します。多くのコンテナは外部 XML ファイルで構成できます。たとえば、Windsor は次のように構成することが可能です。
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<components>
<component id="Foo"
service="JamesKovacs.IoCArticle.IFoo, JamesKovacs.IoCArticle"
type="JamesKovacs.IoCArticle.Foo, JamesKovacs.IoCArticle"/>
</components>
</configuration>
XML 構成はアプリケーションを再コンパイルせずに変更可能であるという利点がありますが、変更を適用するには、多くの場合にアプリケーションの再起動が必要です。XML 構成の短所はこれだけではなく、冗長な傾向がある、実行するまでエラーが検出されない、汎用型を一般的な C# の汎用表記ではなく、CLR のバッククォート表記で宣言する、などの問題もあります (Company.Application.IValidatorOf<Invoice> は Company.Application.IValidatorOf`1[[Company.Application.Invoice, Company.Application]], Company.Application と記述されます)。
XML 以外に、Windsor を C# などの Microsoft .NET Framework 準拠言語で構成することもできます。構成のコードを独立したアセンブリに切り離してある場合、構成を変更後、構成アセンブリを再コンパイルしてアプリケーションを再起動するだけで済みます。
Binsor を使用して Windsor の構成スクリプトを作成できます。Binsor は、Windsor の構成専用のドメイン固有言語 (DSL) です。Binsor を使用して構成ファイルを Boo で作成することもできます (Boo は、言語やコンパイラの拡張を重視した、静的に型指定された CLR 言語であるため、DSL の作成に適しています)。Binsor で前述の XML 構成ファイルを次のように変更可能です。
import JamesKovacs.IoCArticle
Component("Foo", IFoo, Foo)
Boo が本格的なプログラミング言語であることを理解すると、さらに関心が深まります。Binsor を使用すれば、Windsor の型を自動登録でき、XML ベースの構成のようにコンポーネントの登録を手動で追加する必要がありません。
import System.Reflection
serviceAssembly = Assembly.Load("JamesKovacs.IoCArticle.IoCContainer")
for type in serviceAssembly.GetTypes():
continue if type.IsInterface or type.IsAbstract or
type.GetInterfaces().Length == 0
Component(type.FullName, type.GetInterfaces()[0], type)
Boo に詳しくない場合でも、このコードの目的は理解する必要があります。新しいサービスを JamesKovacs.IoCArticle.Services 名前空間に追加するだけで、サービスはサービス インターフェイスの既定の実装として自動的に登録されます。たとえば、次のようなクラスを作成します。
public class AuthorizationService : IAuthorizationService {
...
}
その他のクラスが IAuthorizationService への依存関係をコンストラクタに渡すパラメータとして追加することによって宣言する場合、Binsor は依存関係を自動的に接続するため、構成ファイルにその依存関係を明示的に指定する必要はありません。Binsor の詳細については、
ayende.com/Blog/category/451.aspx を参照してください。また、Boo の詳細については、
boo.codehaus.org を参照してください。
有効期間管理
SimpleDependencyResolver は常にインターフェイスに対して登録された同一インスタンスを返すため、事実上そのインスタンスはシングルトンになります。SimpleDependencyResolver を変更して、インスタンスではなく、具象型を登録することができます。その後、別のファクトリを使用して具象型のインスタンスを作成できます。シングルトン ファクトリは常に同一のインスタンスを返します。一時ファクトリは常に新しいインスタンスを返します。スレッド単位ファクトリは要求するスレッドごとに 1 つのインスタンスを保持します。
インスタンスの作成方法を制約するものは作成者の発想力のみです。その発想を Windsor が提供しています。XML 構成ファイルに属性を適用することにより、特定の具象型のインスタンスを作成するために使用するファクトリの種類を変更できます。既定では、Windsor はシングルトン インスタンスを使用します。IFoo がコンテナから要求されるたびに新しい Foo を返すようにする場合は、構成を次のように変更するだけです。
<component id="Foo"
service="JamesKovacs.IoCArticle.IFoo, JamesKovacs.IoCArticle"
type="JamesKovacs.IoCArticle.Foo, JamesKovacs.IoCArticle"
lifestyle="transient"/>
依存関係の自動接続
依存関係の自動接続とは、コンテナが要求されたタイプの依存関係を確認し、その依存関係を作成することであり、開発者が既定のコンストラクタを指定する必要はありません。
public InvoiceService(IAuthorizationService authorizationService,
IInvoiceValidator invoiceValidator,
IInvoiceRepository invoiceRepository) {
...
}
クライアントがコンテナから IInvoiceService を要求すると、コンテナは、具象型に IAuthorizationService、IInvoiceValidator、および IInvoiceRepository の具象実装が必要であることを認識します。コンテナは適切な具象型を検索し、存在する可能性がある依存関係を解決し、その依存関係を作成します。次に、これらの依存関係を使用して InvoiceService を作成します。自動接続では既定コンストラクタを保持する必要がないため、コードが簡素化され、多くのクラスの IoC 静的ゲートウェイへの依存関係がなくなります。
具象実装ではなく、コントラクトにコーディングし、コンテナを使用することにより、アーキテクチャの柔軟性がはるかに高まり、変更に対応しやすくなります。InvoiceRepository の構成可能な監査ログを実装するにはどうしたらよいでしょうか。密結合アーキテクチャでは、InvoiceRepository を変更する必要があります。また、監査ログを有効にするかどうかを指定するためにアプリケーションの構成を設定する必要があります。
では、疎結合アーキテクチャでは、より優れた方法があるでしょうか。IInvoiceRepository を実装する AuditingInvoiceRepositoryAuditor を実装することができます。監査担当者は監査機能のみを実装し、コンストラクタに指定された実際の InvoiceRepository にデリゲートします。このパターンをデコレータと呼びます (図 5 を参照)。

Figure 5 Using the Decorator Pattern
public class AuditingInvoiceRepository : IInvoiceRepository {
private readonly IInvoiceRepository invoiceRepository;
private readonly IAuditWriter auditWriter;
public AuditingInvoiceRepository(IInvoiceRepository invoiceRepository,
IAuditWriter auditWriter) {
this.invoiceRepository = invoiceRepository;
this.auditWriter = auditWriter;
}
public void Save(Invoice invoice) {
auditWriter.WriteEntry("Invoice was written by a user.");
invoiceRepository.Save(invoice);
}
}
監査を有効にするには、IInvoiceRepository に対する要求が行われた場合に AuditingInvoiceRepository で修飾された InvoiceRepository を返すようにコンテナを構成します。クライアントは相変わらず IInvoiceRepository にアクセスしています。この方法には次のような利点があります。
- InvoiceRepository は変更されていないため、コードが破壊される可能性はありません。
- AuditingInvoiceRepository を実装し、InvoiceRepository から切り離してテストすることができます。これにより、実際にデータベースが存在するかどうかに左右されずに監査を適切に行うことができます。
- InvoiceRepository を複雑化させずに監査、セキュリティ、キャッシュなどの用途で複数のデコレータを作成できます。つまり、疎結合システムのデコレータ方式は、新しい機能を追加する際の拡張性が優れています。
- コンテナは、注目に値するアプリケーションの拡張性メカニズムです。AuditingInvoiceRepository を InvoiceRepository や IInvoiceRepository と同じアセンブリに実装しなければならない理由はありません。構成ファイルで参照されるサードパーティ製のアセンブリにも簡単に実装可能です。
結合を緩和して変更に対応する
ソフトウェア アーキテクチャが階層化されていても、階層間の密結合によりアプリケーションのテストや進化が妨げられる場合があります。ただし、この設計の結合は切り離すことが可能です。依存関係の反転および依存関係の注入を使用して、具象実装ではなく、コントラクトにコーディングする利点を得ることができます。コントロール コンテナの反転を導入することにより、アーキテクチャの柔軟性が向上します。結論として、疎結合設計の方が変更への対応力に優れています。
James Kovacs は、アルバータ州カルガリー在住のフリー設計者、開発者、兼トレーナーの、いわゆる何でも屋です。.NET Framework を使用したアジャイル開発を専門にしています。ソリューション アーキテクチャの Microsoft MVP でもあり、ハーバード大学で修士号を取得しました。James の連絡先は、
jkovacs@post.harvard.edu または
www.jameskovacs.com です。