《编程机制探析》第二十一章 AOP

《编程机制探析》第二十一章 AOP

第二十一章 AOP

程序设计的一个重要目标就是提高重用性,避免重复代码。
到目前为止,我们已经接触到了诸多重用手段——过程式编程,面向对象编程,函数式编程,泛型编程,设计模式,等等。
本章介绍一种新的重用手段——面向方面编程(Aspect Oriented Programming),简称AOP。
什么叫做方面(Aspect)?这个词很难从字面上解释。我们还是通过具体的应用场景来看理解AOP的概念。
在程序设计中,我们虽然有各种手段来消除重复代码,但是,总有那么一些地方,遍布着重复代码,而那些地方,是我们现有的重用手段无法涉及的领域。我给那些地方起个名字,叫做设计死角。
那么,有哪些地方是设计死角呢?让我们来看几个具体的例子。
第一个例子,程序运行日志(Log)。
为了方便程序员(或者管理员)追踪应用程序的运行状态,程序员通常在程序代码中加入日志记录功能。
在原始的年代里,程序员大量使用print语句来输出程序运行的当前状态。虽然现在还是有很多程序员这么做。但是,这种做法已经是被摒弃的。正确的做法是使用日志(Log)编程接口。
通过日志(Log)编程库,我们可以定义日志开关,决定是否记录日志;我们可以定义日志存放位置,屏幕、文件、或者数据库;我们可以定义日志格式,简单文本、XML或者HTML;我们可以定义日志内容,决定哪些内容记录,哪些内容不记录;对于多个模块,我们可以定义多种日志记录方式;总之,日志编程库的好处不胜枚举。
现代程序员基本上都用日志编程库进行日志记录。但是,他们使用日志编程库的手段还是那么原始,他们照样把日志记录语句写得到处都是,遍布程序中各个角落。
那些日志记录语句就是重复代码,那些日志记录语句所在之地,就是设计死角。因为,日志记录遍及所有的对象类型中,我们无法用现有的设计手段进行统一设计。
第二个例子,数据库事务(Transaction)。
数据库是应用开发中最常用到的存储结构。数据库事务是操作数据库中最常遇到的问题。
这里简单地介绍一下数据库事务的概念。
数据库事务的和“原子操作”的概念有些相似。
在数据库中,我们执行一件任务,涉及到改动多处数据,我们希望这些改动,要么一次都成功,要么一切都保持原状。这样的一种“多个步骤合成一个原子步骤”的操作,就叫做数据库事务。
数据库事务分为两种——局部事务(Local Transaction)和全局事务(Global Transaction)。
局部事务中,涉及的数据库只有一个,涉及的数据也只在一个数据库中,这是我们在应用程序开发中最常遇到的事务。
局部事务的处理也很简单,就是把数据库的自动提交(Auto Commit)功能关掉,自己在所有数据操作都成功之后,在手动进行提交(Commit)处理;否则就进行回滚(Roll back)操作,一切恢复原状。
全局事务的情况就要复杂一些,涉及到多个数据库中的数据操作。这时候,就要保证所有数据库中的操作全都要同时成功提交,或者同时回滚、恢复原状。全局事务多见于一些拥有多个数据服务器的大型机构中。
全局事务的处理相当复杂,卷入全局事务的所有数据库,相互之间要进行复杂的通信和确认,才能保证全局事务的“原子性”。
当然,数据库事务的繁简与否,不是我们关心的问题。我们关心的是,这些事务代码分布在哪里。我们需要把事务代码分布到几乎所有的数据库操作代码中。
我们应该如何设计,才能消除这些重复的事务代码?要知道,进行操作数据库的对象各种各样,没有统一的接口定义。
这些事务代码遍及之处,就是设计死角。
看过了这两个例子,你应该对方面(Aspect)这个词有了感性体会。是的,Aspect就是设计死角。Aspect就是遍及在程序中的日志代码。Aspect就是遍及在程序中的事务代码。在AOP的概念中,这是两个不同的Aspect,分别叫做Log Aspect和Transaction Aspect。
AOP的目的就是把这些重复代码从程序中抽离出来,在程序编译或者运行的时候,再把这些重复代码自动“回填”程序中的对应位置,从而免除了程序员的重复劳动。
从AOP的目的,可以看出AOP模型中的两个主要组成部分——重复代码和回填位置。
重复代码就是日志代码、事务代码等。回填位置就是重复代码原来所在的位置,即设计死角,或者叫做Aspect。
AOP处理器(编译器或者解释器)的工作很简单,就是在所有的回填位置中,加入程序员定义的重复代码。
AOP用户的工作是什么?就是定义重复代码和回填位置。
代码部分不用说了,我们来看回填位置应该如何定义。
首先,我们需要明确,回填位置都可以包括哪些位置。
重复代码是否可以填入到程序中的任何一条语句的前面或者后面?可以说,这种想法只存在理论上的可行性。
实现上的效率且不说,就从用法上来说,也没有什么现实意义。
想象一下,我们需要给AOP编译器(或者解释器)定义这样一个任务:请把重复代码加入到方法f中的if(x > 0)语句之前。
为了描述“方法f中的if(x > 0)语句之前”这个位置,我们写的东西可能比重复代码还要长,还不如直接就把重复代码加到“方法f中的if(x > 0)语句之前”呢。
因此,这里有一个原则,回填位置的定义必须是简洁的。
既然语句级别不行,那么我们再上一级,到方法(过程、函数)的级别,这已经是代码块容身的最高级别了。如果这个级别也不行。那么,AOP也就别实现了。
幸运的是,这个级别是可行的。因为方法名简单易写,方法存身的类名、包名、模块名也很容易写,而且,还可以用通配符(*)来定义一批相似的回填范围。
AOP处理器(编译器或者解释器)根据回填位置定义,就可以把重复代码回填到方法(过程、函数)定义的前后。这个 “回填”的工作实际上相当于“篡改”了方法的原有行为,很像是黑客或者计算机病毒的行为。不过,AOP的“篡改”工作不是为了破坏,而是为了帮助。另外,“回填”这个动作,除了“篡改”这个别名之外,还有一个用得更广的别名,叫做“织入”(weave,编织),意思就是,把回填代码“编织”进原来的代码中。
AOP的“回填”动作的实现,基本上都是在Proxy Pattern(代理模式)的基础上实现的,即用一个Proxy对象包装原有对象的方法。例如:
LogProxy {
innerObject; // 真正的对象
f1() {
// Log here

innerObject.f1(); // 调用原来对象的对应方法

// Log here too
}
}
再例如:
TransactionProxy {
innerObject; // 真正的对象
f1() {
// start Transaction

innerObject.f1(); // 调用原来对象的对应方法

// finish Transacion
}
}
这些Proxy是可以叠加在一起的。叠加顺序由程序员自己指定。
当然,一个对象中的方法可能不止一个。这时候,我们就需要包装对象中所有需要AOP处理的方法。比如:
LogProxy {
innerObject; // 真正的对象
f1() {
// Log here

innerObject.f1(); // 调用真正的对象的对应方法

// Log here too
}

f2() {
// Log Hear

innerObject.f2(); // 调用真正的对象的对应方法

// Log here too
}
}
这里面还是存在重复代码。Bad Smell。我们可以利用Reflection机制,写一个通用的方法截获器。截获器的英文叫做Interceptor。这个词汇有拦截的意思。
还有种叫法,叫做劫持(Hijack)。我们经常听说,某种浏览器被某某木马插件劫持了。就是这个意思。为了避免这种不良联想,我们还是用Interceptor这个更通用的词汇。
MethodInterceptor{

around( method ){
before(method)

method.invoke(…); // 调用真正的对象方法

after(method, result)
}
}
Proxy可以继承这个MehtodInterceptor,实现before()和after()两个方法,把重复代码填进去。
函数式语言中,每个函数对象只有一个方法,用代理模式来包装,更加容易。从回填位置的声明上来说,回填位置定义中的通配符,等同于函数式编程中的模式匹配(Pattern Match)。从这两个方面看,函数式语言更有利于实现AOP。
Proxy Pattern + Method Reflection Interceptor + 代码自动生成(即回填)
这样一个三元组合,就是AOP的基本实现原理。AOP的实现多种多样,但是,其实现原理都脱不了这个范式。
AOP就像一个大染缸,本来干干净净的对象,经AOP一处理,就染上了(织入了)本来不属于它的特性。
AOP实现之间的区别,主要就在于代码自动生成(织入,填入)的方案。一般有两种方案。一种是在源代码上做文章,在编译器中加入插件,在编译源代码的过程中,把重复代码填进去(织进去),最后一起编译成目标代码。
一种是在目标代码上做文章。这种AOP直接处理目标代码,在目标代码中间填入(织入)重复代码。
这两种方案都是代码生成技术,我都不喜欢。实质上,重复代码还是分布在各处了。写在一处,复制到各处。
我喜欢第三种方案——在解释器或者虚拟机里做文章。当解释器或者虚拟机在执行代码的过程中,遇到回填位置,就会执行相应的重复代码。
这种实现方案就摆脱了代码生成技术,重复代码只有一处。但是,这种方案有一种致命的缺陷,那就是效率。
如果采取这种方案,那么,解释器或者虚拟机每次调用方法的时候,都需要查找“回填位置表”,看看这个方法是否在回填范围中。当然,我们可以做有限的优化。在第一次查找某方法之后,就在这个方法上贴上一个标签,比如,“无Interceptor”、“有LogInterceptor”、“有LogInterceptor、TransactionInterceptor”等。下一次,再遇到这个方法的时候,就可以不用查表了。但是,一个应用程序中的方法如此之多,这种优化的作用是很有限的。所以,这种方案是不现实的。我没有看到过这种方案,也没有想出解决思路。所以,目前的AOP还是代码生成技术的天下。
AOP的实现原理,讲到这里就结束了。我们这里稍微涉及一下具体的应用。由于AOP目前还是一种代码生成技术,不符合我的审美观。而且,AOP的入门例子到处都是,也不值得占用本书的篇幅。我们这里说明一下常见的AOP声明语法。
首先,我先给出,我理想中的AOP声明语法是什么样的,然后我们再来看现有AOP实现框架中的语法惯例。
我理想中的AOP声明语法是这样的。形式上类似于Haskell函数。
weave 拦截器列表 回填位置列表
用英文表示就是
weave InterceptorList LocationList
具体例子就是:
weave [LogInterceptor, TransationInterceptor] [ com.*, net.db.* ]
这段声明的含义就是,把[LogInterceptor, TransationInterceptor]这个列表里面的所有拦截器,都织入到[ com.*, net.db.* ]这些位置的所有方法里面。
这样就够了吗?还不够。我们还需要一个过滤器,叫做Filter或者Exclude,用来过滤掉或者排除掉一些不需要拦截的特殊方法。我们可以直接在LocationList里面支持这种过滤和排除功能。比如:
weave [LogInterceptor, TransationInterceptor] [ com.*, net.db.*, - net.db.test.* ]
net.db.test.*前面加了一个负号(-)。这就表示,把net.db.test.*里面的所有方法都排除出去。
我们看到,这种声明形式很像是合法的程序调用代码。事实上,AOP确实会提供类似于这样的程序编程接口。只不过,他们的用词惯例不同。他们把LocationList叫做PointCut(切点,切面),把Interceptor叫做Advice。换成他们的词,AOP声明形式就是这样:
weave AdviceList PointCut
这就是一个weave函数,接受两个参数,AdviceList和PointCut。
在解释语言中,我们可以直接用这种函数调用代码的形式来声明AOP。因为解释语言的程序本身就可以当做配置文件来用,随时修改,随时运行,不需要重新编译。
但是,编译语言就不行了。为了保持AOP定义的灵活,我们必须把AOP声明从代码中抽出来,放在一个配置文件中,通常是XML文件中。
我们可以看到解释语言(通常是动态类型语言)的优越性。这也是我为什么倾向于动态类型语言的又一个原因。
另外,关于用XML表达程序结构,我是不太赞成的。在我看来,XML就是用来存放树形数据的。用XML来表达程序结构,简直就是滥用。但是,由于XML很容易解析,因此,这种滥用现象越来越严重。毕竟,解析一门解释语言,需要一个颇为复杂的词法、语法分析器,而XML需要的解析器则简单许多。
由于个人的好恶,我这里坚决不给出XML声明AOP的例子。反正只不过是把好端端的代码转换成蹩脚的XML格式而已。
另外,需要说明的是,在AOP的惯例声明语法中,Advice并不是直接对应Interceptor,而是对应Interceptor里面的before和after两个方法。我们可以把上面的MethondInterceptor进一步细化,变成:
BeforeAdvice{
before(method)
}

AfterAdvice{
after(method)
}


MethodInterceptor{
BeforeAdvice beforeAdvice
AfterAdvice afterAdvice

around( method ){
beforeAdvice.before(method)

result = method.invoke(…); // 调用真正的对象方法

afterAdvice.after(method, result)
}
}
按理来说,AOP的声明也就这些,到这里就完了。但是,还没有完。有些AOP玩出了花活儿,发明出了新式的AOP声明方式——运用一些语言中提供的标注(Annotation)特性进行AOP声明。
这是怎么做的呢?这种声明方式要求程序员用某种Annotation标注每一个需要织入AOP重复代码的方法。然后,AOP实现框架就在代码中去寻找这些Annotaion,进行相应的织入重复代码的处理。
这种写法用散布在各处的Annotation替换了集中在一处的AOP声明。你可以说这是一种创新,你也可以说,这是开历史的倒车。
俗话说,分久必合,合久必分。分有分的道理,合有合的道理,分分合合,就是这个道理。
AOP的作用本来是把散落在各个方法中的重复代码合在一处,进行集中处理。现在,又用Annotiaon把它分散开了。这个游戏倒是有趣。
当然了,喜欢这种声明方式的程序员可以举出若干好处。比如,Annotion比重复代码更加简洁方便,比集中式声明更加一目了然,云云。但这劝服不了我。我仍然固执地认为,这就是一种没事找事型的做法。天下本无事,庸人自扰之。还是那句老话,这是我的个人偏见,不必当真。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值