首先请大家不要被我的标题唬住,关于AOP我是知之甚少的,只是对我所知道的这一些东西有些想法而已,缭表于此,博各位一笑。
AOP,面向方面编程,关注各种程序构造中的横切面。AOP主要的实现手段是代码注入,即由编译器负责在编译过程中、生成代码之前将一个切面代码注入到用户代码中的指定位置,这些位置通常是用户代码的最前面和最后面。前些天听熊节讲述J2EE,在提到AOP时介绍了一个典型的例子——事务型的业务逻辑。在编写一个事务型的业务逻辑时,通常的做法是首先OpenTransact,然后编写业务逻辑,最后Commit;如果期间出现错误还要Rollback。很明显这样产生了非常冗余的代码,作为一个开发者,可能只注重其中的业务逻辑代码,因为事务的处理代码在各个逻辑模块中都是相同的。因此人们想到,通过特殊的标记,指定编译器来自动生成这些事务处理代码,而一个逻辑模块中仅出现业务逻辑代码。这样,AOP诞生了。
以上的讲解是粗浅的,甚至不十分准确。但我所想到的仅仅是这个所谓的“方面”和代码的“注入”,事实上我最想说的就是,原来AOP的思想是由来已久的!
我没有经历过打孔纸带的年月,也没有用汇编语言写过什么东西,不过还是有听过一些关于大型机的故事,并且也看过一些汇编代码的。纸带我们就不提了,首先来看看汇编语言。
最古老的汇编语言可以被称为“指令影射语言”,这个名字是我自己起的,为什么呢?因为最早的时候一条汇编语言仅对应一条机器指令,它仅仅是机器语言的“助记符”(注意这里的“仅仅”一词);这也就意味着一个程序最终有多少条机器指令,在编写的时候我们就要写多少条,并且很多跳转地址是需要我们手工计算的。
随后很多不甘于现状的聪明的程序员们发明了一种称之为“宏汇编”的语言,可以大大降低代码的长度,并且提高了可读性。“宏”的意义就是一条汇编语句可以生成多条机器指令。这是如何做到的呢?原来这群人仔细观察了现有的代码,发现其中存在着“模式”!比如跳转指令的用法,大部分(甚至所有)情况下的用法是通过对条件进行判断,有选择地执行两个语句块中的一个(注意不是多个语句块中的一个);于是,他们通过一种特殊的语法来形成类似于后来的高级语言中if语句的语言形态,并且由编译器在预处理过程中计算具体的跳转地址并“注入”跳转语句。
宏汇编还有一个特性,就是关于子过程的调用。最早的时候,子过程的编写非常混乱。由于子过程往往需要参数,于是有用寄存器传递参数的、有用RAM单元传递的。慢慢人们就发现用RAM,并且是用堆栈的方式来传递参数非常方便,可以将寄存器解放出来完成更高效率的操作——于是乎,人们广泛地使用堆栈进行参数传递;再后来慢慢地人们又发现,每次调用子过程的时候,我们做的事情都是push各个参数,然后call子过程。啊哈,这时又轮到聪明的程序员们登场了,他们通过一些手段实现了类似MASM中的invoke语句,通过给定子过程的原型,使用单条语句即可完成子过程的调用。同样是由编译器将push参数和call子过程的代码“注入”到用户的宏汇编代码中。
当然,要想令汇编语言更清晰,其实还有很多代码是可以由编译器自动生成的;然而,如果构建更复杂的宏汇编语法来实现这些功能,会使得代码反而不易阅读;因此,高级语言出现了。
这里所谓的高级语言当然远没有今天的语言那么高级,无非就是Fortran、Ada、Cobol等,也可以说是过程语言。面向过程的语言实际上是在大量地完成着很多“模式”的汇编代码的生成,当然不乏一些更为高级的特性,如多道条件语句、存储器(变量)的控制等。
可以认为过程语言是更好的宏汇编语言罢,当然其自然的数学表达式(如a+b这样的)则是汇编语言望尘莫及的。于是乎大家都转向了高级语言的学习和使用。而几乎所有的高级语言都提供了“结构(类似C语言的struct)”这种语言元素,因此人们慢慢又发现,他们写的程序依然有“模式”:几乎所有的程序都是“结构+结构上的操作“;几乎每次操作一个新的结构都是:首先为结构分配地址并得到其指针,调用用于初始化该结构的函数,使用其他函数对其操作,不再使用该结构的时候调用清理函数来释放其内存和由其分配的其他资源(整个过程如下图所示)。并且,初始化和清除对于每个同类的结构来说都是相同的,而操作往往是不同的,但是我们关心的又是如何操作一个结构,往往忽视其初始化和清除。
于是乎,又有聪明人在想办法了:可不可以由编译器来生成这些初始化代码呢?其实,读这篇文章的你更聪明,这不是AOP中经典的横切镜头么,而实现的方法却正是现在广为流行的面向对象方法么!是这样,于是出现了面向对象的程序设计语言,其典型特征就是引入了class这种语言元素和object的概念,而“对象”首先处理的就是初始化和清除问题,请看下图:
想必大家已经再熟悉不过了,用构造函数和析构函数来进行对象的初始化和清除操作;最矛盾的一点就是,构造函数和析构函数只需编写却无需调用。相信学过面向对象程序设计的朋友早已经知道,编译器会将构造函数的代码“注入”可执行代码(对于静态分配的对象)或将其调用代码注入到分配存储空间的时候;析构函数也一样,编译器将其代码或对其进行的调用“注入”到对象结束其生存期的时刻。
接下来的事情我就不细说了。Java出现了,AOP成了和OOP平等的名词……
文章结束得很潦草。因为我已经不知道我要传达什么思想了……总之本文就AOP中的关注点和代码注入在其他语言中找到了相似体,感到任何新技术都不是突然冒出来的,而是需要长期的积累和观察。