面向对象技术很好地解决了软件系统中角色划分的问题。借助于面向对象的分析、设计和实现技术,开发者可以将问题领域的“名词”转换成软件系统中的对象,从而很自然地完成从问题到软件的转换.
但是,问题领域的某些需求却偏偏不是用这样的“名词”来描述的.我的一个朋友就曾经遇到这样的问题:需要对系统中的某些方法进行日志记录,这种需要记录方法散布在40多个类中。面对这种需求,应该怎么办呢?最直接的办法就是:创建一个起类(或接口),将日志的功能放在其中,并让所有需要日志功能的类继承这个起类(或接口).如果这个需求是后期提出的.需要修改的地方就会分散在40多个文件(如果是C+十,这个数量还可能加倍)中。这样大的修改量,无疑会增加出错的几率,并且加大系统维护的难度。
人们认识到,传统的程序经常表现出一些不能自然地适合单个程序模块或者几个紧密相关的程序模块的行为 例如日志记录、对上下文敏感的错误处理、性能优化以及设计模式等等、我们将这种行为称为“横切关注点(crosscuttingconcern)”,因为它跨越了给定编程模型中的典型职责界限。如果使用过用于核切关注点的代码,您就会知道缺乏模块性所带来的问日。因为横切行为的实现是分散的,开发人员发现这种行为难以作逻辑思维、实现和更改.
因此,面向方面的编程(Aspect-OrientedProgramming,AOP)应运而生。AOP为开发者提供了一种描述横切关注点的机制,并能够自动将横切关注点织入到面向对象的软件系统中,从而实现了横切关注点的模块化.通过划分Aspect代码,横切关注点变得容易处理。开发者可以在编译时更改、插入或除去系统的Aspect,甚至重用系统的Aspect.
更重要的是,AOP可能对软件开发的过程造成根本性的影响。我们可以想象这样一种情况:OOP只用于表示对象之间的泛化一特化(generalization-specialization)关系(通过继承来表现),而对象之间的校向关联则完全用AOP来表现。这样,很多给对象之间横向关联增加灵活性的设计模式(例如Decorator、Role Object等)将不再必要.
一种编程思想是否真正优秀,只有从实现语言上才能看到。施乐公司帕洛阿尔托研究中心(Xerox PARC)开发了第一个AOP的开发环境——AsPectJ ,这个工具提供了一整套的语法,能够清楚地描述横切关注点,并将其织入到Java源代码中。织入后的代码仍是标准Java代码,因此AspectJ不会影响Java的移植能力。此外,AspectJ提供了一个独立的IDE,并且能够嵌入到Jbuilder、Forte等Java开发环境之中,无缝地提供AOP的能力。关于AspectJ,读者可以在http://www.aspectj.org找到更多的信息。
但是,现在的AOP还处于相当不完善的阶段:它只能应用于很少的几种语言环境下,并且必须掌握源代码才能进行织入.但以RUP之父Ivar Jacobson为代表的科学家们仍对AOP推崇备至:他认为AOP将最终改变整个软件开发的方式,并且更完美地实现“用例驱动”的开发思想.
利用AOP分离软件关注点
一个关注点(concern)就是一个特定的目的,一块我们感兴趣的区域。从技术的角度来说,一个典型的软件系统包含一些核心的关注点和系统级的关注点。举个例子来说,一个信用卡处理系统的核心关注点是借贷/存入处理,而系统级的关注点则是日志、事务完整性、授权、安全及性能问题等,许多关注点——我们叫它横切关注点(crosscutting concerns)——会在多个模块中出现,使用现有的编程方法,横切关注点会横越多个模块,结果是使系统难以设计、理解、实现和演进。
AOP能够比上述方法更好地分离系统关注点,从而提供模块化的横切关注点。
在这篇文章里,我首先会解释横切关注点在软件系统中引起的问题,接着我会介绍AOP是怎样解决横切关注点问题的。
软件编程方法的演进在计算机科学的早期阶段,开发人员使用简单的机器级代码来编程。不幸的是,程序员得花费更多时间来考虑一种特定机器的指令集而不是手中需要解决的问题本身。慢慢地,我们转而使用允许对底层机器做某种抽象的高级语言。然后是结构化语言,我们可以把问题分解成一些必要的过程来完成任务。但是,随着复杂程度的增加,我们又需要更适合的技术。面向对象的编程方式(OOP)使我们可以把系统看作是一批相互合作的对象。类允许我们把实现细节隐藏在接口下。多态性为相关概念提供公共的行为和接口,并允许特定的组件在无需访问基础实现的前提下改变特定行为。
编程方法和语言决定了我们和计算机交流的方式。每一种新的方法学都提出一种新的分解问题的方法:机器码、伪代码、过程和类等。每种新的方法学都使得从系统需求到编程概念的映射更加自然。编程方法学的发展让我们可以建立更加复杂的系统,这名话反过来说也地,我们能够建立更加复杂的系统是加为这些技术允许我们处理这种复杂度。
现在,大多数软件项目都选择OOP的编程方式。确实,OOP已经表明了它处理一般行为的能力,但是,我们将会看到(或许你已经感觉到了):OOP不能很好地处理横越多个——经常是不相关的——模块的行为。相比之下,AOP填补了这个空白,它很可能会是编程方法学发展的一个里程碑。
把系统看作一批关注点我们可以把一个复杂的系统看作是由多个关注点来组合实现的。一个典型的系统可能会包括几个方面的关注点,如业务逻辑、性能,数据存储、日志和调度信息、授权、安全、线程、错误检查等,还有开发过程中的关注点,如易懂、易维护、易追查、易扩展等,图1演示了由不同模块实现的一批关注点组成一个系统。
图2把需求比作一束穿过三棱镜的光。我们让需求之光通过鉴别关注点的三棱镜,就会区别出每个关注点。
开发人员建立一个系统以满足多个需求,我们可以大致地把这些需求分类为核心模块级需求和系统组需求。很多系统级需求一般来说是相互独立的,但它们一般都会横切许多核心模块。举个例子来说,一个典型的企业应用包含许多横切关注点,如验证、日志、资源地、系统管理、性能及存储管理等,每一个关注点都牵涉到几个子系统,如存储管理关注点会影响到所有的有状态业务对象。
让我们来看一个简单的例子,考虑一个封装了业务逻辑的类的实现框架:
横切关注点的问题虽然横切关注点会跨越多个模块,但当前的技术倾向于使用一维的方法学来处理这种需求,把对应需求的实现强行限制在一维的空间里.这个一维空间就是核心模块级实现.其他需求的实现被嵌入在这个占统治地位的空间.换句话说,需求空间是一个n维空间,而实现空间是一维空间,这种不匹配导致了糟糕的需求到实现的映射。
表现用当前方法学实现横切关注点是不好的.它会带来一些问题,我们可以大致把这些问题分为两类。
代码混乱:软件系统中的模块可能要同时兼顾几个方面的需要.举例来说,开发者经常要同时考虑业务逻辑、性能、同步,日志和安全等问题,兼顾各方面的需要导致相应 关注点的实现元素同时出现,引起代码混乱.
代码分散:由于横切关注点本来就涉及到多个模块.相关实现也就得遍布在这些模块里.如在一个使用了数据库的系统里,性能问题就会影响所有访问数据库的模块。这导致代码分散在各处.
结果混乱和分散的代码会从多个方面影响系统的设计和开发:
可读性差:同时实现几个关注点模糊了不同关注点的实现,使得关注点与其实现之间的对应关系不明显。
低产出:同时实现几个关注点把开发人员的注意移到外围关注点,导致生产效率降低.
低代码重用率: 由于一个模块实现多个关注点,因此其他需要类似功能的系统不能马上使用该模块.进一步降低了生产效率。
代码质量差:混乱的代码掩盖了代码中隐藏的问题。而且,由于同时要处理多个关注点.应该特别注意的关注点得不到应有的关注.
难以扩展:狭窄的视角和有限的资源总是产生仅注意当前关注点的设计.新的需求导致从新实现.由于实现不是模块化的,就是说实现牵涉到多个模块,为了新需求修改子系统可能会带来数据的不一致,而且还需相当规模同测试来保证这些修改不会带来bug。
当前解决方法由于多数系统中都包含横切关注点、自然的已经形成了一些技术来模块化横切关注点的实现,这些技术包括:混入类、设计模式和面向特定问题域的解决方式。
使用混入类,你可以推迟关注点的最终实现.基本类包含一个混入类的实例,允许系统的其他部分设置这个实例。举个例子来说,实现业务逻辑的类包含一个混入的logger,系统的其他部分可以设置这个logger已得到合适的日志类型,比如logger可能被设置为使用文件系统或是消息中间件。在这种方式下,虽然日志的具体实现被推迟,基本类还是必须在所有写日志的点调用日志操作和控制日志信息的代码。
行为型设计模式,如Visitor和Template Method模式,也允许你推迟具体实现。但是也就像混入类一样,操作的控制——调用visitor或template Method的逻辑——仍然留给了基本类。
面向特定问题域的解决方式,如框架和应用服务器,允许开发者用更模块化的方式处理某些横切关注点。比如E J B(Enterprise JavaBean)架构,可以处理安全、系统管理、性能和容器管理的持久化(container-managed persistence)等横切关注点。B e a n与数据库的映射,但是大多数情况下,开发者还是要了解存储结构。这种方式下,你用基于XML的映射关系描述器来实现于数据持久化相关的横切关注点。
面向特定问题域的解决方式提供了解决特定问题的专门机制,它的缺点是对于每一种这样的解决方式开发人员都必须重新学习,另外,由于这种方式是特定问题域相关的,属于特定问题域之外的横切关注点需要特殊的对待。
设计师的两难局面。好的系统设计师不仅会考虑当前需求,还会考虑到可能会有的需求以避免到处打补丁。这样就存在一个问题预知将来是很困难的,如果你漏过了将来可能会有的横切关注点的需求,你将会需要修改或甚至是重新实现系统的许多部分;从另一个角度来说,太过于关注不一定需要的需求会导致过分设计的、难以理解的、臃肿的系统。所以系统设计师处在这么一个两难局面中:怎么设计算不了过分设计?应该宁可设计不足还是宁可过分设计?
举个例子来说,设计师是否应该在系统中包含现在并不需要的日志机制?如果是的话,哪里是应该写日志的点?日志应该记录那些信息相似的例子还有关于性能的优化问题,我们很少能预知瓶颈的所在。常用的方法是建立系统,profile它,然后翻新系统以提高性能,这种方式可能会依照profiling而修改系统的很多部分。此外,随着时间的流逝,由于使用方式的变化,可能还会产生新的瓶颈。类库设计师的任务更困难,因为他很难设想出所有对类库的使用方式。
总而言之,设计师很难顾及到系统可能需要处理的所有关注点。即使是在已经知道了需求的前提下,某些建立系统时需要的细节 也可能不能全部得到,整体设计就面临着设计不足/过分设计的两难局面。
AOP基础
到目前为止的讨论说明模块化横切关注点是有好处的。研究人员已经尝试了多种方法来实现这个任务,这些方法有一个共同的主题:分离关注点。A O P是这些方法中的一种,它的目的是清晰的分离关注点来解决以上提到的问题。
AOP, 从其本质上讲,使你可以用一种松散耦合的方式来实现独立的关注点,然后 组合这些实现来建立最终系统、用它所建立的系统是使用松散偶合的,模块化实现的横切关注点来搭建的、与之对照 用OOP建立的系统则是用松散耦合的模块化实现的一般关注点来实现的。在AOP中,这些模块化单元叫“方面(aspect)”,而在OOP中,这些一般关注点的实现单元叫做类。
AOP 包括三个清晰的开发步骤:
方面分解:分解需求撮出横切关注点。在这一步里,你把核心模块级关注点和系统级的横切关注点分离开来、就前面所提到的信用卡例子来说,你可以分解出三个关注点:核心的信用卡处理、日志和验证。
关注点实现:各自独立的实现这些关注点,还用上面信用卡的例子,你要实现信用卡处理单元、日志单元和验证单元。
方面的重新组合:在这一步里,方面集成器通过创建一个模块单元—一方面来指定重组的规则,重组过程—一也叫织入(weaving)或结合(integrating)——则使用这些信息来构建最终系统、还拿信用卡的那个例子来说你可以指定(用某种AOP的实现所提供的语言)每个操作的开始和结束需要记录,并且每个操作在涉及到业务逻辑之前必须通过验证。
AOP与OOP最重要的不同在于它处理横切关注点的方式.在AOP中 每个关注点的实现都不知道其它关注点是否会“关注”它,如信用卡处理模块并不知道其它的关注点实现正在为它做日志和验证操作。它展示了一个从OOP转化来的强大的开发范型。
注意:一个AOP实现可以借助其它编程范型作为它的基础,从而原封不动的保留其基础范型的优点。例如,AOP可以选择OOP作为它的基础范型,从而把OOP善于处理一股关注点的好处直接带过来。用这样一种实现,独立的一般关注点可以使用OOP技术、这就像过程型语言是许多OOP语言的基础一样。
AOP语言剖析
就像其他编程范型的实现一样,AOP的实现由两部分组成:语言规范和实现。语言规范描述了语言的基础单元和语法;语言实现则按照语言规范来验证代码的正确性,并把代码转成目标机器可执行的形式。这一节,我来解释一下AOP组成部分。
AOP语言规范
从抽象的角度看来 一种AOP语言要说明下面两个方面:
关注点的实现:把每个需求映射为代码,然后,编译器把它翻译成可执行代码。由于关注点的实现以指定过程的形式出现,你可以使用传统语言如C、C++、JAVA等。
织入规则规范:怎样把独立实现的关注点组合起来形成最终系统呢?为了这个目的 需要建立一种语言来指定组合不同的实现单元,以形成最终系统的规则。这种指定织入规则的语言可以是实现语言的扩展,也可以是一种完全不同的语言。
AOP语言的实现
AOP的编译器执行两步操作:
l 组装关注点
2 组装结果转成可执行代码。
AOP实现可以用多种方式实现织入, 包括源码到源码的转换、它预处理每个方面的源码,产生织入过的源码,然后把织入过的源码交给基础语言的编译器, 产生最终可执行代码。比如 使用这种方式,一个基于Java的AOP实现可以先把不同的方面转化成Java源代码,然后让Java编译器把它转化成字节码.也可以直接在字节码级别执行织入; 毕竟 字节码本身也是一种源码。此外,底层的执行系统—一Java虚拟机—一也可以设计为支持AOP的。基于Java的AOP实现如果使用这种方式的话,虚拟机可以先装入织入规则,然后对后来装入的类都应用这种规则、也就是说,它可以执行just-in-time的方面织入。
AOP的好处
AOP可帮助我们解决上面提到的代码混乱和代码分散所带来的问题,它还有一些别的好处:
模块化横切关注点:AOP用最小的耦合处理每个关注点,使得即使是横切关注点也是模块化的。这样的实现产生的系统,其代码的冗余小。模块化的实现还使得系统容易理解和维护。
系统容易扩展:由于方面模块根本不知道横切关注点,所以很容易通过建立新的方面加入新的功能.另外,当你往系统中加入新的模块时,已有的方面自动横切进来,使系统易于扩展。
设计决定的迟绑定:还记得设计师的两难局面吗?使用AOP 设计师可以推迟为将来的需求作决定,因为他可以把这种需求作为独立的方面很容易地实现。
更好的代码重用性:AOP把每个方面实现为独立的模块, 模块之间是松散耦合的.举例来说,你可以用另外一个独立的日志写入器方面来替换当前的,用于把日志写入数据库,以满足不同的日志写入要求。松散藕合的实现通常意味着更好的代码重用性,AOP在使系统实现松散出合这一点上比OOP做得更好。
AspectJ:一个Java的AOP实现
AspectJ是一个可免费获得的、由施乐公司帕洛阿尔托研究中心(Xerox PARC)开发的、Java的AOP实现。它使用java作为单个关注点的实现语言,并扩展Java以指定织入规则.
另外,AspectJ允许以多种方式用方面和类建立新的方面,你可以引入新的数据成员和方法或是声明一个新的类来继承和实现另外的类或接口。
AspectJ的织入器——AspectJ的编译器——负责把不同的方面组合在一起,由于AspectJ编译器建立的最终系统是Java字节码,因此,它可以运行在任何符合Java标准的虚拟机上。而且,AspectJ还提供了一些工具,例如调试器和Java IDE集成等。
我需要AOP吗?
AOP仅仅是解决设计上的缺点吗?在AOP里,每个关注点的实现的并不知道是否有其它关注点关注它,这是AOP和OOP的主要区别。在AOP里,组合的流向是从横切关注点到主关注点,而OOP则相反。但是,OOP可以和AOP很好地共存。比如,你可以使用一个混入类来做组合,既可以用AOP实现,也可以用OOP实现,这取决你对AOP的接受程度,在这两种情况下,实现横切关注点的混入类实现都无需知道它自己是被用在类中还是被用在方面中。举个例子来说,你可以把一个日志写入器接口用作某些类的混入类或是用作一个日志方面,因而,从OOP到AOP 是渐进的。
了解AOP
在这篇文章里,你看到了横切关系带来的问题,这些问题当前解决方法以及这些方法的缺点。你也看到了AOP 是怎样克服这些缺点的,AOP的编程方式试图模快化横切关注点的实现,提供了一个更好更快的软件开发方式。
如果你的系统中涉及到多个横切关注点,你可以考虑进一步了解AOP,了解它的实现和它的好处。AOP很可能会是编程方式的一个里程碑。