前几天的News Link提到了AspectJ的Adrian Colyer对AOP的一些解释,今天抽了点时间翻译了出来,感觉对AOP的本质是比较有见地,也是说的比较清晰的。
p.s. 当我完成了这个翻译的时候,Adrian又写了一篇相关的文章”Making Programs Simpler“,发表在他的blog上,可以作为后续的读物。(有时间再翻?嘿嘿!)
Ted Neward的挑战:没有废话的AOP
by Adrian Colyer
翻译:Neo
今年TSS Java座谈会的AOP专题结束之后,Ted Neward向部分与会者提出了一个挑战,请大家给AOP一个解释,前提是不能求助于那些特定的名词(例如:scattering, tangling, crosscutting, modularity, encapsulation, abstraction, dominant decomposition, concern等等)。我最近几天仔细的考虑了这个问题,接下来我想告诉大家我的观点——这是对AOP核心意义的一个解释,当然,这还只是一个早期的、不是很成熟的想法。按照Ted的要求,上面说的那些“buzzwords”是不会在下面的段落中出现的 :)
当你写一段程序,如果你能够把每一个想法、概念、需求都能够在源代码中有一个直接而又清晰的反映,这毫无疑问是一件好事。例如,假设我写一个银行的应用,关于银行计帐之类的概念会在程序中很直接的反映为“一个”类,比如说BankAccount。如果我在写一个医疗服务的应用,那么关于病人的概念可能被直接的反映为一个Patient类。“一个”设计上的概念被映射到“一个”实现结构上。
现在考虑一个简单的MVC应用。我们有个很简单、清晰、单一的设计需求,就是说:当model类的某个状态发生变化时,所有注册了与之相关联的view类要被(以某种方式)通知到这一变化。显然,在这里,我们要想把这个单一的设计需求映射到一个单一的实现结构,将是十分困难的。一般来说,在一个典型的实现中,你可能会在model类管理状态改变的方法中看到散布各处的一大堆类似notifyObservers()的调用。显然,这个实现偏离了我们1-1映射的目标,最后带来的是“概念:实现=1:n”的结果。
在设计中捕获的概念和需求,在这个软件进化的过程中,往往也是发生变化的最小单元。一个设计到实现的1-1的映射是可以很容易的添加、删除、维护的,而一个1-n的映射相对来说就难得多,因为一个概念发生变化,你必须去更新很多地方——而且你必须保证一个不漏的改到,而且必须所有的修改必须是严格一致的。
这种1-n的一个极端的例子——经典的或者说老掉牙的例子——就是tracing。这个设计需求说的是,当打开tracing开关的时候,所有方法的进入和退出都要记录在某个tracing缓存中(例如:日志文件)。在我研究过的一个系统中,这种“概念:实现”的比例大约是1:100,000。想想创建这些tracing代码和维护它们要付出多少!面对这样的境况,很多开发者会从投入产出的角度出发,作出一个选择,从而使得这种比例变成1:0——他们根本不去实现任何tracing代码!(译者按:在中国大量的开发者不做tracing不是因为这种投入产出考虑,而是根本不知道应该做-_-bbb )
同样在这个系统中,还有一个相关的例子,就是对记录失败日志,这个部分的比例大约为1:8,000。当然,这是个很极端的例子,你一般不会碰到很多这么恐怖比例的实例,一般来说比较常见的比例是1:20或者1:30,上面说的那个MVC的例子很容易达到这个范围。如果有兴趣你可以研究一个开源的例子,例如Eclipe JDT编译器,看看处理编译进度通知(progress notification during compilation)部分的代码。
现在看看另一个问题,一个更加精细的需求。有一个很常见的设计就是某个类要实现某个接口,我们仍然希望实现上面提到的那种1-1的映射,但是在大多数情况下,这种实现不够清晰和直接:这个类中的状态和方法,到底哪些是属于对那个接口的实现呢?当然,很多程序员解决这个问题的办法,就是在书写程序时把来自一个接口的内容都放在一起,不同接口的内容变成一组一组的排列,嗯,这个办法大部分时候还行,但是还不能算是个真正的解决方案吧!
现在我们来看看这个硬币的另一面。我们都希望一个设计概念清晰的对应一个实现结构,也希望每个实现结构清晰的对应到一个设计概念。但是事实上我们也看到了,有些设计概念最后不得不被分散在实现的各个部分,这也就是说,实现的某些部分在处理多个设计概念。这就是跟上面说得1:n问题相反的,同样令人困扰的n:1的问题。可以再考虑一下上面说过的那个MVC的例子,在那个例子中,model类不仅要处理相关的数据/状态的问题(这是它的本质),还要处理把状态改变通知给view类的问题。
这种n-1的问题的出现跟1-n问题一样多,也一样令人不快。也就是说,当一个程序员维护一个类的时候,他必须完全清楚这个类处理的每一个设计需求,而且要把它们均衡得很好。在某些应用领域,这种情况被证明是极其痛苦的过程。一个很典型的例子就是企业应用的开发中,往往要求一个拥有领域专家知识的开发者——他的本职本是处理领域相关的模型和业务逻辑的实现类——同时要考虑和平衡诸如事务处理、持久化、安全等等的设计需求。一个类如果包含和依赖于多个设计概念,这个类将是很难重用,也很难测试的。考虑上面那个MVC例子中的model类,你可能想使用它表示的那个业务模型,但是你不得不同时接受它处理通知(notification)的策略。(译者按:这一段说明了cross-cutting concern的危害绝对不仅仅是代码维护量问题,还涉及到面向对象的核心价值之一:重用。)
于是,我们在软件应用系统——除了很小,几乎没什么意义的系统——中没有得到我们期望的概念和实现的1:1的理想,而是面对一个n:m的尴尬局面。所以不用说,这些系统都很难维护、很难理解,就一个字——繁!而且,这不是你的错,这完全是因为面向对象语言的开发工具(译者注:这里工具是泛指的,不专门指IDE这类工具)没办法做的到:既不能保证每个设计概念能够被单一的实现结构来实现,相应的,也不能保证每个实现结构能被映射到单一的设计概念。(译者按:于是,终于。。。)这就是AOP希望解决的问题:带给我们尽可能接近1:1的映射。AOP解决这个问题的办法是引入一种新的结构——所谓的aspect——使得你可以把原来OOP没法在一个地方实现的那些设计概念保持在一个地方去实现,例如MVC中的视图通知(view-notification)机制。
当你开始以这种方式考虑AOP,我希望你会看到,AOP决不仅仅是现存软件系统的一些附属物/添加剂,而是软件程序的设计和实现框架中一个有机组成部分。这也可以解释为什么AspectJ直接扩展一种编程语言——Java——来整合AOP的概念。记住,我们的目标是实现1:1的关系,只要我们还没有达到这种1:1的平衡,无论哪边多一点都会带来麻烦,而AOP就是为了尽可能的接近这个1:1平衡。