用AOP对设计模式进行改进.doc_第1页
用AOP对设计模式进行改进.doc_第2页
用AOP对设计模式进行改进.doc_第3页
用AOP对设计模式进行改进.doc_第4页
用AOP对设计模式进行改进.doc_第5页
已阅读5页,还剩135页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

用AspectJ增强设计模式上设计模式长期以来一直是一些经验丰富的开发人员的工具箱的重要组成部分。不幸的是,因为模式可以影响多个类,所以它们也是侵入性的、难于使用(和重用)。本文是 AOPWork 系列的第 3 部分,是一篇由两部分组成的文章,在这篇文章中,Nicholas Lesiecki 将介绍 AOP 是怎样通过根本转变模式实现来解决这一问题的。他研究了三个经典的 Gof 设计模式 (适配器模式、修饰器模式和观察者模式),同时还将讨论使用面向方面技术实现这些模式所带来的实践和设计方面的好处。 什么是设计模式?根据 Design Patterns: Elements of Reusable Object-Oriented Software: 设计模式系统地命名、促进和解释了解决面向对象系统中重复出现的设计问题的一个通用设计。它描述了问题、解决方案、何时应用该解决方案以及所产生的结果。它还提供了一些实现提示和示例。解决方案是解决问题的对象和类的总体安排。可以定制并实现解决方案,解决具体上下文环境中的问题。 在多年成功地应用模式解决 OO 系统中的问题之后,我发现自己也认同了这个定义。模式是与普通程序员谈论设计的最好方式,它们代表解决“重复出现的设计问题”的最佳实践。所以,当我参加了 Stuart Halloway 的一次访谈时,我感到有点震惊:他为 GoF 提供了另一个头衔:“处理 C+ 中破损事物的修理厂”。他的观点是:在一种语言中以“模式”方式存在的东西,在不同的范式下,可以融入语言本身。接着他给出了 Factories 的示例 该示例在 Java 语言中有用,但在 Objective-C 中却没多大用,后者支持从构造函数中返回子类型。 我思考了很长一段时间,然后我认识到两个方面实际上说的是同一件事之前:设计模式提供了表达那些无法直接在编程语言中表达的概念的词汇表。 那么,AOP 位居何处呢?对于 OOP,我们有 GoF 模式,它提供了处理公共的概念(像观察者和修饰器)的统一方法(尽管有时有点麻烦)。AOP 构建在 OOP 之上,提供了表达横切关注点的直接方式。它认为某些 GoF 模式是关于横切的,可以直接用 AOP 表示。所以您会注意到,对于一些包含许多类的模式,用一个方面就可以表达。有些模式变得更易使用,因为它们可以包含更少代码。有些模式得到了非常好的支持,以致于它们几乎消失不见。其他模式严格绑定到 OOP (例如处理类结构的模式),所以它们在与 AOP 结合使用的时候保持不动。 本文的目的是探索用 AOP (特别是用 AspectJ)进行的模式实现。我选择 GoF 模式,是因为它是一个非常流行和通用的工具。在本文的第 1 部分中,我要设置一些分析模式影响的指标,然后研究适配器和修饰器模式。适配器会演示静态交叉的优势,而修饰器则会暴露它自身是一个正在消失的模式。在 第 2 部分中,我将提供对观察者模式更加深入的研究,这种模式并没有消失,但您会看到在用 AspectJ 实现它时的一些主要好处。第 2 部分将显示 AspectJ 如何使模式转变成可重用的基本方面,从而允许您下载预先构建好的模式库 这是让模式爱好者们兴奋的一大优势。 1为什么把 AOP 应用到设计模式? 我前面说过,许多模式都是横切的,当然我不是第一个想到这一点的人。最近有一篇研究论文对 GoF 模式进行了分析,分析发现:在 23 个模式中,有 17 个模式表现出某种程度的横切。(这篇论文是 Jan Hannemann 和 Gregor Kiczales 合著的“Java AspectJ 中的设计模式实现”,请参阅 参考资料 一节,以获取更多细节。) 如果 AOP 承诺可以协助解决横切,那么在设计模式上使用 AOP 有什么好处呢?我先从用通用术语回答这个问题开始,然后设置一个框架,通过它来考察每个设计模式。 在设计模式上使用 AOP 的好处 AOP 第一个关键的好处是把指定设计模式的代码 本地化 的能力。这意味着通常只在一个方面或一对密切关联的方面上就可以实现模式。(这与 Java 语言实现形成对比,在 Java 语言实现中,模式应用程序可以分布在多个类中。)能够在一个地方看到所有代码会带来几个实际的好处。首先,如果模式的所有交互都能在一个地方看到的话,那么阅读代码的人就能更容易地理解模式。其次,如果开发人员需要改变模式的实现,那么他或她就能在一个地方修改模式,而不用在整个系统中追踪模式的片断。最后,开发人员可以用有意义的名称描述封装模式的方面,为以后的维护人员提供有关模式意图的文字线索。例如,可以把方面命名为 SensorAdapter,这表明在传感器上使用的是适配器模式。 AspectJ 模式实现的另一个关键好处就是 遗忘性(obliviousness)。换句话说,在模式中发挥作用的类不必知道这个角色。这个好处直接来自本地化 因为模式是在某一个方面实现本地化的,所以不需要冒犯其参与者。遗忘性让模式参与者更容易理解代码。不仅如此,遗忘性还让模式更容易组合。在 Java 语言实现中,如果类参与到多个模式中,模式的机制会迅速模糊它的核心含义。如果类不知道那些在模式中的参与者,那么可以在其他上下文中重用这些类。在本文的 第 2 部分 中将看到这方面的一个具体示例。 这些好处允许对某些模式实现代码级重用。设计模式的概念和结构一直都是可重用的。如果想实现观察者模式,任何人都可以找出 GoF 的书籍,把模式应用到自己的代码中。但是,如果使用面向方面技术,那么通过下载 ObserverProtocol 方面 (可以在设计模式计划中获得它,请参阅参考资料 一节),就可以避免这个麻烦。除了节省的实现工作之外,代码级重用还允许模式代码和文档进行更紧密的耦合。例如,我可以浏览 ObserverProtocol 的 javadoc,不用另找一本书就可以理解它的意图和结构。 分析框架 每个模式的描述都跟着一个公共结构。我将从一个示例问题开始介绍,提供对这个模式的通用描述。然后我会描述如何实现这个模式,先使用 Java 语言实现它,然后使用 AspectJ 语言实现它。在每个实现后面,我都会描述是什么造成了模式的横切,以及这个版本的模式在理解、维护、重用和编排代码的时候有什么效果。1适配器模式 我要详细考虑的第一个模式就是适配器模式。适配器模式完全是关于兼容性的。这个模式允许类与其他原来由于接口不兼容而无法交互的类进行交互。要在 Java 代码中实现适配器,需要用特殊的适配器类包装目标类,适配器类能把目标类的 API 转换成客户想要的 API,或者转换成能够更容易利用的 API。 设置:提供一个聚合的传感器读取器 假设现在要设计一个航天器,则需要提供一个读取器,读取航天器上所有关键传感器。由于是在扩展现有系统,所以对于每个要访问的传感器而言,都存在方便的 Java 类。例如,可以用以下类访问温度计: public class TemperatureGaugepublic int readTemperature()/accesses sensor internals 可以用以下类访问辐射传感器:public class RadiationDetector public double getCurrentRadiationLevel()/read radiation somehow 这些传感器类中有一些是其他团队成员编写的,有一些是第三方供应商编写的。现在想要做的是提供一个显示方式,显示每个传感器的状态,这样司令官只要看一眼,就知道飞船是否有问题。以下是所需要功能的一个示例。(实际的显示可能包含闪烁的红灯和高音喇叭,但现在我们只显示文本。) Readout: Sensor 1 status is OK Sensor 2 status is OK Sensor 3 status is BORDERLINE Sensor 4 status is DANGER! 可以用以下方法实现上述显示: public static void main(String args)RadiationDetector radiationDetector = /find critical detector TemperatureGauge gauge = /get exhaust nozzle gauge List allSensors = new ArrayList();allSensors.add(radiationDetector);allSensors.add(gauge);int count = 1;for (Sensor sensor : allSensors) /How to read each type of sensor.? 目前为止,一切顺利。但是怎样才能不使用丑陋的 if(sensor instanceof XXX) 检测就能读出每个传感器呢?选项将修改每个传感器类,让它们拥有 getStatus() 方法,以便解释传感器的读取操作,并返回 String,如下所示。 if(this.readTemperature() 160)return DANGER;return OK 这样做会将一些不相关的状态显示代码带到多个类当中,增加它们的复杂性。而且,可能存在一些实际限制 (例如必须重新编译第三方类)。这就是适配器模式发挥作用的地方。 1Java 语言的适配器 适配器模式的传统实现方式是:用一个实现了方便的 API 的类来包装每个目标。在这种情况下,要创建一个公共接口,比如 StatusSensor,如下所示: public interface StatusSensorString getStatus(); 有这个公共接口存在,就可以像以下这样实现读取器方法: for (StatusSensor sensor : allSensors) System.out.print(Sensor + count+);System.out.println( status is + sensor.getStatus(); 剩下的惟一挑战就是让每个传感器符合这个接口。适配器类可以实现这一点。在清单 1 中可以看到,每个适配器都以成员变量的形式保存自己包装的传感器,用这个底层的传感器实现 getStatus 方法: 清单 1. 适配器类和客户代码 /Adapter classes public class RadiationAdapter implements StatusSensor private final RadiationDetector underlying;public RadiationAdapter(RadiationDetector radiationDetector) this.underlying = radiationDetector;public String getStatus() if(underlying.getCurrentRadiationLevel() 1.5)return DANGER;return OK;public class TemperatureAdapter implements StatusSensor /.similar /glue code to wrap each sensor with its adapter. allSensors.add(new RadiationAdapter(radiationDetector);allSensors.add(new TemperatureAdapter(gauge); 清单还显示了在读取器之前用适当的适配器包装每个传感器的“胶水代码”。模式并没有指定这个胶水代码应当在哪个具体位置出现。可能的位置包括“创建之后”和“使用之前”。可以将示例代码放在向读取器集合添加传感器之前。1对 Java 语言适配器的分析 适配器模式在传统的实现中运用得很成功。但是什么让它产生横切呢?是“状态”关注点横切了多个不同的传感器类。如果想协同定位一个包中的适配器类,那么这个模式的 OO 实现会很好地得到模块化。包会成为“适配器模块”。包装术语将阻止传感器了解模式,从而形成更加松散的耦合。不幸的是,实现包装任务的那部分应用程序既需要知道适配器,还需要知道应用适配器的传感器。因此,模式会造成“胶水代码”的位置成为横切。 现在,让我们来看看 Java 语言适配器是如何在我的评价指标上堆叠起来的: 易于理解:在包中协同定位的统一命名的 SensorAdapter,使得这个模式的意图清晰。不幸的是,胶水代码的位置可能远在适配器之外。由于胶水代码的区域不是结构化的,所以在试图理解模式的时候会忽略它,或者在试图理解它处理的代码时忽略它。 您还必须关注对象标识的处理问题。也就是说,如果同一对象包装和未包装的版本同处在一个系统中,那么必须想好是否应当对它们同等对待。 重用:要重用这个模式,就必须从头开始重新实现该模式。 维护:在向读取器添加新传感器时,必须添加新的适配器类,并更新包装它们的胶水代码。 组合:假设想要在另一个模式中包含传感器。由于适配器已经忘记传感器,所以它们不受影响。但这是把双刃剑。新的模式应当把适配版本的传感器当作自己的对象,还是应该将未适配版本的传感器当作自己的对象呢? AspectJ 适配器 像使用其他设计模式一样,适配器的 AspectJ 实现保留了它的同类的意图和概念。这个实现采用类型间声明,这是一个重要的横切类型支持,要比切入点和通知(advice)的启动时间更少。如果需要对静态横切进行回顾,请参阅 参考资料 一节,以获得适当的指示。 像使用纯 OOP 版本一样,AOP 版本的适配器需要 StatusSensor 接口。但是,AspectJ 版本没有采用独立的包装器类,而是采用 声明父母 的形式,让不同的传感器直接实现 StatusSensor,如下如示: public aspect SensorAdapter declare parents :(TemperatureGauge | RadiationDetector)implements StatusSensor; 现在传感器应当符合接口。但是它们还没有 实现 接口 (这是 AspectJ 编译器会很高兴指出的一个事实)。要完成模式的实现,必须向方面添加要使每个传感器符合要求的类型间方法声明。下面的代码把 getStatus() 方法添加到 TemperatureGauge 类中: public String TemperatureGauge.getStatus()if(this.readTemperature() 160)return DANGER;return OK; AspectJ 版本的读取器类看起来与用 Java 语言实现的版本相同,除了不必用胶水代码包装传感器。每个传感器同时又是自己的包装器。 AspectJ 适配器的分析 用 AspectJ 实现适配器的关键好处是本地化。整个模式都包含在一个方面中,而不是两个以上的独立适配器和非结构化的“包装”位置。这里是根据我的指标考察的 AspectJ 实现的情况: 易于理解:没有包装,理解模式只需查看适配器方面即可,这消除了四处查看的需要。没有包装还消除了处理对象标识问题的需要。 重用:AspectJ 版本与其他版本具有同样的可重用性。 维护:因为每个新传感器只包括编写一个方法(而不是完整的类),所以扩展 AspectJ 实现应当略微容易些。随着适配器的数量增长或者每个适配要求的逻辑变复杂,可能会发现单一的方面会变得不合理地长。在这种情况下,可以把一个方面拆分成几个方面。拆分方面会损失本地化的好处,但是可以保留其他好处。 组合:可以很容易地组合多个模式,因为不需要处理包装协调的问题。 结果是:Java 和 AspectJ 实现在处理传感器类的方式上都做得不错。但是,只有 AspectJ 版本处理了应用程序的其他方面。这是一个主要优势吗?这可能取决于应用程序是否会表现出分析中描述的复杂属性。如果我正对某一个项目使用 AspectJ,那么我肯定会用它实现适配器,虽然我介绍 AspectJ 不仅仅是为了解决这个问题。下一个模式,即修饰器模式,提供了一些更引人注目的优势。修饰器模式 从面向方面的角度来考虑,修饰器是一个有趣的模式,因为它是接近“消失”的模式之一,具有面向方面的语言(例如 AspectJ)所引入的能力。为什么这么说呢?如果深入研究修饰器在面向方面和面同对象实现中的演变,就会让事情变得更清晰。 修饰器模式的目标是动态地把功能添加到现有对象上。GoF 书中给出的规范示例包括文字修饰。在他们的示例中,GUI 组件类是在一个修饰器类中包装的,可以添加边框或者是滚动条来显示该组件。Java 类库明显非常支持修饰器,允许用java.util.Collections 的方法修饰 Collection,这样,集合就变成不可修改的或者是同步的,还有各种各样的 IO 流,它们可以缓冲、扩大或者监视其他流。 设置:监视文件读取 为了给这个示例增加一些趣味,我选择了 Java 发行包中的修饰器,用它来查看用 AspectJ 复制一个修饰器时需要做些什么。其中一个我觉得有趣的修饰器是来自 javax.swing 的 ProgressMonitorInputStream。根据记录,ProgressMonitorInputStream 监视底层输入流的读取进度。 为了演示修饰的实际效果,我编写了一个简单的读取文件的 GUI。可以在清单 2 中查看读取输入流的代码,还可以在图 1 看到运行的应用程序的截屏。(也可以单击这一页顶部或底部的 代码 图标,研究示例的完整源代码。) 图 1. 流的 ProgressMonitor 在考虑下面一节的时候,您可能想拥有 java.io 和 ProgressMonitorInputStream javadoc 或者源代码,请参阅 参考资料,以获得更多参考消息。1Java 语言的修饰器 在 Java 语言中,最初是通过创建 AbstractComponent 接口 (或类)认识修饰器模式的,基本实现 (有时称为 ConcreteComponent) 和修饰器都要符合这一点。在这个示例中,AbstractComponent 是 java.io.InputStream,它定义了 FileInputStream (ConcreteComponent) 和 BufferedInputStream (ConcreteDecorator) 的接口。 虽然并不是严格要求的,但修饰器实现通常提供一个 AbstractDecorator,它维护一个对已修饰组件的引用,并在不添加额外行为的情况下提供基本的修饰机制。在 java.io 中,FilterInputStream 提供了这项功能。最后,ConcreteDecorator 扩展了 AbstractDecorator,覆盖了需要修饰的方法,并在调用已修饰组件上的相同方法之前或之后添加行为。在这种情况下,ProgressMonitorInputStream 使用了 ConcreteDecorator。 看一看 Sun 的 ProgressMonitorInputStream 实现(由于许可考虑,我在这里不再重新打印),可以看到它在创建 javax.swing.ProgressMonitor 时实例化了该监视器。在每个 read 方法后面,它用从底层流中读取的字节数更新监视器。ProgressMonitor 类决定了什么时候弹出监视对话框并更新可视显示。 要使用 ProgressMonitorInputStream,只需要包装另外一个输入流(如清单 2 所示),并确保在进行读取操作时引用已包装的实例即可。在这里,请注意适配器和修饰器模式之间的相似性:两者都需要以编程方式对目标类应用额外的行为。 清单 2. 监视 InputStream private void actuallyReadFile() try InputStream in = createInputStream();byte b =new byte1000;while (in.read(b) != -1) /do whatever here bytesRead+=1000;bytesReadLabel.setText(Read + (bytesRead/1000) + k);bytesRead = 0;in.close(); catch (Exception e) /handle. private InputStream createInputStream() throws FileNotFoundExceptionInputStream stream = new FileInputStream(name.getText();stream = new BufferedInputStream(stream);/_this_ is a JPanel GUI component stream = new ProgressMonitorInputStream(this, This is gonna take a while, stream);return stream; Java 语言修饰器的分析 查看以上示例之后,看起来没有东西比使用修饰器模式更简单了。但是,不要忘记,为了使这个示例能够运行,Sun 实现了InputStream、FilterInputStream 和 ProgressMonitorInputStream 代码的数量可不是微不足道的。 在这个示例中,监视的关注点横切了 InputStream。更加通用的情况是,修饰可以横切修饰的目标。更进一步地说,修饰关注点可以横切应用程序。例如,用户可能要求一个针对 全部 文件输入的 ProgressMonitor。(为了避免您把它当作一个人为的示例,请自己问一下自己,有多少次您使用输入流的时候,没有 对其进行缓冲。) 现在看一下剩下的指标: 易于理解:一旦了解了修饰器的工作,就很容易理解它。但是我永远不会忘记我在第一次打开 java.io 并试图了解组成应用于流上的机器类(machinery class)的健康情况时所产生的迷惑。虽然一份快速培训教程可以很容易地让我摆正方向,但这只适用于查看代码,没有理解模式的简易途径。更具体地度量理解方面的负担的方法是计算代码行数。在研究完 AspectJ 实现之后,我将看一下行数。不过,还是一点价值都没有,由于修饰器使用了包装功能,所以它也会遭遇那些影响适配器的对象标识问题。 重用:要重用这个模式实现,必须重新实现它。 维护:有两个关键的维护场景:在第一个场景中,要向现有实现添加新的修饰器。根据修饰器中方法的数量,这项工作可能很冗长,但显然不是很难。在第二个场景中,要向 AbstractComponent 添加新操作(即 InputStream)。这意味着要更新所有现有修饰器,以反映新的操作,还要针对每个修饰器作出决策,决定是不是应当把修饰应用到新方法上。 组合:因为修饰器和组件共享公共接口,所以修饰器允许在指定实例上透明地组合修饰器。(参见清单 2,在那里,代码缓冲也将监视输入流)。这非常好,特别是因为修饰的目标不必知道自己受到了修饰。1AspectJ 修饰器 在 Hanneman 和 Kiczales 合著的论文中,他们谈到了以下这点: 如果使用 AspectJ,那么某些模式的实现就会完全消失,因为 AspectJ 语言的构造直接实现了它们。这适用于 修饰器。 查看一下 Gof 书中关于使用修饰器的动机的那一节,然后您就会很清楚为什么会是这种情况了: 修饰器把请求转发到组件,在转发前后可能执行附加动作(例如绘制边框)。透明性支持递归地嵌套修饰器,所以它支持无限数量的附加功能。 那么所给的建议是什么呢?是透明地把附加“操作”添加到任何操作上的能力?从某种意义上说,AspectJ 支持对任何方法进行修饰。为了看到它在真实系统中的作用,可以研究 AspectJ 实现的输入流读取监视。 识别被修饰的操作 为了正确地实现监视,方面必须识别出流上的读取操作。通过编写一个捡取读取取操作的切入点可以做到这一点: pointcut arrayRead() :call(public int InputStream+.read(.); 现在可以应用一些下面这样通用格式的通知: after() returning (int bytesRead) :arrayRead()updateMonitor(bytesRead); 此通知用 returning 格式公开方法调用的返回值。读取的字节数被传递给方面上的一个私有方法:updateMonitor()。该方法负责更新实际的 ProgressMonitor 的细节(稍后会有更多关于这项工作的内容)。 公开相关上下文 到目前为止,解决方案都很简单。但是,这表明 ProgressMonitor 类还要求做几件事才能实现它的工作。具体地说,它需要一个 GUI 组件来绑定监视对话模式。在传统的实现中可以看到这个要求: /this is a JPanel GUI component stream = new ProgressMonitorInputStream(this, This is gonna take a while, stream); 要获得需要的 GUI 组件,方面必须把它绑定到切入点,通知才能使用它。清单 3 包含修订后的切入点和通知。注意,fromAComponent() 切入点利用了原始的 cflow() 切入点。切入点实际上是在说“选择作为 JComponent 上面方法执行结果的全部连接点,并公开这个组件,供通知使用。” 清单 3. 用 cflow 将上下文环境交给监视器 pointcut arrayRead(JComponent component, InputStream is) :call(public int InputStream+.read(.) & target(is)& fromAComponent(component);pointcut fromAComponent(JComponent component) :cflow(execution(* javax.swing.JComponent+.*(.)& this(component);after(JComponent component, InputStream is) returning (int bytesRead) :arrayRead(component, is)updateMonitor(component, is, bytesRead); 维护状态 为了让方面的应用面更广(也为了准确地模拟其他实现),方面必须维护状态。也就是说,它应当为每个受监视的流弹出一个惟一的进度监视器。AspectJ 提供了几个处理这个问题的选择。对于这个方面来说,最佳选择可能是用 Map 维护每个对象的状态存储。这种技术在我实现的观察者模式中会再次出现,请注意观察!(将特定状态保存到对象的其他方法中,包括类型间声明和 pertarget/perthis 方面,但是对这些概念的考虑超出了本文的范畴。) 要实现状态存储,首先要声明一个 WeakHashMap,它用流作为键,把监视器保存为值。可以使用 WeakHashMap,因为如果正常使用中不再需要键,那么 WeakHashMaps 不会阻止将它的键作为垃圾进行收集。这个最佳实践可以防止由于方面持有对不活动对象的引用而造成的内存泄漏。 然后 updateMonitor() 方法用 map 消极地初始化一个新的 IncrementMonitor。一旦该方法确定存在监视器,就会用最新的进度(read() 的返回值表示)来更新监视器。在清单 4 中,可以看到实现消极初始化和进度更新的代码,以及 IncrementMonitor 的完整代码: 清单 4. 每个流监视器的消极初始化 /From the aspect. private void updateMonitor(JComponent component, InputStream is,int amount)IncrementMonitor monitor =(IncrementMonitor)perStreamMonitor.get(is);if(monitor = null)monitor = initMonitor(is, component);monitor.increment(amount);private IncrementMonitor initMonitor(InputStream is,JComponent component)try int size = is.available();IncrementMonitor monitor =new IncrementMonitor(component, size);perStreamMonitor.put(is, monitor);return monitor; catch (Exception e) /.handle /.end aspect public class IncrementMonitor extends ProgressMonitorprivate int counter;public IncrementMonitor(Component component, int size)super(component, Some Title, null, 0, size);public void increment(int amount)counter += amount;setProgress(counter); 最后,在已经完全读取完流的时候,方面需要释放监视器。如果在这个时候按照方面的思路去思考,您就会认识到这是个机会:InputStream 可以很方便地为将要通知的方面定义一个 close() 方法,如下所示: before(InputStream is):call(public void InputStream+.close()& target(is)System.out.println(Discarding monitor.);perStreamMonitor.remove(is); 目前,练习已经完成。但是,如果熟悉 InputStream 实现的话,那么可能会发现,我故意遗漏了一些事。必须用与其他读取方法不同的方法来处理 read() 方法 (没有参数),因为它的返回值不是读取的字节数,而是读取流中的下一个字节。随着本文的示例代码对方面进行扩展,可以处理这个限制,但是我建议您在参阅代码之前,想想自己编写方面时应当如何解决这个问题。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 部分中,还将探索面向方面的重用 把模式或协议定义为抽象方面并用特定于应用程序的方面来应用它的能力。 1用AspectJ增强设计模式下在这篇文章的第1 部分中,我从面向方面的角度研究了两个广泛应用的面向对象设计模式。在演示了适配器和修饰器模式在 Java 系统和 AspectJ 系统中的实现方式之后,我从代码理解、重用、维护性和易于组合几方面考虑了每种实现的效果。在两种情况下,我发现横切 Java 实现模块性的模式在 AspectJ 实现中可以组合到单独的一个方面中。理论上,这种相关代码的协同定位可以使模式变得更易理解、更改和应用。用这种方式看模式,就转变对 AOP 的一个常见批评 阻止开发人员通过阅读代码了解代码的行为。在这篇文章的第 2 部分中,我将通过深入研究观察者(Observer)模式,完成我对 Java 语言的模式实现和 AspectJ 模式实现的比较。 我选择把重点放在观察者(Observer)模式上,因为它是OO设计模式的皇后。该模式被人们广泛应用(特别是在 GUI 应用程序中),并构成了 MVC 架构的关键部分。它处理复杂的问题,而在解决这类问题方面表现得相对较好。但是,从实现需要的努力和代码理解的角度来说,它还是带来了一些难以解决的难题。与修饰器或适配器模式不同(它们的参与者主要是为模式新创建的类),观察者(Observer)模式要求您先侵入系统中现有的类,然后才能支持该模式 至少在 Java 语言中是这样。 方面可以降低像观察者(Observer)模式这种侵入性模式的负担,使得模式参与者更灵活,因为不需要包含模式代码。而且,模式本身可以变成抽象的基本方面,允许开发人员通过导入和应用它来实现重用,不必每次都要重新考虑模式。为了查看这些可能性如何发挥作用,我将继续本文第一部分设置的格式。我将从示例问题开始,提供对观察者(Observer)模式的通用描述。然后我将描述如何用 AspectJ 和 Java 语言实现观察者(Observer)模式。在每个实现之后,我将讨论是什么造成模式的横切,模式的这个版本在理解、维护、重用和组合代码方面有什么效果。 在继续后面的讨论之前,请单击本页顶部或底部的 代码 图标,下载本文的源代码。 观察者(Observer)模式 根据 Portland Pattern Repository Wiki(请参阅 参考资料 一节,获得有关的细节),观察者(Observer)模式的用途是定义对象之间的一对多依赖关系,因此,当一个对象的状态发生改变时,其所有依赖项都会得到通知,并自动更新。这使得观察者适用于所有类型的通知需要。请考虑以下情况: 关于本系列 AOPWork 系列是为具有一定面向方面的编程背景、并准备扩展或者加深其知识的开发人员准备的。与大多数developerWorks 文章一样,本系列具有很高实用性:从每一篇文章中学到的知识立刻就能使用得上。 所挑选的为这个系列撰稿的每一位作者,都在面向方面编程方面处于领导地位或者拥有这方面的专业知识。许多作者都是本系列中讨论的项目或者工具的开发人员。每篇文章都经过仔细审查,以确保所表达的观点的公平和准确。 关于文章的意见和问题,请直接与文章的作者联系。如果对整个系列有意见,可以与本系列的组织者 Nicholas Lesiecki 联系。更多关于 AOP 的背景知识,请参阅 参考资料。 条形图可以观察它显示的数据对象,以便在这些对象变化时对它们进行重新绘制。 Acc

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论