通过AOP提高代码的封装和可复用性

[简介]
面向方面编程(AOP)是施乐公司帕洛阿尔托研究中心(Xerox PARC)在上世纪90年代发明的一种编程范式,它使开发人员可以更好地将本不该彼此纠缠在一起的任务(例如数学运算和异常处理)分离开来。AOP方法有很多优点。首先,由于操作更为简洁,所以改进了性能。其次,它使程序员可以花费更少的时间重写相同的代码。总之,AOP能够为不同过程提供更好的封装性,提高未来的互操作性。



是什么使软件工程师都希望自己能成为硬件工程师呢?自从函数发明以来,程序员花费了大量时间(及其老板的大多数资金)试图设计这样的系统:它们不过是一些组合模型,由其他人创建的部件构成,布置成独特的形状,再覆盖上一些悦目的颜色。函数、模板、类、组件等等一切,都是软件工程师自己创建“软件集成电路”(模拟硬件设计师的电子器件)的种种尝试。
我把这些都归结于Lego(乐高玩具)。把两个玩具块(即组件)拼起时发出的悦耳的咔哒声很让人上瘾,会促使许多程序员发明一种又一种新的封装和重用机制。这方面最新的进展就称为面向方面编程(AOP:Aspect-Oriented Programming)。AOP的核心是安排(一个摞在另一个之上)组件的一种方式,可以获得其他种类基于组件的开发方法无法得到的重用级别。这种安排是在客户端和对象之间的调用堆栈中进行的,其结果是为对象创建了一种特定的环境。这种环境正是AOP程序员主要追求的东西,继续阅读本文,您将了解这一点。
随本文一起提供的代码示例分为两部分:COM部分和Microsoft .NET部分。COM部分创建了一种基础结构,可以在COM对象中添加方面(编者按:英文原文为Aspect,本文之后均将此词译为“方面”),提供用户界面来配置类的方面,还给出了在我们提供的基础结构上创建的一个示例方面实现。 .NET部分说明了如何使用内置于.NET基础结构来完成COM版本同样的任务,但是所用代码更少,选择也更多。也提供了适合此基础结构的示例方面。本文后面将讲述所有这些代码。

何谓AOP?
一般情况下,对象是由一行行的代码“粘合”在一起的。创建这个对象。创建那个对象。为那个对象(其值为这个对象)设置属性。其间还点缀着一些用户数据。将一切搅拌在一起。在运行时达到450摄氏度时就可以执行了。将多个组件以这种方式连接起来会出现这样的问题:要实现不同的方法时,需要花费大量时间编写同样的代码。这些代码行中往往会有以下操作:将这个方法的活动记录日志到一个文件中以备调试,运行安全性检查,启动一个事务,打开一个数据库连接,记住捕捉C++异常或者Win32结构化异常以转换为COM异常,还要验证参数。而且,还要切记在方法执行完之后销毁方法开始时的设置。
这种重复代码经常出现的原因在于,开发人员被训练为根据软件新闻稿中的名词来设计系统。如果设计的是银行系统,Account类和Customer类必不可少,它们都将自己独特的详细信息收集到一处,但是它们的每个方法也都需要进行日志、安全检查、事务管理等操作。区别在于,日志等操作是一些与特定应用无关的系统方面。人人都需要它们。人人都编写这些代码。人人都讨厌这样。
噢,并不是人人……人人都需要使用这些服务,人人都讨厌编写重复的代码,但并不是人人都需要编写这些代码。例如,COM+和.NET程序员可以进行所谓的属性化编程,也称声明性编程。这种技术允许程序员用属性来修饰类型或者方法,声明需要运行时提供某种服务。例如COM+提供的一些服务,包括基于角色的安全性、实时激活、分布式事务管理和封送(marshaling)处理。在调用方法时,运行时(编者按:runtime,指.NET Framework提供的软件运行环境)会放置一组在客户端和服务器之间获得的对象(对于COM+程序员而言称为“侦听器”,对于.NET程序员而言称为“消息接收”),为每个方法提供服务,无需组件开发人员编写任何代码。这是面向方面编程最简单的形式。
在AOP的领域中,COM+侦听器就是通过元数据与组件相关联的一些方面。运行时使用元数据来构造这组方面,通常是在创建对象时进行的。当客户端调用方法时,那些特殊的方面依次获得了处理调用、执行其服务的机会,最后再调用对象的方法。返回时,每个方面又有机会进行展开。这样,就可以将每个组件的每个方法中都要编写的同样代码行提取出来并放在各个方面中,让运行时来放置它们。这组方面共同为组件方法的执行提供了上下文。上下文在环境中提供了方法的实现,其中的操作具有特殊的意义。
例如,图1显示了一个安全地存在于上下文中的对象,该上下文提供了错误传播、事务管理和同步。Win32控制台应用程序中的程序能够假定在上下文中存在控制台,而且调用printf的结果将显示在控制台上,与之一样,AOP对象也可以假设事务已经建立,且该事务包含调用数据库这一部分。如果设置这些服务出现了问题(例如没有足够资源建立事务),对象将不会被调用,因此也就无须担心了。

常规用途AOP
虽然COM+提供了AOP所需的大多数服务,但是若要用来作为常规用途AOP环境,它还缺乏一个必需的重要细节:定义自定义方面的能力。例如,如果基于角色的安全性不适合的话,就不能实现基于角色扮演的安全性(如同让最危险的人保护自己的对象)。如果程序员有这种能力,许多COM惯用法都可以用AOP框架实现。图2提供了示例的简短列表。

异常和错误处理
事务管理
日志的方法调用
异步方法调用
安全检查
扩展二进制组件自动化,使之具有expando对象的能力
类似Eiffel语言的契约式设计
参数校验

图2 针对AOP的COM惯用法

设计方面框架
当然,有了这样的框架构思之后,我们还必须把它构建出来。我们希望这个框架具备以下功能:
q 将客户端和对象之间的方面串联起来的运行时。
q 用户定义的方面,以COM组件实现。
q 有关哪个方面与每个COM组件相关联的元数据描述,如COM+目录一样。
q 在方面就绪时客户端可以用来激活组件的方法。

有关AOP框架的概念很简单。关键就是侦听和委托。侦听的技巧在于,让调用者相信它持有的接口指针指向它所请求的对象,而实际上这是一个指向侦听器的指针,可以通过本文后面讲述的激活技术之一获取该侦听器。侦听器需要实现与目标组件相同的接口,但是需要通过与组件相关联的方面堆栈来委托所有调用。在调用方法时,侦听器将为每个方面提供预处理和后处理调用的机会,以及传播或者取消当前调用的能力。
AOP框架执行两个不同的步骤,组件激活和方法调用。在组件激活中,AOP框架构建方面实现对象的堆栈,并返回一个侦听器的引用,而不是实际对象的引用。在方法调用中,当调用者对侦听器进行方法调用时,侦听器将调用委托给所有已经注册的方面,从而对调用堆栈中的[in]和[in,out]参数进行预处理,并将实际调用提供给对象。然后通过传递组件返回的调用返回值,以及调用堆栈上的[in,out]和[out]参数,将调用提供给方面进行后处理。

作为COM对象的方面
在我们的AOP框架中,方面是实现了IAspect接口的COM类,如图3所示。


interface IAspect : IUnknown {
HRESULT PreProcess( [in] IUnknown* pUnkDelegatee,
[in] BSTR riid,
[in] BSTR strMethodName,
[in] long nvtblSlot,
[in] IUnknown* pEnum);
HRESULT PostProcess([in]HRESULT hrOriginal,
[in] IUnknown* pUnkDelegatee,
[in] BSTR riid,
[in] BSTR strMethodName,
[in] long nvtblSlot,
[in] IUnknown* pEnum);
}

框架在将方法调用传递给实际的底层组件实例(以下称之为被委托者)之前,会调用所有指定方面的IAspect::PreProcess方法。它把被委托者的身份、接口的IID、方法名、方法发生的vtbl槽以及对[in]和[in,out]参数的枚举器传递给相应的方面。如果方面从PreProcess返回故障HRESULT,框架就不把调用提供给被委托者,实际上是取消了调用。
方面预处理成功返回后,框架将实际调用提供给被委托者。无论是否从被委托者返回HRESULT,框架都会调用IAspect::PostProcess方法,传递被委托者返回的HRESULT和PostProcess方法需要的所有参数,只不过这一次枚举器是建立在[out], [in,out]和[out,retval]参数之上的。
图4显示了如何编写调用跟踪方面,它能够跟踪传递给被委托者方法的所有调用者提供的参数。
图4 Call-tracing Aspect

class CCallTracingAspect : public IAspect, ... {
public:
BEGIN_CATEGORY_MAP(CCallTracingAspect)
IMPLEMENTED_CATEGORY(CATID_Aspects)
END_CATEGORY_MAP()

STDMETHODIMP PreProcess(...)
{ return DumpStack ( true, riid, strMethodName, pEnum ) ; }

STDMETHODIMP PostProcess(...)
{ return DumpStack ( false, riid, strMethodName, pEnum ) ; }

HRESULT DumpStack(bool preProcess, BSTR riid,
BSTR strMethodName, IUnknown *pEnum) {
if (preProcess) ATLTRACE("PreProcessing: %S(", strMethodName);
else ATLTRACE("PostProcessing: %S(", strMethodName);

CComPtr<IEnumVARIANT> spEnumVar;
pEnum->QueryInterface(&spEnumVar);
CComVariant v;
bool bNeedComma = false;
while (spEnumVar->Next(1, &v, 0) == S_OK) {
if (bNeedComma) ATLTRACE(", ");
else bNeedComma = true;
ATLTRACE("%S", ToString(v));
}
ATLTRACE(")\n");
return S_OK ;
}
...
};

既然已经有了一个用来调用方面的框架和一个可以使用的方面,我们需要一种机制将它们串联起来。这种操作在对象激活时进行。

对象激活
尽管我们要将客户端和对象之间任意数量的方面堆放起来,客户端应该可以创建对象并调用其方法,正如没有侦听时的方式一样。糟糕的是,COM如果不采取一些奇特的技术手段(这正是Microsoft事务服务在集成到COM基础结构中并改名为COM+之前必须实现的),就无法支持在其主激活API CoCreateInstance中注入的任意扩展性代码。但是,COM确实提供了完全扩展的激活API:Visual Basic中的GetObject(对于C++程序员是CoGetObject)。我们使用自定义名字对象基于该API构建AOP激活代码。
COM名字对象是将任意字符串(称为显示名)转换为COM对象的一段代码,这意味着可以创建一个新的,或者从文件中找到一个,甚至从月球上下载。我们的AOP名字对象获得元数据(描述与此处谈论的类相关联的方面),创建该类的实例,构造方面堆栈,通过AOP侦听器将它们串联在一起,然后将侦听器返回给客户端。下面是一个示例:

Private Sub Form_Load()
Set myfoo = GetObject("AOActivator:c:\AopFoo.xml")
myfoo.DoSomethingFooish
End Sub

请注意,除了获取Foo的实例,客户端使用组件不需要任何特殊操作。尽管AopFoo.xml文件会将任意数量的方面与Foo的该特定实例关联起来,它还是实现相同的接口,更重要的是,它具有相同的语义。
实现自定义COM名字对象在某种意义上是一种神奇的技术,主要涉及以前OLE细节的内幕知识。幸运的是,实现的大部分内容都是陈词滥调,而COM社区很久以前就把名字对象的基本实现都写进了一个称为CComMoniker的ATL类中。(可以访问 http://www.sellsbrothers.com/tools获取该COM名字对象框架。)使用这个框架,我们真正需要关心的就是实现ParseDisplayName(这是一个分析自定义显示名语法的枯燥方法)和BindToObject(属于名字对象的一部分,该名字对象用于激活客户端提供的显示名称所指示的COM对象)(参见图5)。
图5 Creating the Object in BindToObject

STDMETHODIMP CAopFactory::BindToObject(
IBindCtx* pbc, IMoniker* pmkToLeft, REFIID riidResult,
void** ppvResult) {
// ParseDisplayName has already pulled in the metadata from
// the XML file supplied by the client

// Create the object to be hosted in our AOP environment
CComPtr<IUnknown> spComp;
HRESULT hr = spComp.CoCreateInstance ( m_clsid ) ;
if (FAILED(hr)) return hr ;

// Create our interceptor and return it the client
... // Magic happens...
}

请注意,图5中的代码没有显示最困难的部分——侦听器的创建和初始化。之所以困难,不在于侦听器本身,而是侦听器必须要做的工作。请记住,我们的通用AOP框架要想能发挥作用,必须能够用与所包装的任何组件完全相同的一组接口来响应QueryInterface方法。而返回的接口必须能够获得每个方法的客户端所提供的调用堆栈,将其传递给所有方面,并一直传递到组件本身,保持参数完整-无论有多少,以及是何种类型。这是一项非常困难的任务,涉及到大量的__declspec(naked)和ASM thunk。
幸运的是,因为COM社区已经非常成熟,我们得以站在巨人的肩膀上使用通用委托器(UD),一个由Keith Brown创建的执行这一任务的COM组件。Keith曾在MSJ中分两部分撰文描述了其UD,文章名为“Building a Lightweight COM Interception Framework, Part I:The Universal Delegator”(http://www.microsoft.com/msj/0199/intercept/intercept.aspx[l1]),和Part II:“The Guts of the UD”(http://www.microsoft.com/msj/0299/intercept2/intercept2.aspx[l2])。我们可用Keith的UD实现AOP框架,这减少了BindToObject实现中“神奇”的部分,如图6所示。
图6 Rest of BindToObject Implementation

STDMETHODIMP CAopFactory::BindToObject(
IBindCtx* pbc, IMoniker* pmkToLeft, REFIID riidResult,
void** ppvResult) {
...
// Create our interceptor and return it the client

// Create and initialize our hook
CComObject<Chook>* pHook ;
hr = pHook->CreateInstance(&pHook) ;
if (FAILED(hr)) return hr ;

CComPtr<IDelegatorHookQI> spHook ;
hr = pHook->QueryInterface(&spHook) ;
if (FAILED(hr)) return hr ;

hr = pHook->SetAspects ( m_displayName, m_aspects.size(),
&m_aspects[0] ) ;
if (FAILED(hr)) return hr ;

// Create the UDFactory
CComPtr<IDelegatorFactory> spDel ;
hr = CoGetClassObject ( __uuidof(CoDelegator21), CLSCTX_INPROC, 0,
__uuidof(IDelegatorFactory), (void **)&spDel );
if (FAILED(hr)) return hr ;

// Create the interceptor
hr = spDel->CreateDelegator(0, spComp, 0, spHook, 0,
riidResult, ppvResult);
}

为了包装目标组件供客户端使用,执行以下四个步骤:
1. 使用组件的CLSID创建实际组件,该CLSID传递给原来在元数据XML文件中的名字对象。
2. 创建了一个DelegatorHook对象,侦听对对象的QueryInterface调用。挂钩负责将方法调用路由到每个方面。
3. 接下来,创建UD对象,检索IDelegatorFactory接口。
4. 使用IDelegatorFactory调用CreateDelegator,传递实际对象的接口、委托器挂钩、源调用者请求的接口的IID (riidResult),以及指向接口指针的指针(ppvResult)。委托器返回指向侦听器的指针,可以调用每个调用的委托器挂钩。
结果如图7所示。由此开始,客户端可以将侦听器用作目标组件的实际接口指针。调用后,它们沿着路径上的方面路由到目标组件。

Aspect Builder
为了激活组件,并使所有方面都正确的串联起来,我们的AOP名字对象依赖一个XML文件来描述组件和相关联的方面。其格式非常简单,只包含组件的CLSID和方面组件的CLSID。图8的示例包装了带有两个方面的Microsoft FlexGrid Control。
图8 Wrapping the Microsoft FlexGrid Control
<AOPFramework>
<Component Name="Microsoft FlexGrid Control, version 6.0">

<CLSID>{6262D3A0-531B-11CF-91F6-C2863C385E30}</CLSID>
</Component>
<Aspects>
<Aspect Name="Call-Tracing Aspect">
<CLSID>{49EFA33A-FDB2-4AED-807E-4D447D096642}
</CLSID>
</Aspect>
<Aspect Name="Synchronization Aspect">
<CLSID>{6DBA0579-8846-46A2-BEFF-382725A1022C}
</CLSID>
</Aspect>
</Aspects>
</AOPFramework>

为了简化AOP元数据实例的创建任务,我们创建了Aspect Builder(如图9所示)。
Aspect Builder会枚举机器上注册的所有方面,并在右边的列表视图中用云状图将它们都显示出来。Aspect Builder的客户端区域包含组件的图形表示。可以双击它(或者使用相应的菜单项)并指定组件的ProgID。选择了一个组件之后,可以将方面拖放到客户端区域中,将方面添加到组件的AOP元数据中。
要生成提供给AOP名字对象所必需的XML格式,可以在“Tools”菜单中选择“Compile”菜单项,元数据就会显示在底部窗格中。可以在Verify Aspects窗格中实际编写脚本,验证元数据确实正确。可以将已经编译的XML实例保存在磁盘上,也可以用Aspect Builder重载它们。

.NET中的方面
尽管Aspect Builder使工作大大简化,但是由于方面元数据与组件分开存储,这使得在COM中进行AOP编程并不方便。糟糕的是,COM的元数据在扩展性方面缺乏很多必要的功能,这正是我们感到首先需要将元数据和类分开存储的原因。但是,作为COM显然的继承者,.NET就没有这种问题。.NET的元数据是完全可扩展的,因此具备所有必要的基础,可以将方面通过属性直接与类本身相关联。例如,给定一个自定义的 .NET属性,我们就可以轻松地将调用跟踪属性与.NET方法相关联:

public class Bar {
[CallTracingAttribute("In Bar ctor")]
public Bar() {}

[CallTracingAttribute("In Bar.Calculate method")]
public int Calculate(int x, int y){ return x + y; }
}

请注意,方括号中包含CallTracingAttribute和访问方法时输出的字符串。这是将自定义元数据与Bar的两个方法相关联的属性语法。
与COM中的AOP框架一样,.NET中的属性根据.NET中的组件分类。.NET中的自定义属性是用派生自Attribute的类实现的,如下所示:

using System;
using System.Reflection;

[AttributeUsage( AttributeTargets.ClassMembers,
AllowMultiple = false )]
public class CallTracingAttribute : Attribute {
public CallTracingAttribute(string s) {
Console.WriteLine(s);
}
}

我们的属性类本身也有属性,这些属性会修改其行为。在这种情况下,我们要求该属性只与方法,而不是程序集、类或者字段关联,并且每个方法只能有一个跟踪属性与之关联。
将属性与方法关联以后,我们就成功了一半。为了提供AOP功能,还需要在每个方法建立执行组件所必需的环境的前后访问调用堆栈。这就需要一个侦听器,以及组件赖以生存的上下文。在COM中,我们要求客户端使用AOP名字对象激活组件,从而实现该任务。幸运的是,.NET已经内置了挂钩,因此客户端无须做任何特殊的工作。

上下文绑定对象
.NET中侦听的关键(与COM中一样)在于要为COM组件提供上下文。在COM+和自定义的AOP框架中,通过在客户端和对象之间堆放方面,在方法执行之前为组件建立上下文的属性来提供上下文。而在.NET中,将为任何从System .ContextBoundObject派生的类提供上下文:
public class LikeToLiveAlone : ContextBoundObject {...}
当LikeToLiveAlone类的实例被激活时,.NET运行时将自动创建一个单独的上下文供其生存,并建立一个侦听器,可以从中挂起我们自己的方面。.NET侦听器是两个对象的组合-透明代理和真实代理。透明代理的行为与目标对象相同,与COM AOP侦听器也一样,它将调用堆栈序列化为一个称为消息的对象,然后再将消息传递给真实代理。真实代理接收消息,并将其发送给第一个消息接收进行处理。第一个消息接收对消息进行预处理,将其继续发送给位于客户端和对象之间的消息接收堆栈中的下一个消息接收,然后对消息进行后处理。下一个消息接收也如此照办,以此类推,直到到达堆栈构建器接收,它将消息反序列化回调用堆栈,调用对象,序列化出站参数和返回值,并返回到前面的消息接收。这个调用链如图10所示。
为了参与到这个消息接收链中,我们首先需要从ContextAttribute(而不只是Attribute)派生属性,并提供所谓上下文属性,将属性更新为参与上下文绑定对象:

[AttributeUsage(AttributeTargets.Class)]
public class CallTracingAttribute : ContextAttribute {
public CallTracingAttribute() :
base("CallTrace") {}
public override void
GetPropertiesForNewContext
(IConstructionCallMessage ccm) {
ccm.ContextProperties.Add(new
CallTracingProperty());
}
...
}

当激活这个对象时,为每个上下文属性调用GetPropertiesForNewContext方法。这样我们就能将自己的上下文属性添加到与为对象创建的新上下文关联的属性列表中。上下文属性允许我们将消息接收与消息接收链中的对象关联起来。属性类通过实现IContextObject和IContextObjectSink作为方面消息接收的工厂:

public class CallTracingProperty : IContextProperty,
IContributeObjectSink {
public IMessageSink GetObjectSink(MarshalByRefObject o,
IMessageSink next) {
return new CallTracingAspect(next);
}
...
}
代理创建属性的过程如图11所示,其中先创建上下文属性,然后创建消息接收。

.NET方面
当一切都正确附加后,每个调用都会进入方面的IMessageSink实现。SyncProcessMessage允许我们预处理和后处理消息,如图12所示。
图12 .NET Call-tracing Aspect

internal class CallTracingAspect : IMessageSink {
private IMessageSink m_next;
private String m_typeAndName ;

internal CallTracingAspect(IMessageSink next) {
// Cache the next sink in the chain
m_next = next;
}

public IMessage SyncProcessMessage(IMessage msg) {
Preprocess(msg);
IMessage returnMethod = m_next.SyncProcessMessage(msg);
PostProcess(msg, returnMethod);
return returnMethod;
}

private void Preprocess(IMessage msg) {
// We only want to process method calls
if (!(msg is IMethodMessage)) return;

IMethodMessage call = msg as IMethodMessage;
Type t = Type.GetType(call.TypeName) ;
m_typeAndName = t.Name + "." + call.MethodName ;
Console.Write("PreProcessing: " + m_typeAndName + "(");

// Loop through the [in] parameters
for (int i = 0; i < call.ArgCount; ++i) {
if (i > 0) Console.Write(", ");
Console.Write(call.GetArgName(i) + "= " + call.GetArg(i));
}
Console.WriteLine(")");

// set us up in the callContext
call.LogicalCallContext.SetData(ContextName, this);
}

private void PostProcess(IMessage msg, IMessage msgReturn)
{
// We only want to process method return calls
if (!(msg is IMethodMessage) ||
!(msgReturn is IMethodReturnMessage)) return;

IMethodReturnMessage retMsg = (IMethodReturnMessage)msgReturn;
Console.Write("PostProcessing: ");
Exception e = retMsg.Exception;
if (e != null) {
Console.WriteLine("Exception was thrown: " + e);
return;
}

// Loop through all the [out] parameters
Console.Write(m_typeAndName + "(");
if (retMsg.OutArgCount > 0) {
Console.Write("out parameters[");
for (int i = 0; i < retMsg.OutArgCount; ++i ) {
if (i > 0) Console.Write(", ");
Console.Write(retMsg.GetOutArgName(i) + "= " +
retMsg.GetOutArg(i));
}
Console.Write("]");
}
if (retMsg.ReturnValue.GetType() != typeof(void))
Console.Write("returned [" + retMsg.ReturnValue + "]");

Console.WriteLine(")");
}
...
}

最后,希望将自己与调用跟踪方面相关联的上下文绑定类使用CallTracingAttribute声明其首选项:

[AOP.Experiments.CallTracingAttribute()]
public class TraceMe : ContextBoundObject {
public int ReturnFive(String s) {
return 5;
}
}

请注意,我们把上下文属性与类而非每个方法相关联。.NET上下文体系结构将自动通知我们每个方法,因此我们的调用跟踪属性拥有所有需要的信息,这为我们避免了以前处理普通属性时,需要手工将属性和每个方法相关联的麻烦。当客户端类实例化类并调用一个方法时,方面就被激活了:

public class client {
public static void Main() {
TraceMe traceMe = new TraceMe();
traceMe.ReturnFive("stringArg");
}
}

运行时,客户端和面向方面的对象输出如下内容:
PreProcessing: TraceMe.ReturnFive(s= stringArg)
PostProcessing: TraceMe.ReturnFive( returned [5])


方面和上下文
迄今为止,我们这个简单的方面还没能真正实现预期的AOP理想。尽管方面确实可以用来对方法调用进行单独的预处理和后处理,但真正有趣的还是方面对方法执行本身的影响。例如,COM+事务性方面会使对象方法中使用的所有资源提供程序参与同一个事务,这样方法就可以仅通过中止COM+事务性方面提供的事务来中止所有活动。为此,COM+方面增加了COM调用上下文,后者为有兴趣访问当前事务的所有组件提供了聚集点。同样,.NET也提供了可以用来允许方法参与的可扩展的调用上下文。例如,可以通过将自身置于.NET上下文中,使对象(它封装在调用跟踪方面中)能够在跟踪消息流中添加信息,如下所示:
internal class CallTracingAspect : IMessageSink {
public static string ContextName {
get { return "CallTrace" ; }
}

private void Preprocess(IMessage msg) {
...
// set us up in the call context
call.LogicalCallContext.SetData(ContextName, this);
}
...
}
一旦将方面添加到调用上下文后,方法可以再次将其抽出,并参与到跟踪中:
[CallTracingAttribute()]
public class TraceMe: ContextBoundObject
public int ReturnFive(String s)
Object obj =
CallContext.GetData(CallTracingAspect.ContextName) ;
CallTracingAspect aspect = (CallTracingAspect)obj ;
aspect.Trace("Inside MethodCall");
return 5;
}
通过提供一种方式来增加调用上下文,.NET允许方面为对象设置真实的环境。在我们的例子中,允许对象向流添加跟踪语句,而无需知道流的目的地、如何建立流以及何时销毁流,这很像COM+的事务性方面,如下所示:
PreProcessing: TraceMe.ReturnFive(s= stringArg)
During: TraceMe.ReturnFive: Inside MethodCall
PostProcessing: TraceMe.ReturnFive( returned [5])

小结
借助面向方面编程,开发人员可以用与封装组件本身相同的方式跨组件封装公共服务的使用。通过使用元数据和侦听器,可以在客户端和对象之间放置任意服务,这种操作在COM中是半无缝的,在.NET中是无缝的。本文介绍的方面可以在进出方法调用的过程中访问调用堆栈,它们提供增加了的上下文,供对象在其中生存。虽然与结构化编程或者面向对象编程相比AOP尚不成熟,但是.NET中对AOP的本地支持为我们追求像乐高玩具那样的软件梦想提供了宝贵的工具。
posted on 2005-08-10 13:20  wanna  阅读(1184)  评论(0编辑  收藏  举报