[简介]
面向方面编程(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,框架就不把调用提供给被委托者,实际上是取消了调用。
方面预处理成功返回后,框架将实际调用提供给被委托者。无论是否从被委托者返回HR