アスペクト指向プログラミング、インターセプト、および Unity 2.0
Dino Esposito
オブジェクト指向がプログラミングの主流パラダイムの 1 つであり、システムをコンポーネントに分解し、そのコンポーネントを通じてプロセスを表現することにかけては卓越していることに疑問の余地はありません。オブジェクト指向 (OO) パラダイムは、あるコンポーネントのビジネス固有の問題を処理する場合にも卓越しています。しかし、コンポーネント間の横断的問題の処理に関しては、OO パラダイムはそれほど効果的ではありません。一般に、横断的問題とは、あるシステムの複数のコンポーネントに影響を与える問題のことです。
複雑なビジネス ロジック コードを最大限に再利用するため、通常は、システムの中核となる主要ビジネス機能を基盤としてクラス階層を設計する傾向があります。ですが、ビジネス固有ではなく、クラスの階層を横断する他の問題はどうなるでしょう。キャッシュ、セキュリティ、ログ記録などの機能はどこに入れればよいのでしょう。おそらく、こうした機能は結局、影響を受けるすべてのオブジェクトで繰り返されることになります。
横断的問題は、特定のコンポーネントやコンポーネント ファミリ固有の責任ではなく、アプリケーション クラスを越えた異なる論理レベルで処理する必要がある、システムの側面 (アスペクト) です。このような理由から、数年前に、アスペクト指向プログラミング (AOP) という別のプログラミング パラダイムが定義されました。ちなみに、AOP の概念は 1990 年代に Xerox の PARC 研究所で生み出されました。PARC のチームは、最初の (そしていまだに最も人気のある) AOP 言語である AspectJ も生み出しました。
AOP の利点についてはほぼすべての人が同意するにもかかわらず、AOP はまだ広く実装されてはいません。私見では、採用する人が限られている主な理由は、基本的に、適切なツールが存在しないことです。AOP が (部分的にでも) Microsoft .NET Framework でネイティブにサポートされる日が来れば、その日は AOP の歴史において重大な分岐点となると確信しています。現在、.NET で AOP を行うには、一時的なフレームワークを使用するしか方法はありません。
.NET で AOP を行うための最も強力なツールは PostSharp です。PostSharp は sharpcrafters.com (英語) から入手することができます。PostSharp は、AOP 理論の主な機能すべてを体験できる完全な AOP フレームワークを提供します。ただし、多くの依存関係挿入 (DI) フレームワークにはなんらかの AOP 機能が含まれていることに注目すべきです。
たとえば、Spring.NET、Castle Windsor、そして (もちろん) Microsoft Unity には、AOP 機能が含まれています。アプリケーション層でのトレース、キャッシュ、コンポーネントの装飾などの比較的単純なシナリオでは、通常、DI フレームワークの機能が功を奏します。しかし、ドメイン オブジェクトや UI オブジェクトの処理となると、DI フレームワークで行うのは困難です。横断的問題は確かに外部依存関係と見なすことができ、DI の手法を使用すれば確かに外部依存関係をクラスに挿入することができます。
問題は、DI を使用するには、場当たり的な事前設計や多少のリファクタリングが必要になる可能性が高いことです。つまり、既に DI フレームワークを使用している場合は AOP 機能を導入するのは簡単ですが、システムに DI が使用されていない場合は、DI フレームワークを導入するにはかなりの労力が必要になる可能性が高いということです。大規模プロジェクトの場合やレガシ システムの更新時には、これは常に可能とは限りません。一方、標準の AOP アプローチでは、横断的問題をアスペクトと呼ばれる新しいコンポーネントにまとめます。今月のコラムでは、まず、アスペクト指向のパラダイムについて簡単に概説します。その後、Unity 2.0 に含まれている AOP 関連の機能について説明します。
AOP クイック ガイド
オブジェクト指向プログラミング (OOP) のプロジェクトは複数のソース ファイルから構成され、各ソース ファイルは 1 つ以上のクラスを実装しています。プロジェクトには、ログ記録やキャッシュなどの横断的問題を表すクラスも含まれます。すべてのクラスはコンパイラによって処理され、実行可能コードが生成されます。AOP でのアスペクトは、プロジェクト内の複数のクラスに必要となる動作をカプセル化する再利用可能なコンポーネントです。アスペクトが実際にどのように処理されるかは、使用する AOP テクノロジによって異なります。一般に、アスペクトは、コンパイラによって単純に直接処理されるわけではありません。アスペクトが考慮に入れられるように実行可能コードに変更を加えるため、テクノロジ固有のツールが追加で必要となります。AspectJ (最初に作成された AOP ツールである Java AOP コンパイラ) ではどのようなことが行われるかについて簡単に考えてみましょう。
AspectJ を使用する場合は、Java プログラミング言語を使用してクラスを記述し、AspectJ 言語を使用してアスペクトを記述します。AspectJ ではカスタム構文がサポートされており、カスタム構文を使用してアスペクトの想定動作を指定することができます。たとえば、ログ記録アスペクトでは、特定のメソッドが呼び出される前と呼び出された後にログ記録を行うことが指定される可能性があります。アスペクトは、なんらかの方法で通常のソース コードに統合され、ソース コードの中間バージョンが生成されます。その後、この中間バージョンは実行可能な形式にコンパイルされます。AspectJ の専門用語では、アスペクトを前処理しソース コードと統合するコンポーネントを "ウィーバー" と呼びます。ウィーバーは、コンパイラが実行可能ファイルに変換することのできる出力を生成します。
簡潔に言うと、アスペクトとは、既存のクラスのソース コードに手を加えることなく既存のクラスに挿入するための再利用可能なコードのことです。他の AOP フレームワーク (.NET PostSharp フレームワークなど) には、ウィーバー ツールはありません。しかし、アスペクトのコンテンツは常にフレームワークによって処理され、結果的になんらかの形でコードの挿入が行われます。
なお、"コード" の挿入は "依存関係" の挿入とは異なります。コードの挿入とは、特定のアスペクトで修飾されたクラスの本体内の特定の場所にアスペクト内のパブリック エンドポイントの呼び出しを挿入するという AOP フレームワークの機能を指します。たとえば、PostSharp フレームワークを使用すると、アスペクトを .NET の属性として記述することができ、その属性をクラス内のメソッドにアタッチすることができます。PostSharp の属性はビルド後のステップで PostSharp コンパイラ (ウィーバーと呼んでもいいでしょう) によって処理されます。最終的に、コードは属性のコードの一部を含むように強化されます。ですが、挿入場所は自動的に解決されるので、開発者は、自己完結型のアスペクト コンポーネントを記述し、それをパブリック クラスのメソッドにアタッチすればよいだけです。コードを記述するのは簡単で、保守するのはさらに簡単です。
AOP に関するこの簡単な概説を締めくくるにあたって、固有の用語をいくつか紹介し、その本来の意味を明確にしておきましょう。"ジョイン ポイント" は、ターゲット クラスのソース コード内で、アスペクトのコードを挿入する場所を示します。"ポイントカット" は、ジョイン ポイントの集合を表します。"アドバイス" は、ターゲット クラスに挿入するコードを指します。コードは、ジョイン ポイントの前、後、および前後に挿入することができます。アドバイスは、ポイントカットに関連付けられています。これらの用語は AOP の元の定義にあるもので、お使いの特定の AOP フレームワークに文字どおりに反映されるとは限りません。用語の背景にある概念 (AOP の根幹) を理解するよう努め、その知識を、特定のフレームワークの詳細への理解を深めるために使用することをお勧めします。
Unity 2.0 クイック ガイド
Unity は、Microsoft Enterprise Library プロジェクトの一部として入手でき、個別にダウンロードすることも可能なアプリケーション ブロックです。Microsoft Enterprise Library は、.NET アプリケーション開発につきものの多くの横断的問題 (ログ記録、キャッシュ、暗号化、例外処理など) に対処するアプリケーション ブロックの集まりです。Enterprise Library の最新バージョンは 5.0 で、2010 年 4 月にリリースされ、Visual Studio 2010 を完全にサポートしています (詳細については、patterns & practices デベロッパー センター (msdn.microsoft.com/library/ff632023、英語) を参照してください)。
Unity は、Enterprise Library アプリケーション ブロックの 1 つです。Unity は、Silverlight 用にも提供されており、基本的には、クラスのアスペクト指向性を少し高めることができるインターセプト メカニズムのサポートが追加された、DI コンテナーです。
Unity 2.0 におけるインターセプト
Unity におけるインターセプトの基本概念は、オブジェクトのメソッドを呼び出すのに必要な呼び出しチェーンを開発者がカスタマイズできるようにするものです。つまり、Unity のインターセプト メカニズムでは、構成済みのオブジェクトに対する呼び出しがキャプチャされ、通常のメソッド実行前、後、または前後にコードを追加することでターゲット オブジェクトの動作がカスタマイズされます。インターセプトは、基本的に、オブジェクトのソース コードに手を加えることも、同じ継承パス内にあるクラスの動作に影響を与えることもなく、実行時にオブジェクトに新しい動作を追加するための非常に柔軟なアプローチです。Unity におけるインターセプトは、デコレータ パターンを実装する方法です。デコレータ パターンとは、実行時、オブジェクトが使用されるときにオブジェクトの機能を拡張するために考案された人気のある設計パターンです。デコレータは、ターゲット オブジェクトのインスタンスを取得 (およびこのようなインスタンスへの参照を保持) し、外部に向けてそのインスタンスの機能を拡張するコンテナー オブジェクトです。
Unity 2.0 のインターセプト メカニズムでは、インスタンスのインターセプトと型のインターセプトの両方がサポートされています。さらに、インターセプトは、オブジェクトのインスタンスが作成される方法 (オブジェクトが Unity コンテナーを通じて作成されるか既知のインスタンスであるか) にかかわらず機能します。後者の場合、別個の、完全に独立した API を使用するだけです。ただし、そうすると、構成ファイルはサポートされなくなります。図 1 は、Unity のインターセプト機能のアーキテクチャを示しています。ここでは、コンテナーを通じて解決されない特定のオブジェクト インスタンスがどのように処理されるかが詳しく示されています (この図は単に、MSDN のドキュメントで使用されている図を少し手直ししたものです)。
図 1 Unity 2.0 におけるオブジェクトのインターセプトのしくみ
インターセプト サブシステムは、インターセプター (プロキシ)、動作パイプライン、および動作 (アスペクト) という 3 つの主要要素で構成されます。このサブシステムの両端には、クライアント アプリケーションとターゲット オブジェクト (ソース コード内にハードコーディングされていない追加の動作を割り当てられるオブジェクト) があります。クライアント アプリケーションが特定のインスタンスに対して Unity のインターセプト API を使用するように構成されると、すべてのメソッド呼び出しはプロキシ オブジェクト (インターセプター) を経由するようになります。このプロキシ オブジェクトは、登録済みの動作のリストを確認し、内部パイプラインを通じて動作を呼び出します。構成済みの動作それぞれに、オブジェクト メソッドの通常の呼び出しの前後に実行されるチャンスが与えられます。プロキシは、入力データをパイプラインに挿入し、ターゲット オブジェクトから最初に生成され動作によって、その後さらに変更を加えられた戻り値を受け取ります。
インターセプトを構成する
Unity 2.0 で推奨されるインターセプトの使い方は、以前のバージョンとは異なります。ただし、以前のバージョンで使用されていたアプローチは、下位互換性のために Unity 2.0 でも完全にサポートされます。Unity 2.0 では、インターセプトは、オブジェクトが実際にどのように解決されるかを表現するためにコンテナーに追加される新しい拡張にすぎません。流暢なコードを通じてインターセプトを構成する場合に必要なコードを以下に示します。
var container = new UnityContainer();
container.AddNewExtension<Interception>();
コンテナーは、インターセプトする型および追加する動作に関する情報を見つける必要があります。この情報は、流暢なコードを使用して追加することも、構成を通じて追加することもできます。私は、構成による方法が特に柔軟だと感じました。アプリケーションに手を加えることも、新たなコンパイル ステップを実行することもなく、変更を加えることができるためです。では、構成ベースのアプローチを使用しましょう。
まず、構成ファイル内に次の内容を追加します。
<sectionExtension type="Microsoft.Practices.Unity.InterceptionExtension.
Configuration.InterceptionConfigurationExtension,
Microsoft.Practices.Unity.Interception.Configuration"/>
このスクリプトの目的は、インターセプト サブシステム固有の新しい要素とエイリアスで構成スキーマを拡張することです。また、次の内容も追加する必要があります。
<container>
<extension type="Interception" />
<register type="IBankAccount" mapTo="BankAccount">
<interceptor type="InterfaceInterceptor" />
<interceptionBehavior type="TraceBehavior" />
</register>
</container>
流暢なコードを使用して同じことを行うには、コンテナー オブジェクトの AddNewExtension<T> と RegisterType<T> を呼び出します。
構成スクリプトをもっと詳しく見てみましょう。<extension> 要素では、コンテナーにインターセプトを追加しています。スクリプト内で使用されている "Interception" は、セクション拡張内で定義されているエイリアスの 1 つであることに注目してください。インターフェイス型 IBankAccount は具象型 BankAccount にマップされ (これは DI コンテナーの標準的な役割です)、特定の型のインターセプターに関連付けられます。Unity では、主に、インスタンス インターセプターと型インターセプターという 2 つの種類のインターセプターが提供されます。来月は、インターセプターについてもっと深く掘り下げる予定です。今のところは、インスタンス インターセプターはインターセプトされたインスタンスをターゲットとした受信呼び出しをフィルター処理するためのプロキシを作成するとだけ言っておきましょう。一方、型インターセプターは、単にインターセプトされたオブジェクトの型を模倣し、派生型のインスタンスを処理します (インターセプターの詳細については、msdn.microsoft.com/library/ff660861(PandP.20) (英語) を参照してください)。
インターフェイス インターセプターは、オブジェクトのたった 1 つのインターフェイスのプロキシとしてしか機能できないインスタンス インターセプターです。インターフェイス インターセプターは、動的コード生成を使用してプロキシ クラスを作成します。構成内の interceptionBehavior 要素は、インターセプトされたオブジェクト インスタンスの前後で実行する外部コードを示しています。TraceBehavior クラスは、コンテナーが TraceBehavior とその依存関係を解決できるように、宣言によって構成されている必要があります。<register> 要素は、以下に示すように、TraceBehavior クラスとその予想されるコンストラクターについてコンテナーに通知するために使用します。
<register type="TraceBehavior">
<constructor>
<param name="source" dependencyName="interception" />
</constructor>
</register>
図 2 に、TraceBehavior クラスの抜粋を示します。
図 2 Unity の動作のサンプル
class TraceBehavior : IInterceptionBehavior, IDisposable
{
private TraceSource source;
public TraceBehavior(TraceSource source)
{
if (source == null)
throw new ArgumentNullException("source");
this.source = source;
}
public IEnumerable<Type> GetRequiredInterfaces()
{
return Type.EmptyTypes;
}
public IMethodReturn Invoke(IMethodInvocation input,
GetNextInterceptionBehaviorDelegate getNext)
{
// BEFORE the target method execution
this.source.TraceInformation("Invoking {0}",
input.MethodBase.ToString());
// Yield to the next module in the pipeline
var methodReturn = getNext().Invoke(input, getNext);
// AFTER the target method execution
if (methodReturn.Exception == null)
{
this.source.TraceInformation("Successfully finished {0}",
input.MethodBase.ToString());
}
else
{
this.source.TraceInformation(
"Finished {0} with exception {1}: {2}",
input.MethodBase.ToString(),
methodReturn.Exception.GetType().Name,
methodReturn.Exception.Message);
}
this.source.Flush();
return methodReturn;
}
public bool WillExecute
{
get { return true; }
}
public void Dispose()
{
this.source.Close();
}
}
動作クラスは、IInterceptionBehavior を実装します。IInterceptionBehavior は、基本的に Invoke メソッドで構成されます。Invoke メソッドには、インターセプターの制御下にあるメソッドに対して使用するロジックがすべて含まれています。ターゲット メソッドが呼び出される前になんらかの処理を行う必要がある場合は、Invoke メソッドの先頭でその処理を行います。ターゲット オブジェクトに (より正確に言うと、パイプラインに登録された次の動作に) 移行する場合は、フレームワークによって提供される getNext デリゲートを呼び出します。最後に、任意のコードを使用してターゲット オブジェクトの後処理を行うことができます。Invoke メソッドは、パイプライン内の次の要素への参照を返す必要があります。null が返された場合、チェーンは中断され、その後の動作は呼び出されません。
構成の柔軟性
インターセプトは (より広く、"AOP は" と言ってもよいでしょう)、多くの興味深いシナリオに対処します。たとえば、インターセプトを使用すると、クラス全体に変更を加えることなく個々のオブジェクトに役割を追加することができるので、デコレータを使用する場合と比べて、ソリューションははるかに柔軟になります。
今月は、.NET に適用される AOP の表面をなぞったにすぎません。今後数か月の間に、Unity におけるインターセプトや AOP 全般についてさらにご紹介する予定です。
Dino Esposito は、『Programming Microsoft ASP.NET MVC』(Microsoft Press、2010 年) の著者であり、『Microsoft .NET: Architecting Applications for the Enterprise』(Microsoft Press、2008 年) の共著者です。Esposito はイタリアに在住し、世界各国で開催される業界のイベントで頻繁に講演しています。ブログは weblogs.asp.net/despos (英語) で読むことができます。
この記事のレビューに協力してくれた技術スタッフの Chris Tavares に心より感謝いたします。