Cutting Edge
Unity でのインターセプター
Dino Esposito
先月のコラムでは、Unity 2.0 の依存関係挿入コンテナーで使用されているインターセプトのメカニズムについて簡単に説明しました。そこでは、アスペクト指向プログラミング (AOP) の中核となる原理を説明してから、現在の多くの開発者のニーズに非常に近いと思われる、インターセプトの具体例を紹介しました。
ソース コードに手を加えることなく、既存のコードの動作を拡張したい、と思ったことはありませんか。既存のコードの前後で別のコードを実行できたら、と考えたことはありませんか。
AOP は、主要なビジネス ロジックを横断する問題から中核となるコードを分離するために生み出されました。Unity 2.0 は Microsoft .NET Framework 4 ベースのフレームワークを提供し、驚くほど迅速かつ簡単にこのような分離を実現します。
この続編コラムの目的を完璧に理解していただくため、まずは先月のコラムの内容をまとめておきましょう。ご存知のように、先月のコードでは、いくつか前提を置き、既定のコンポーネントをいくつか使用しました。今月は少し戻って、途中の過程で通常遭遇することになる選択肢やオプションについて詳しく解説します。
Unity における AOP
ある時点で、なんらかのビジネス固有の動作を実行するアプリケーションの開発を済ませたとしましょう。ある日、顧客からもっと多くの作業を実行できるよう、その動作を拡張することを求められました。ソース コードの内容を把握し、それを変更して、新機能をコーディングしてテストするためのコンサルティングに数時間を費やしました。しかし、このような場合、既存のソース コードに手を加えることなく、シームレスに新しい動作を追加できたらすばらしいと思いませんか。
少しシナリオを変えて考えてみましょう。まず、独立コンサルタントではなく、正社員だとしたらどうでしょう。新たにこの変更が求められるため、現行のプロジェクト以外の仕事に費やす時間が増え、さらに悪いことには、コードベースに新しく (かつ、必ずしも必要とは言えない) 分岐を作成する危険を冒す羽目になります。そのため、新しい動作をシームレスに追加できる、ソース コードに何も公開しないソリューションを切望するのではないでしょうか。
今度は、だれかからバグや深刻なパフォーマンスへの影響について報告された場面を想像してみましょう。問題を調査して修正する必要があり、完全に内密に行いたいと考えています。この場合も、新しい動作をシームレスに追加して、ソース コードには何も公開しない方が適切です。
AOP は、このようなシナリオすべてに役立ちます。
先月は、Unity 2.0 のインターセプト API を利用して、既存のメソッドに手を加えることなく、メソッドの前後に前処理と後処理を追加する方法について説明しました。ですが、この簡単な説明には、いくつか前提を設けました。
1 つ目に、Unity の制御の反転 (IoC) インフラストラクチャに登録し、Unity のファクトリ層を通じてインスタンスを作成する形式で作業しました。
2 つ目に、ポイントカットは、インターフェイスを通してのみ定義しました。AOP の専門用語の "ポイントカット" とは、ターゲット クラス本体において、フレームワークがオンデマンドで追加の動作を挿入する場所の集まりを表します。インターフェイス ベースのポイントカットとは、インターフェイスのメンバーのみが、コードの挿入を通じて実行時に拡張することを意味します。
3 つ目に、インターセプトを有効にするために構成設定のみに注目したため、コードで Unity を構成できる、Fluent API (滑らかなインターフェイス) には注意を払いませんでした。
ここからは、Fluent API と、Unity のインターセプターを定義する別の方法について見ていきます。
インターセプト可能なインスタンス
新しい動作を、クラスの既存のインスタンスや新しく作成したインスタンスに追加するには、ファクトリをある程度制御しなくてはなりません。つまり、AOP は純粋なる魔法というわけではないので、次に示す標準の new 演算子を使用して作成した、プレーンな CLR クラスのインスタンスをフックできるようにはならないということです。
var calculator = new Calculator();
AOP フレームワークがインスタンスを制御する方法は、かなり異なります。Unity では、元のオブジェクトのプロキシを返すいくつかの明示的な呼び出しを利用するか、IoC フレームワークの背後ですべてのものを実行し続けることが可能です。このため、ほとんどの IoC フレームワークが AOP 機能を提供します。Spring.NET と Unity はその例です。AOP が IoC と組み合わされば、シームレスかつ簡単で効果的なコーディング エクスペリエンスを実現できます。
まず、IoC 機能を使用しない例を見てみましょう。以下に、Calculator クラスの既存のインスタンスをインターセプト可能にする、基本的なコードを示します。
var calculator = new Calculator();
var calculatorProxy = Intercept.ThroughProxy<ICalculator>(calculator,
new InterfaceInterceptor(), new[] { new LogBehavior() });
Console.WriteLine(calculatorProxy.Sum(2, 2));
最終的には、元のオブジェクトをラップする、インターセプト可能なプロキシを操作することになります。ここでは、Calculator クラスが ICalculator インターフェイスを実装すると想定しています。インターセプト可能にするためには、クラスがインターフェイスを実装するか、MarshalByRefObject から継承しなくてはなりません。クラスを MarshalByRefObject から派生する場合、インターセプターは、次のように TransparentProxyInterceptor 型にする必要があります。
var calculator = new Calculator();
var calculatorProxy = Intercept.ThroughProxy(calculator,
new TransparentProxyInterceptor(), new[] { new LogBehavior() });
Console.WriteLine(calculatorProxy.Sum(2, 2));
Intercept クラスは、より直接的な方法でインターセプト可能なオブジェクトを作成するために呼び出せる、NewInstance メソッドも提供します。使用方法を次に示します。
var calculatorProxy = Intercept.NewInstance<Calculator>(
new VirtualMethodInterceptor(), new[] { new LogBehavior() });
NewInstance を使用するときは、インターセプターのコンポーネントを若干変える必要があります。ご覧のように、オブジェクトは InterfaceInterceptor でも TransparentProxyInterceptor でもなく、VirtualMethodInterceptor にしています。では、Unity には何種類のインターセプターが存在しているのでしょう。
インスタンス インターセプターと型インターセプター
インターセプターは Unity のコンポーネントで、ターゲット オブジェクトへの本来の呼び出しをキャプチャして、それを動作パイプラインにルーティングし、通常のメソッド呼び出しの前後に各動作を実行するチャンスを与える役割があります。インターセプトには、インスタンスのインターセプトと型のインターセプトの 2 種類があります。
インスタンス インターセプターは、インターセプトしたインスタンスをターゲットとした受信呼び出しをフィルター処理するためのプロキシを作成します。型インターセプターは、インターセプトしている型から派生する新しいクラスを生成して、その派生型のインスタンスを処理します。言うまでもなく、元の型と派生型の差分は、受信呼び出しをフィルター処理するのに必要なロジックにすべて含めます。
インスタンスをインターセプトする場合、アプリケーション コードでは、まず従来のファクトリ (または new 演算子) を使用してターゲット オブジェクトを作成し、その後 Unity によって提供されるプロキシを使用してそのオブジェクトとの対話を強制します。
型をインターセプトする場合、アプリケーションでは API か Unity を通じてターゲット オブジェクトを作成し、そのインスタンスを操作します (new 演算子でオブジェクトを直接作成して、型のインターセプトを行うことはできません)。ですが、ターゲット オブジェクトは元の型ではありません。実際の型が、その場で Unity によって派生され、インターセプトのロジックを組み込みます (図 1 参照)。
図 1 インスタンス インターセプターと型インターセプター
InterfaceInterceptor と TransparentProxyInterceptor は、インスタンス インターセプターのカテゴリに属する Unity のインターセプターです。VirtualMethodInterceptor は、型インターセプターのカテゴリに属します。
InterfaceInterceptor は、ターゲット オブジェクトのインターフェイス (1 つだけ) のパブリック インスタンス メソッドをインターセプトできます。インターセプターは、新しいインスタンスにも既存のインスタンスにも適用することができます。
TransparentProxyInterceptor は、インターフェイス (複数可) のパブリック インスタンス メソッドと、参照マーシャリング オブジェクトをインターセプトできます。これは、インターセプトへのアプローチとして最も低速ですが、メソッドを最も広範囲にインターセプトできます。インターセプターは、新しいインスタンスにも既存のインスタンスにも適用することができます。
VirtualMethodInterceptor は、パブリックかつ保護された仮想メソッドをインターセプトできます。インターセプターは、新しいインスタンスにのみ適用できます。
インスタンスのインターセプトは、あらゆるパブリック インスタンス メソッドに適用できますが、コンストラクターには適用できません。これは、インターセプトを既存のインスタンスに適用するシナリオではかなり明確です。しかし、インターセプトを新しく作成するインスタンスに適用するシナリオではあまり明確にはなりません。インスタンスのインターセプトを実装すると、操作するオブジェクトからアプリケーション コードに戻るときには、既にコンストラクターが実行されていることになります。結果として、すべてのインターセプト可能な操作は、必然的にインスタンスの作成の後になります。
型インターセプトは、元の型から継承するオブジェクトを返すために、動的なコード生成を使用します。これを実行するにあたって、パブリックかつ保護されたすべての仮想メソッドを、インターセプトをサポートするためにオーバーライドします。次のコードについて考えてみましょう。
var calculatorProxy = Intercept.NewInstance<Calculator>(
new VirtualMethodInterceptor(), new[] { new LogBehavior() });
Calculator クラスは次のようになります。
public class Calculator {
public virtual Int32 Sum(Int32 x, Int32 y) {
return x + y;
}
}
図 2 は、calculatorProxy 変数の動的な検査の結果として現れる、実際の型の名前を示しています。
図 2 型のインターセプト後の実際の型
インスタンスのインターセプトと型のインターセプトの間には、他にも大きく異なる点があることも指摘しておく必要があります。たとえば、オブジェクトによるオブジェクト上でのインターセプトの呼び出しなどです。型のインターセプトを使用しているときにメソッドが同じオブジェクトの別のメソッドを呼び出していれば、その自己呼び出しは、インターセプトのロジックが同じオブジェクトにあるため、インターセプトできます。しかし、インスタンスのインターセプトの場合、呼び出しがプロキシを経由する場合のみインターセプトできます。自己呼び出しは、もちろんプロキシを使用しないため、インターセプトできません。
IoC コンテナーを使用する
先月の例では、Unity ライブラリの IoC コンテナーを使用してオブジェクトを作成しました。IoC コンテナーは、オブジェクト作成前後に層を追加することにより、アプリケーションをより柔軟にします。追加の AOP 機能がある IoC フレームワークにおいては、このことが一層顕著です。さらに、私が見る限り、IoC コンテナーをオフライン構成と組み合わせると、コードの柔軟さのレベルはさらに増します。コード ベースの滑らかな構成のある、Unity のコンテナーを使用する例から見ていきましょう。
// Configure the IoC container
var container = UnityStarter.Initialize();
// Start the application
var calculator = container.Resolve<ICalculator>();
var result = calculator.Sum(2, 2);
コンテナーをブートストラップするのに必要なコードは、別のクラスに分離し、アプリケーションの起動時に 1 回呼び出します。ブートストラップ コードは、アプリケーションの前後の型を解決する方法と、インターセプトを処理する方法について、コンテナーに指示します。Resolve メソッドへの呼び出しは、インターセプトのすべての詳細を開発者から隠ぺいします。図 3 に、ブートストラップ コードの実装例を示します。
図 3 Unity のブートストラップ
public class UnityStarter {
public static UnityContainer Initialize() {
var container = new UnityContainer();
// Enable interception in the current container
container.AddNewExtension<Interception>();
// Register ICalculator with the container and map it to
// an actual type. In addition, specify interception details.
container.RegisterType<ICalculator, Calculator>(
new Interceptor<VirtualMethodInterceptor>(),
new InterceptionBehavior<LogBehavior>());
return container;
}
}
このコードが優れている点は、別のアセンブリに移動でき、動的な読み込みや変更が可能なことです。さらに重要な点は、Unity を 1 か所から構成できることです。これは、賢いファクトリのように動作する、使用するたびに準備を行う必要がある Intercept クラスに固執する限りは不可能です。このため、アプリケーションで AOP が必要な場合は、何としてでも IoC コンテナーを通じて利用する必要があります。構成の詳細を、app.config ファイル (Web アプリケーションの場合は web.config ファイル) に移動することで、さらに柔軟な方法で同じソリューションを実装することができます。この場合、ブートストラップ コードは、次の 2 行から構成します。
var container = new UnityContainer();
container.LoadConfiguration();
図 4 に、構成ファイルに必要なスクリプトを示します。ここでは、ICalculator 型に 2 つの動作を登録しました。つまり、インターフェイスのパブリック メンバーへの呼び出しは、すべて LogBehavior と BinaryBehavior によって前処理と後処理が行われます。
図 4 構成を使用したインターセプトの詳細の追加
<unity xmlns="https://schemas.microsoft.com/practices/2010/unity">
<assembly name="SimplestWithConfigIoC"/>
<namespace name="SimplestWithConfigIoC.Calc"/>
<namespace name="SimplestWithConfigIoC.Behaviors"/>
<sectionExtension
type="Microsoft.Practices.Unity.
InterceptionExtension.Configuration.
InterceptionConfigurationExtension,
Microsoft.Practices.Unity.Interception.Configuration" />
<container>
<extension type="Interception" />
<register type="ICalculator" mapTo="Calculator">
<interceptor type="InterfaceInterceptor"/>
<interceptionBehavior type="LogBehavior"/>
<interceptionBehavior type="BinaryBehavior"/>
</register>
<register type="LogBehavior">
</register>
<register type="BinaryBehavior">
</register>
</container>
</unity>
LogBehavior と BinaryBehavior は具象型なので、実際にはそれらを登録する必要はまったくありません。Unity は、既定でそれを実行するようになっています。
動作
Unity において、動作とは、実際に横断的問題を実装するオブジェクトのことです。動作は、IInterceptionBehavior インターフェイスを実装するクラスで、インターセプトするメソッドの実行サイクルを書き換え、メソッド パラメーターを変更したり値を返したりすることが可能です。また、メソッドがまったく呼び出されないようにすることや、複数回呼び出されるようにすることもできます。
動作は、3 つのメソッドから構成されます。図 5 は、Sum メソッドをインターセプトして、その戻り値をバイナリ文字列として書き換える、サンプルの動作を示します。WillExecute メソッドは、単にプロキシを最適化する手段です。false を返すと動作は実行されません。
図 5 動作のサンプル
public class BinaryBehavior : IInterceptionBehavior {
public IEnumerable<Type> GetRequiredInterfaces() {
return Type.EmptyTypes;
}
public bool WillExecute {
get { return true; }
}
public IMethodReturn Invoke(
IMethodInvocation input,
GetNextInterceptionBehaviorDelegate getNext) {
// Perform the operation
var methodReturn = getNext().Invoke(input, getNext);
// Grab the output
var result = methodReturn.ReturnValue;
// Transform
var binaryString = ((Int32)result).ToBinaryString();
// For example, write it out
Console.WriteLine("Rendering {0} as binary = {1}",
result, binaryString);
return methodReturn;
}
}
実は、これは少し微妙です。Invoke は常に呼び出されるため、false を返しても、実際には動作は実行されます。ですが、プロキシまたは派生型が作成されるとき、型に登録されているすべての動作に、false に設定された WillExecute があれば、プロキシそのものは作成されず、未処理のオブジェクトを再び操作することになります。これはプロキシ作成の最適化の問題です。
GetRequiredInterfaces メソッドによって、動作は、新しいインターフェイスをターゲット オブジェクトに追加できるようになります。このメソッドから返されるインターフェイスがプロキシに追加されます。このため、動作の中核となるのは Invoke メソッドであるといえます。パラメーターの入力によって、ターゲット オブジェクトで呼び出されているメソッドにアクセスできるようになります。getNext パラメーターは、パイプライン内の次の動作に移動するデリゲートで、最終的には、ターゲットのメソッドを実行します。
Invoke メソッドは、ターゲット オブジェクトのパブリック メソッドへの呼び出しの実行に使用される実際のロジックを決定します。ターゲット オブジェクト上でインターセプトしたすべてのメソッドは、Invoke で表現されるロジックに従って実行されます。
より具体的な一致規則を使用する場合はどうすればよいでしょう。このコラムで説明したような簡単なインターセプトで、実際に呼び出されるメソッドの把握するには、次のような IF ステートメントを多く実行することしかありません。
if(input.MethodBase.Name == "Sum") {
...
}
来月は、ここから再開して、インターセプトを適用するさらに効果的な方法について解説し、インターセプトしたメソッドの一致規則を定義します。
Dino Esposito は、『Programming ASP.NET MVC』(Microsoft Press、2010 年) の著者で、『Architecting Applications for the Enterprise』(Microsoft Press、2008 年) の共著者でもあります。Esposito はイタリアに在住し、世界各国で開催される業界のイベントで頻繁に講演しています。ブログは weblogs.asp.net/despos (英語) で読むことができます。
この記事のレビューに協力してくれた技術スタッフの Chris Tavares に心より感謝いたします。