The HelloWorld of AOP

终于,AOP系列的第一弹终于出来了,感觉这个东西主要是证明了“Hello world”情结对软件从业人员的影响之巨大。。。

文中使用了这篇文章中的示例和部分分析,特此致谢。


SD Magazine 2002年5月号的“The 15 % Solution”这篇文章用了一个针筒来展现 aspectj 把 aspect “注射”到“正常”的 java object 中的能力,——然后你就得到了一个“感染”了 aspect 的 java object。在下面的介绍之后我们会看到,AOP其实和Refectoring技术有着很多相似之处,事实上,对于 AOP 着重关注的crosscutting concerns(横切关系),专门有一组 Refactoring 技术(萃取,extract)是处理这类问题的好手,何况 Martin Fowler 的书上就提到,extract 是最常见的重整技巧,特别是重整过长,责任不清的方法,这类的萃取最常用到。那么我们真的需要 AOP 吗?(顺便说一句,有人就把 AOP 成为“动态重整 Refactoring”。)

现在,我准备用下面的这个 HelloWorld 为例来看看其中的差异。——对这个简单例子来看似乎你会说差异不大,但是对于成百上千的对象来说,这可就会是完全不同的实现方案了。

STEP 1 HelloWorld

首先我们实现一个简单包含 main 方法的执行类 MatrixHello:

// MatrixHello.java
public class MatrixHello {
    public static void main(String[] args) {
        HelloWorld.say("Welcome to the REAL world!");
        HelloWorld.sayToPerson("Welcome to the REAL world!", "Neo");
    }
}

然后我们来实现上面用到的那个关键的 HelloWorld 类:

// HelloWorld.java
public class HelloWorld {
    public static void say(String message) {
        System.out.println("Hi!");
        System.out.println(message);
        System.out.println("Goodbye!");
    }
 
    public static void sayToPerson(String message, String name) {
        System.out.println("Hi!");
        System.out.println(message + ", " + name);
        System.out.println("Goodbye!");
    }
}

显然我们的 Hello world 比常见的那个有礼貌多了(这个当然,出自书香门第嘛:) ),不过我们也为这些礼貌付出了一定的代价—— Martin Fowler 看到这些代码想必会皱起鼻子说“… Smell bad …” —— 所以我们要来对它动一些手术。。。

STEP 2 HelloWorld + Refectoring ( Extract Class )

反复出现的代码十分适于使用 Refactoring 重整,这里选择的方法是萃取一整个 Manners 类出来,利用类的特色与下面的 aspect 的做法作一比较将是很有意思的。预计的 refactoring 后的草图如下:

注意这个萃取行为的箭头方向,代表 HelloWorld 必须要“知晓” Manners 类的存在,但是 Manners却不需要,因为通常萃取出一个 class 不会是只给原来被萃取的类使用,别的类一般也会需要重整来使用这个萃取出的 class,希望萃取出来的 class 可以在系统中扮演特定的责任(responsibility)。

这个 Extract Class 的 Refectoring 很容易实现,首先实现萃取类 Manners :

// Manners.java
public class Manners {
    public static void say(String message) {
        System.out.println("Hi!");
        System.out.println(message);
        System.out.println("Goodbye!");
    }
}

原来的 HelloWorld 则改为下面的实现方式,直接调用 Manners.say() 方法将这个前后问候的工作转包给萃取出的类处理。这其中已经牵扯到 Manners ,代表 HelloWorld 这个类会要求 Manners 类担负起它在系统中该负的责任。(“En … smell better now … ” —— M.F. )

// HelloWorld.java
public class HelloWorld {
    public static void say(String message) {
        Manners.say(message);
    }
 
    public static void sayToPerson(String message, String name) {
        Manners.say(name + ", " + message);
    }
}

照理说这样分工可以显而易见的让架构更简洁清楚,不易使这个“礼貌”的责任被各个类实现的七零八落的,A 说早安,B 说你好等等,因为经过这次的 Extract Class 整理,并顺利通过 unit test 的那一刻起,整个系统已经有专门的对象负责这件事,其他对象不用担心 —— 就将这件事交给那个萃取出来的对象吧!——一切既然如此美好,何来 aspect-oriented 的需求? 看来,问题出在“交给”这个动词上面。

STEP 3 HelloWorld + AspectJ

现在我们来试试改用 aspectj 的写法。主要的差异表现在两个方面:一个是 aspect 的语法完全不同,一个是对象不同。

按照 AOP 的思想,对象结构(职责)方面需要将被萃取类中上下交错的藤蔓拿掉,还原 HelloWorld 的责任原貌。换句话说,说 Hello 的就专心说 Hello ,前后的繁文缛节就不劳它操心了,甚至都不用知道最后到底有没有礼貌。。。这里的 HelloWorld 没有交付责任给给什么 MannersAspect 之类的东西,所以,AOP 的关键点之一就是:还原职责,HelloWorld 对象不再担心/负责“将 xx 责任交给 xx 对象”这件事。下图并非标准 UML ,因为 UML 到目前为止还没有适当的 aspect 图规范,所以下面先用class 图代替,但是请务必将 MannersAspect 视为一个 aspect ,而不是 class。:)

除了萃取出的东西变为 aspect 之外,与上面的 Extract Class Refactoring 的差异还有箭头方向不同。我们使用 UML 的association 关系来画出这种关联,换成 aspectj 的说法,这像是“注射”,或是“感染”(Biohazard??)。

利用 aspect-oriented ,变成完全以 HelloWorld 为中心,这个 HelloWorld 不需去关注或是 import 或是担心用错萃取类别的东西,因为萃取出来 aspect 根本不需要让 HelloWorld 知道,也就是说 HelloWorld 从来不知道自己有这个调用责任,这点就是与上述的 extract class 最大的差异。

// HelloWorld.java
public class HelloWorld {
    public static void say(String message) {
        System.out.println(message);
    }
 
    public static void sayToPerson(String message, String name) {
        System.out.println(name + ", " + message);
    }
}

(可以看到,这就是一个最一般的 HelloWorld,职责清晰简单)

然后将前后问候分给另一个 java aspect 去处理:

// MannersAspect.java
public aspect MannersAspect {
    pointcut callSayMessage() : call(public static void HelloWorld.say*(..));
 
    before() : callSayMessage() {
        System.out.println("Hi!");
    }
 
    after() : callSayMessage() {
        System.out.println("Goodbye!");
    }
}

显然这个东西不是一般的 java source,也是 javac 们处理不了的,所以要用 ajc 这个 aspectj 的(预)编译器来处理,如果你使用 AspectJ For JBuilder 插件,这是无比容易的事 —— 然后包含 aspectjrt.jar 这个运行库(很小很小)就可以运行了。

AspectJ 基本概念

AspectJ 替 Java 语言加入一些新概念。aspect 的语法有几个特点:

* joinpoints: 程序执行过程的一个时间点,举例来说,joinpoint 可以定在某个 class 中调用特定 method 的地方,就像 debug 中的断点一样。
* pointcuts: 一种程序,其目的是定义 joinpoints,也可以收集这些时间点上前后的上下文信息。
* advices: 遇到条件符合就会执行的代码。举例来说,可以要求在执行某个时间点的前面先来个 log 动作,遇到该时间点就是代表这个 advice 的条件符合,整个 log 的 advice 代码就会被启动。
aspect 在 aspectj 中看起来和 class 很像,只是不由 field/method 组成,而是由 pointcuts/advices 组成一个 crosscutting unit —— pointcuts 会指出执行的时间点,然后由 advices 指出应该执行的动作。
下面的图利用 aspectj 的 browser 显示出的pointcut 和 advice 的关系:

总结

所以 —— 当你发现重复的 cross-cutting code 出现时,你可以使用 AOP 的萃取方式取出一个 aspect,这点和你发现重复的 code 出现在 method 时候,采用 典型的 OOP 式的方法来萃取出一个 common method 供其他代码调用的情况基本是一样的。其主要差异在于萃取物与被萃取物之间的关系不大相同,OOP 的萃取物 class 不需知道被萃取物 class 是谁,反而是被萃取物需要知道如何调用萃取物来填补本身没有具备的 crosscutting code,反之 AOP 的被萃取物 class 根本不认得萃取物 aspect,被萃取物 class 中间的 crosscutting code 被抽出成为 aspect 之后,本身就不需理那些 aspect,也管不到它们了,class 就可以完全专注在本身的责任。明白了区别,那么到底那种更加合理呢?

我们知道,责任分配是面向对象设计的核心,那么 HelloWorld 对象是否应该“负责”去调用这些 crosscutting concern 呢?这里大师们有个比较清楚的结论,经过 extract aspect 之后,整个对象系统的责任总量会下降,而采用 Refactoring 的 extract class 并不会使责任总量下降,而是重分配而已,这是 extract aspect 和 extract class 应用最大的差异。所以,对于那些责任不能减少的(必然不是不是 crosscutting),你应该选 extract class,反之就应该卸下不该由每个对象去负责的 crosscutting code,利用 aspect 整理出更加专注与责任明确的对象(反正需要做的crosscutting 职责 aspect 会神不知鬼不觉的注射回去的 :) )。

对照 The 15 % Solution (这个标题。。。谁说IT人不擅长炒作?!)这篇 Gregor Kiczales 的访谈录可以发现,他提到的观点是:系统 85 % 由对象控制,因为对象负责着核心责任,剩下的 15 % 纠缠不清的 crosscutting code,使用 aspect 会是更好的解决方案。原文如下:

One important thing about programming in AspectJ or any of the other aspect
languages is that you’re still programming 85 percent objects. We’re solving
the problems that objects didn’t solve. When we went from procedures to
objects, we didn’t throw out procedures.

这句话很有历史的感觉,也很适合作为本文的结束。

3 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *