Cutting Edge
Unity におけるポリシーの挿入
Dino Esposito
ライブラリでサポートされるようになっています。Unity も同様に改訂されています。AOP の主な目的は、コンポーネントやロジックにまたがる横断的問題を、開発者がより効率的に処理できるようにすることです。つまり、アプリケーションのオブジェクト モデルをいつ作成し、セキュリティ、キャッシュ、ログ記録などのコードのアスペクトにどのように対処すればよいかといった疑問に、AOP が対処します。このようなアスペクトは、実装にとっては重要な要素ですが、開発者が構築しているモデルのオブジェクトには厳密には含まれません。設計を犠牲にしてまでも、ビジネスに関係のないアスペクトを組み込む必要があるでしょうか。それとも、ビジネス指向クラスを追加のアスペクトで修飾する方が良いでしょうか。クラスをアスペクトで修飾する場合、基本的には AOP がこのようなアスペクトを定義してアタッチする構文を提供します。
アスペクトとは、横断的問題の実装です。アスペクトを定義する場合、いくつか指定が必要です。まず、実装上の問題に対処するコードを指定します。AOP の専門用語では、これをアドバイスと呼びます。アドバイスは、コードの特定の場所 (メソッド本体、プロパティの getter や setter、例外ハンドラーなど) に適用します。このアドバイスを適用する場所をジョイント ポイントと呼びます。最後に、AOP の専門用語で言うポイントカットを指定します。ポイントカットは、ジョイン ポイントの集合を表します。通常、ポイントカットは、メソッド名とワイルドカードを使用した条件によって定義します。AOP は最終的に実行時に機能し、アドバイスのコードをジョイン ポイントの前か後、あるいは前後に挿入します。その後、アドバイスがポイントカットに関連付けられます。
前回までのコラムでは、Unity のインターセプト API について説明してきました。この API は、クラスにアタッチするアドバイスを定義できるようにします。Unity の用語で言うと、アドバイスは動作オブジェクトになります。通常、Unity の IoC メカニズムで解決する型に動作をアタッチします。ただしインターセプトのメカニズムには IoC の機能がどうしても必要なわけではありません。実際、プレーンなコードで作成したインスタンスに適用するようにもインターセプトを構成できます。
動作は、固定インターフェイス (IInterceptionBehavior インターフェイス) を実装するクラスで構成します。このインターフェイスには、Invoke というメソッドがあります。このメソッドをオーバーライドすることにより、通常のメソッド呼び出しの前か後、あるいは前後に実行する手順を定義します。動作を型にアタッチする場合、コードでも構成スクリプトでも使用できます。このため、必要な作業はジョイン ポイントを定義することだけです。では、ポイントカットはどのように定義するのでしょう。
先月のコラムで説明したように、ターゲット オブジェクト上でインターセプトしたすべてのメソッドは、動作オブジェクトの Invoke メソッドで表現されるロジックに従って実行されます。基本インターセプト API にはメソッドを区別する機能がなく、具体的な一致規則もサポートしません。この問題に対処するため、ポリシーの挿入 API を使用します。
ポリシーの挿入と PIAB
Microsoft Enterprise Library (EntLib) の最新バージョン (5.0) より前のバージョンを使用していた方は、Policy Injection Application Block (PIAB) について耳にし、おそらくアプリケーションで利用したこともおありでしょう。EntLib 5.0 にも PIAB モジュールが用意されています。では、Unity のポリシーの挿入と EntLib の PIAB には、どのような違いがあるのでしょう。
EntLib 5.0 の PIAB は主に互換性の理由から存在しています。新しいバージョンでは、PIAB アセンブリの内容が変更されています。特に、すべてのインターセプト メカニズムが Unity に含まれるようになったため、以前のバージョンの EntLib に含まれていたシステム提供の呼び出しハンドラーは、すべて他のアセンブリに移動されました (図 1 参照)。
図 1 Microsoft Enterprise Library 5.0 の呼び出しハンドラーのリファクタリング
呼び出しハンドラー | Enterprise Library 5.0 での新しいアセンブリ |
承認ハンドラー | Microsoft.Practices.EnterpriseLibrary.Security.dll |
キャッシュ処理ハンドラー | PIAB から削除 |
例外処理ハンドラー | Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.dll |
ログ記録ハンドラー | Microsoft.Practices.EnterpriseLibrary.Logging.dll |
パフォーマンス カウンター ハンドラー | Microsoft.Practices.EnterpriseLibrary.PolicyInjection.dll |
検証ハンドラー | Microsoft.Practices.EnterpriseLibrary.Validation.dll |
図 1 からわかるように、各呼び出しハンドラーは、関連するアプリケーション ブロックのアセンブリに移動されました。つまり、例外処理の呼び出しハンドラーは例外処理アプリケーション ブロックに、検証ハンドラーは検証アプリケーション ブロックにという具合に移動されました。唯一の例外がパフォーマンス カウンター ハンドラーで、PolicyInjection アセンブリに移動されました。アセンブリは変更されましたが、クラスの名前空間は同じままです。また、セキュリティ上の問題から、以前の PIAB に含まれていたキャッシュ呼び出しハンドラーが EntLib 5.0 では削除され、CodePlex の EntLib Contrib Web サイト (bit.ly/gIcP6H、英語) からしか入手できなくなったことにも注意してください。このような変更の結果、PIAB は旧バージョンとの互換性を確保する目的でのみ使用できるレガシ コンポーネントから構成されるようになり、EntLib 5.0 を使ってコンパイルする場合はコードの変更が必要になります。レガシ コンポーネントとの密接な依存関係がなければ、ポリシーの挿入層をアップグレードして、Unity アプリケーション ブロックに組み込まれた新しい (しかもほぼ同じ) ポリシーの挿入 API を利用することをお勧めします。それでは、Unity のポリシーの挿入について詳しく説明しましょう。
ポリシーの挿入の概要
ポリシーの挿入とは、Unity の基本インターセプト API を拡張して、メソッド単位にマップ規則や呼び出しハンドラーを追加する、1 つのコード層です。ポリシーの挿入は特殊なインターセプト動作として実装され、初期化時と実行時という 2 つの主要フェーズから構成されます。
初期化フェーズでは、まず、フレームワークが、使用可能なポリシーのうちインターセプト対象のターゲット メソッドに適用できるポリシーを特定します。このコンテキストでは、インターセプト対象のオブジェクトと実際の呼び出し元の間に特定の順序で挿入できる一連の操作としてポリシーを記述します。インターセプトできるのは、ポリシーの挿入用に明示的に構成したオブジェクト (既存のインスタンスまたは新規作成したインスタンス) のメソッドだけです。
ポリシーの挿入フレームワークは、適用可能なポリシーの一覧を特定したら、一連の操作のパイプラインを準備します (1 つの操作が 1 つの呼び出しハンドラーを表します)。つまり、パイプラインは、対応する各ポリシーに対して定義されたすべてのハンドラーを組み合わせたものになります。パイプライン内のハンドラーは、ポリシーの順序と、親ポリシーの各ハンドラーに割り当てられた優先度に基づいて並べ替えられます。ポリシー対応のメソッドが呼び出されると、事前に構築したパイプラインが処理されます。メソッドが同じオブジェクトの他のポリシー対応メソッドを呼び出すと、呼び出されたメソッドのハンドラーのパイプラインがメインのパイプラインにマージされます。
呼び出しハンドラー
呼び出しハンドラーは、"動作" よりも具体的なもので、元々 AOP で定義されていたアドバイスに似ています。動作は、型に適用し、開発者がさまざまなメソッドを使ってさまざまな動作を用意する必要があるのに対して、呼び出しハンドラーはメソッド単位に指定します。
呼び出しハンドラーはパイプラインに組み立てられ、あらかじめ決められた順序で呼び出されます。各ハンドラーは、メソッド名、パラメーター、戻り値、予想される戻り値の型など、呼び出しの詳細情報にアクセスできます。呼び出しハンドラーは、パラメーターや戻り値の変更、パイプライン内の呼び出しの伝達の停止、例外の生成なども可能です。
興味深いことに、Unity には呼び出しハンドラーが付属していません。独自のハンドラーを作成するか、EntLib 5.0 のアプリケーション ブロックを参照して図 1 のいずれかの呼び出しハンドラーを使用することになります。
呼び出しハンドラーとは、次のような ICallHandler インターフェイスを実装するクラスです。
public interface ICallHandler
{
IMethodReturn Invoke(
IMethodInvocation input,
GetNextHandlerDelegate getNext);
int Order { get; set; }
}
Order プロパティは、他のすべてのハンドラーとの相対優先度を示します。Invoke メソッドは、メソッドからの戻り値を含むクラスのインスタンスを返します。
呼び出しハンドラーの実装の考え方はきわめてシンプルで、特定の処理を実行してからパイプライン内の次のハンドラーに処理を受け渡すことだけが想定されています。パイプラインの次のハンドラーに制御を明け渡すため、ハンドラーは Unity ランタイムから受け取った getNext パラメーターを呼び出します。getNext パラメーターは、次のように定義されるデリゲートです。
public delegate InvokeHandlerDelegate GetNextHandlerDelegate();
また、InvokeHandlerDelegate は次のように定義されます。
public delegate IMethodReturn InvokeHandlerDelegate(
IMethodInvocation input,
GetNextHandlerDelegate getNext);
Unity のドキュメントには、インターセプトを説明するわかりやすい図が掲載されています。図 2 は、その図に少し手を加えた、ポリシーの挿入のアーキテクチャを示す図です。
図 2 Unity のポリシーの挿入における呼び出しハンドラーのパイプライン
システム提供のポリシーの挿入動作の区画内に、プロキシ オブジェクトや派生クラスで呼び出される特定のメソッドを処理する、一連のハンドラーがあるのがわかります。Unity におけるポリシーの挿入に関する概要の締めくくりとして、一致規則を取り上げておく必要があります。
一致規則
一致規則は、インターセプトのロジックを適用する場所を指定するために使用します。動作を使用する場合、そのコードはオブジェクト全体に適用されますが、1 つ以上の一致規則を使用して、フィルターを定義できます。一致規則とは、Unity がハンドラーのパイプラインをアタッチする対象となるオブジェクトやメンバーを選択する条件です。AOP 用語を使用すると、一致規則はポイントカットの定義に使用する条件です。図 3 に、Unity でネイティブにサポートされる一致規則を示します。
図 3 Unity 2.0 でサポートされる一致規則の一覧
一致規則 | 説明 |
AssemblyMatchingRule | 指定したアセンブリの型に基づいて、ターゲット オブジェクトを選択します。 |
CustomAttributeMatchingRule | メンバー レベルのカスタム属性に基づいて、ターゲット オブジェクトを選択します。 |
MemberNameMatchingRule | メンバー名に基づいて、ターゲット オブジェクトを選択します。 |
MethodSignatureMatchingRule | シグネチャに基づいて、ターゲット オブジェクトを選択します。 |
NamespaceMatchingRule | 名前空間に基づいて、ターゲット オブジェクトを選択します。 |
ParameterTypeMatchingRule | メンバーのパラメーターの型名に基づいて、ターゲット オブジェクトを選択します。 |
PropertyMatchingRule | 複数のメンバー名 (ワイルドカード文字を含む) に基づいて、ターゲット オブジェクトを選択します。 |
ReturnTypeMatchingRule | 戻り値に基づいて、ターゲット オブジェクトを選択します。 |
TagMatchingRule | アドホックな Tag 属性に割り当てられた値に基づいて、ターゲット オブジェクトを選択します。 |
TypeMatchingRule | 型名に基づいて、ターゲット オブジェクトを選択します。 |
一致規則は、IMatchingRule インターフェイスを実装するクラスです。このような知識を念頭に、ポリシーの挿入の処理方法を見ていきます。ポリシーを定義する方法には、基本的に、属性の使用、流暢なコードの使用、および構成の 3 つがあります。
属性を使用してポリシーを追加する
図 4 は、操作の結果が負になった場合に例外をスローする呼び出しハンドラーの例を示しています。これと同じハンドラーはさまざまなシナリオで使用しています。
図 4 NonNegativeCallHandler クラス
public class NonNegativeCallHandler : ICallHandler
{
public IMethodReturn Invoke(IMethodInvocation input,
GetNextHandlerDelegate getNext)
{
// Perform the operation
var methodReturn = getNext().Invoke(input, getNext);
// Method failed, go ahead
if (methodReturn.Exception != null)
return methodReturn;
// If the result is negative, then throw an exception
var result = (Int32) methodReturn.ReturnValue;
if (result <0)
{
var exception = new ArgumentException("...");
var response = input.CreateExceptionMethodReturn(exception);
// Return exception instead of original return value
return response;
}
return methodReturn;
}
public int Order { get; set; }
}
ハンドラーの最も単純な使用方法は、有効だと判断した場所のメソッドにハンドラーをアタッチすることです。そのためには、次のような属性が必要です。
public class NonNegativeCallHandlerAttribute : HandlerAttribute
{
public override ICallHandler CreateHandler(
IUnityContainer container)
{
return new NonNegativeCallHandler();
}
}
以下に、属性ベースのポリシーで修飾したサンプルの Calculator クラスを示します。
public class Calculator : ICalculator
{
public Int32 Sum(Int32 x, Int32 y)
{
return x + y;
}
[NonNegativeCallHandler]
public Int32 Sub(Int32 x, Int32 y)
{
return x - y;
}
}
このように属性をアタッチすると、Sum メソッドを呼び出した場合は戻り値に関係なく通常どおり処理されますが、Sub メソッドを呼び出した場合は、負の値が返されたときに例外がスローされます。
流暢なコードを使用する
属性が気に入らなければ、Fluent API (滑らかなインターフェイス) を使用して同じロジックを表現できます。この場合、一致規則に関してもっと多くの詳細を指定する必要があります。ここでは、Int32 値を返す Sub というメソッドだけにコードを挿入するという考えを表現する方法を示します。Fluent API を使用して、Unity コンテナーを構成します (図 5 参照)。
図 5 一連の一致規則を定義する流暢なコード
public static UnityContainer Initialize()
{
// Creating the container
var container = new UnityContainer();
container.AddNewExtension<Interception>();
// Adding type mappings
container.RegisterType<ICalculator, Calculator>(
new InterceptionBehavior<PolicyInjectionBehavior>(),
new Interceptor<TransparentProxyInterceptor>());
// Policy injection
container.Configure<Interception>()
.AddPolicy("non-negative")
.AddMatchingRule<TypeMatchingRule>(
new InjectionConstructor(
new InjectionParameter(typeof(ICalculator))))
.AddMatchingRule<MemberNameMatchingRule>(
new InjectionConstructor(
new InjectionParameter(new[] {"Sub", "Test"})))
.AddMatchingRule<ReturnTypeMatchingRule>(
new InjectionConstructor(
new InjectionParameter(typeof(Int32))))
.AddCallHandler<NonNegativeCallHandler>(
new ContainerControlledLifetimeManager(),
new InjectionConstructor());
return container;
}
ContainerControlledLifetimeManager マネージャーを使用すると、同じ呼び出しハンドラーのインスタンスがすべてのメソッドで共有されることが保証されます。
このコードを実行すると、ICalculator 型を実装する (つまり、インターセプトを構成して、Unity を通じて解決する) 具象型で、Sub メソッドと Test メソッドという 2 つの挿入候補が選択されます。ただし、その次の一致規則で残るのは、戻り値の型が Int32 のメソッドだけです。つまり、Test メソッドが Double 型の値を返すとすると、Test メソッドは除外されます。
構成を使用してポリシーを追加する
最後に、構成ファイルを使用しても同じ考えを表現できます。図 6 に、<unity> セクションに想定される内容を示します。
図 6 構成ファイルでのポリシーの挿入の準備
public class NonNegativeCallHandler : ICallHandler
{
public IMethodReturn Invoke(IMethodInvocation input,
GetNextHandlerDelegate getNext)
{
// Perform the operation
var methodReturn = getNext().Invoke(input, getNext);
// Method failed, go ahead
if (methodReturn.Exception != null)
return methodReturn;
// If the result is negative, then throw an exception
var result = (Int32) methodReturn.ReturnValue;
if (result <0)
{
var exception = new ArgumentException("...");
var response = input.CreateExceptionMethodReturn(exception);
// Return exception instead of original return value
return response;
}
return methodReturn;
}
public int Order { get; set; }
}
<unity xmlns="https://schemas.microsoft.com/practices/2010/unity">
<assembly name="PolicyInjectionConfig"/>
<namespace name="PolicyInjectionConfig.Calc"/>
<namespace name="PolicyInjectionConfig.Handlers"/>
<sectionExtension ... />
<container>
<extension type="Interception" />
<register type="ICalculator" mapTo="Calculator">
<interceptor type="TransparentProxyInterceptor" />
<interceptionBehavior type="PolicyInjectionBehavior" />
</register>
<interception>
<policy name="non-negative">
<matchingRule name="rule1"
type="TypeMatchingRule">
<constructor>
<param name="typeName" value="ICalculator" />
</constructor>
</matchingRule>
<matchingRule name="rule2"
type="MemberNameMatchingRule">
<constructor>
<param name="namesToMatch">
<array type="string[]">
<value value="Sub" />
</array>
</param>
</constructor>
</matchingRule>
<callHandler name="handler1"
type="NonNegativeCallHandler">
<lifetime type="singleton" />
</callHandler>
</policy>
</interception>
</container>
</unity>
実は、1 つのポリシーで複数の一致規則を定義すると、すべての一致規則にブール演算子の AND が適用されます (つまり、すべての一致規則が true になる必要があります)。複数のポリシーを定義する場合、各ポリシーが一致するかどうか (および適用先ハンドラーに対して) 個別に評価されます。したがって、さまざまなポリシーから適用するハンドラーを取得できます。
インターセプトの概要
まとめると、インターセプトは、アスペクト指向を実装するために Microsoft .NET Framework 空間のほとんどの IoC フレームワークが採用している手法です。インターセプトを使用すると、任意のアセンブリに含まれる任意の型の任意のメソッドの前後で、独自のコードを実行できるようになります。以前の EntLib には、この目的のために専用のアプリケーション ブロック (PIAB) が用意されていました。EntLib 5.0 では、PIAB の基盤となるエンジンが Unity に移動され、これまで 2 回のコラムで説明した Unity の低レベル インターセプト API 用の特別な動作として実装されました。ポリシーの挿入動作には Unity コンテナーを使用する必要があり、この動作は低レベルのインターセプト API だけでは機能しません。
しかし、低レベルのインターセプト API ではインターセプト対象の型メンバーを選択できず、開発者が選択用のコードを作成する必要があります。ただし、ポリシーの挿入動作を使用すれば、開発者は目的の動作の詳細に集中でき、開発者が指定した規則に基づいてライブラリから適用先メソッドを特定できます。
Dino Esposito は、『Programming ASP.NET MVC』(Microsoft Press、2010 年) の著者で、『Architecting Applications for the Enterprise』(Microsoft Press、2008 年) の共著者でもあります。Esposito はイタリアに在住し、世界各国で開催される業界のイベントで頻繁に講演しています。ブログは weblogs.asp.net/despos (英語) で読むことができます。
この記事のレビューに協力してくれた技術スタッフの Chris Tavares に心より感謝いたします。