用AspectJ增强设计模式上
AspectJ 修饰器的分析
像适配器模式一样,修饰器模式的两个实现的区别在于本地化。在 AspectJ 版本中,整个模式巧妙地处于一个方面中。(我排除了 IncrementMonitor 助手类,因为它在模式中没有起到结构化的作用。)在 Java 语言版本中,基本模式实现 (不考虑客户代码)要求具有三个类。这有什么效果呢?
易于理解:因为 AspectJ 的切入点语言的威力,方面可以使用同一个通知影响多个操作。对比之下,修饰器类必须在每一个操作上重复该行为。Sun 实现的代码行数是方面实现的两倍多,其中部分是由于上述原因。ProgressMonitorInputStream 大约是 110 行,而 FilterInputStream 是 40 行(我放过了 InputStream,因为它可能是修饰器模式中的合法父类)。对比之下,MonitorFileReads 方面用了 53 行,而 IncrementMonitor 助手类用了 12 行。行的比率是 160 比 65,或者大约是 2.4 : 1。虽然 LOC 只是一个粗略的测量方法,但是一般来说,代码越短就会越清晰。
而且,如果您熟悉 AOP,那么 AspectJ 解决方案不会给您留下正在运行什么特殊事情的感觉。Java 语言解决方案要求几个类小心地进行协作,而 AspectJ 版本看起来就像是正在进行大多数方面所做的工作:即通过通知将行为添加到一组连接点上。
最后,值得记住的是 AOP 的经常遭批评的一个缺点:再也不能通过阅读源代码了解模块将做什么工作了。如果把修饰器应用到对象上,并且没有方面的帮助,那么在客户代码(不是包装的位置) 或者在对象显示附加行为的修改目标中(FileInputStream),都没有基于源代码的线索。对比之下,如果在 AJDT 中检查清单 2 的 GUI,那么可以看到在行 while (in.read(b) != -1) 上的友好注释,这些注释指明监视器方面将影响读取调用。可以将 AspectJ 与它的开发环境结合,在这种情况,要比原先的实现提供更多的信息。
重用:由于修饰构建到了语言中,几乎所有方面都“重用”这个模式。更具体的重用可能是:让监视方面变得更抽象,允许子方面指定监视操作的切入点。通过这种方式,差不多所有对象都能用监视进行修饰 —— 不再需要进行传统实现要求的那些准备工作。(如果想了解抽象方面,本文的 第 2 部分 将更详细地解释它们的用途。)
维护:向对象添加新修饰不需要特殊的努力。如果修饰目标变了(想像一下新的读方法),那么(可能)必须对切入点进行更新,以体现这种变化。必须更新切入点这一点有些累人,但是通过编写能够捕捉新操作的强壮切入点,可以减轻这个负担。(请参阅 参考资料,获得关于强壮切入点的一个 blog 的链接。)在任何情况下,更新切入点看起来都要比更新所有修饰器(就像在 Java 语言实现中进行类似更改所要求的那样)的麻烦少一些。
这里是另一个有趣的场景(在前面 Java 语言分析中提到过):监视 所有的 文件读取。使用 OO 修饰器,这就意味着读取流的每个类都必须记得把自己包装在 ProgressMonitorInputStream 中。对比之下,MonitorFileReads 方法会监视任何输入流上的读取,只要它们是在 JComponent 的控制流程中发生的。由于 ProgressMonitor 只在操作进行的时间比当前阈值大的时候才弹出,所以这个方面可以透明地保证用户在必须等候文件读取时永远不会被打扰 —— 程序员无需对此警惕。
组合:像竞争性实现一样,AspectJ 版本支持用很少的工作就可以透明地组合多个修饰器。
正如我前面提到过的,修饰器的主要秘密(透明地把行为添加到操作)包含在 AspectJ 语言中。AspectJ 实现的惟一挑战是如何把方面的状态(更新的进度监视器)与特定实例关联 —— 示例使用 map 实现这个关联。处理关联关系的需要使得修饰器作为模式保留在 AspectJ 中。有时,当修饰的机制已经存在的时候,使用传统的修饰器看起来更容易一些 —— 特别是模式没有入侵已修饰的类时。但是,如果修饰机制不存在,那么 AspectJ 实现的灵活性和简单性就使其成为更好的选择。
结束语
我希望对这两个熟悉的模式的介绍有助于表现面向方面机制的实际应用。随着开发社区与不断涌现的泛滥的范式斗争,把新技术应用到老问题上(已经存在良好解决方案的问题)可能会有用 —— 这样的练习有助于用熟悉的方式评估新技术。
那么,迄今为止这方面进行得如何呢?虽然不是一个金锤(golden hammer),但 AspectJ 已经成功地保护了一些用来实现传统 OO 模式的固有优势。这些优势来自于 AspectJ 能够更好地处理横切关注点的能力。通过把模式的代码搜集到单一方面中,AspectJ 使得通过阅读代码来理解模式变得更容易。因为模式代码没有在非模式类(例如适配器和修饰器要求的包装位置)中显示,所以其他这些类也很容易理解。这种组合还使得扩展和维护系统变得更加容易,甚至使到处重用模式也变得更加容易。
适配器和修饰器模式代表中等复杂的模式。在系列文章的 第 2 部分 中,我将研究是否可以将面向方面扩展到更复杂的模式。具体地说,第 2 部分将研究观察者模式,其中包括多个角色和动态关系。在第 2 部分中,还将探索面向方面的重用 —— 把模式或协议定义为抽象方面并用特定于应用程序的方面来应用它的能力。