分享

AOP@Work: 用 AspectJ 增强设计模式, 第 2 部分

 CevenCheng 2010-10-14

支持复杂模式的透明组合和代码级重用

级别: 中级

Nicholas Lesiecki (ndlesiecki@yahoo.com), 软件工程师/编程教师, Google

2005 年 7 月 17 日

Nicholas Lesiecki 用这篇深入研究观察者(Observer)模式的文章,继续他对使用面向方面技术实现设计模式的好处的讨论。他演示了 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)模式的用途是

定义对象之间的一对多依赖关系,因此,当一个对象的状态发生改变时,其所有依赖项都会得到通知,并自动更新。

这使得观察者适用于所有类型的通知需要。请考虑以下情况:

关于本系列

AOP@Work 系列是为具有一定面向方面的编程背景、并准备扩展或者加深其知识的开发人员准备的。与大多数 developerWorks 文章一样,本系列具有很高实用性:从每一篇文章中学到的知识立刻就能使用得上。

所挑选的为这个系列撰稿的每一位作者,都在面向方面编程方面处于领导地位或者拥有这方面的专业知识。许多作者都是本系列中讨论的项目或者工具的开发人员。每篇文章都经过仔细审查,以确保所表达的观点的公平和准确。

关于文章的意见和问题,请直接与文章的作者联系。如果对整个系列有意见,可以与本系列的组织者 Nicholas Lesiecki 联系。更多关于 AOP 的背景知识,请参阅 参考资料

  • 条形图可以观察它显示的数据对象,以便在这些对象变化时对它们进行重新绘制。

  • AccountManager 对象能够观察 Account,这样,在帐户状态改变时,它们可以向销售人员发送一封电子邮件。

  • 支付服务能够观察在线音乐商店中的歌曲播放事件,以便向客户收费。

我将把重点放在最后一个场景上,将它作为这篇文章的示例。(这种针对问题域的想法是从 “在 .NET 中实现观察者”中借鉴的;有关的细节,请参阅 参考资料 一节。)假设您要向在线音乐商店添加一些特性。商店已经拥有逐个播放 Songs 的能力,还有把它们组合成在线 Playlists 的能力。客户还可以查看指定歌曲的歌词。现在需要添加收费和统计功能。首先,系统应当跟踪播放和歌词显示事件,以便对客户进行适当的收费。第二,系统该当更新播放最频繁的歌曲列表,用来在“最流行”部分显示。清单 1 包含系统中已经存在的核心对象的代码:


清单 1. 在线音乐服务的核心领域对象
//common interface for items that
                        //can be played
                        public interface Playable {
                        String getName();
                        void play();
                        }
                        public class Song implements Playable{
                        private String name;
                        public Song(String name) {
                        this.name = name;
                        }
                        public String getName() {
                        return name;
                        }
                        public void play() {
                        System.out.println("Playing song " + getName());
                        }
                        public void showLyrics(){
                        System.out.println("Displaying lyrics for " + getName());
                        }
                        }
                        public class Playlist implements Playable {
                        private String name;
                        private List songs = new ArrayList();
                        public Playlist(String name) {
                        this.name = name;
                        }
                        public void setSongs(List songs) {
                        this.songs = songs;
                        }
                        public void play() {
                        System.out.println("playing album " + getName());
                        for (Song song : songs) {
                        song.play();
                        }
                        }
                        public String getName() {
                        return name;
                        }
                        }
                        public class BillingService{
                        public void generateChargeFor(Playable playable) {
                        System.out.println("generating charge for : " + playable.getName());
                        }
                        }
                        public class SongPlayCounter {
                        public void incrementPlays(Song s){
                        System.out.println("Incrementing plays for " + s.getName());
                        }
                        }
                        

现在已经看到了核心系统,让我们来考虑一下 AOP 之前的观察者实现。






Java 语言的观察者

虽然实现的差异很明显,但在它们之间还是有一些相似之处。不论如何实现观察者,代码中都必须回答以下 4 个问题:

  1. 哪个对象是主体,哪个对象是观察者?
  2. 什么时候主体应当向它的观察者发送通知?
  3. 当接收到通知时,观察者应该做什么?
  4. 观察关系应当在什么时候开始,什么时候终止?

我将用这些问题作为框架,带您经历观察者(Observer)模式的 OO 实现。

角色定义

首先从标记器接口来分配角色开始。Observer 接口只定义了一个方法:update(),它对应着 Subject 发送通知时执行的操作。 Subject 承担着更多的职责。它的标记器接口定义了两个方法,一个用来跟踪观察者,另一个用来通知事件的那些观察者。

public interface Subject {
                        public void addObserver(Observer o);
                        public void removeObserver(Observer o);
                        public void notifyObservers();
                        }
                        

一旦定义了这些角色,就可以把它们应用到系统中对应的角色上。

应用观察者角色

可以修改 BillingService,用以下少量代码实现观察者接口:

public class BillingService implements Observer {
                        //...
                        public void update(Subject subject) {
                        generateChargeFor((Playable) subject);
                        }
                        }
                        

跟踪和通知观察者

一旦这项工作完成,就可以转移到两个 Subject。在这里,要对 Song 进行修改:

                        private Set observers = new HashSet();
                        public void addObserver(Observer o) {
                        observers.add(o);
                        }
                        public void removeObserver(Observer o) {
                        observers.remove(o);
                        }
                        public void notifyObservers() {
                        for (Observer o : observers) {
                        o.update(this);
                        }
                        }
                        

现在面临的是一个不太让人高兴的任务:必须把 Subject 的这个实现剪切、粘贴到 Playlist 中。将 Subject 实现的一部分摘录到一个助手类中,这有助于缓解设计的弊端,但是仍然不足以完全消除它。

有没有更好的 OO 观察者?

这篇文章侧重在 Java 语言的 OO 模式实现上。读者可能想知道是否有特性更丰富的 OO 语言能够更好地解决观察者角色带来的这些令人头痛的问题。给这篇文章带来灵感的 .NET 实现采用委托和事件,极大地减轻了 .NET 版本的 SongPlaylist 类的负担。(要比较两个实现,请参阅 参考资料 一节。)类似的,支持多继承的 OO 语言也能把 Song 的负担限制到在一个 Subject 支持类中的混合上。每种技术都有很大帮助,但是都不能处理触发通知或组合多个模式实例的问题。我将在本文的后面部分讨论这些问题的后果。

触发事件

现在已经把类调整到它们在模式中的角色上了。但是,还需要回过头来,在对应的事件发生时触发通知。Song 要求两个附加通知,而 Playlist 则需要一个:

 //...in Song
                        public void play() {
                        System.out.println("Playing song " + getName());
                        notifyObservers();
                        }
                        public void showLyrics(){
                        System.out.println("Displaying lyrics for " + getName());
                        notifyObservers();
                        }
                        //...in Playlist
                        public void play() {
                        System.out.println("playing album " + getName());
                        for (Song song : songs)  {
                        //...
                        }
                        notifyObservers();
                        }
                        

需要牢记的是,虽然示例系统只需要一个状态改变通知,但是实际的系统可能许多许多通知。例如,我曾经开发过一个应用程序,该应用程序要求在 更改购物车状态时发送观察者风格的通知。为了做到这一点,我在购物车和相关对象中的 10 个以上的位置应用了通知逻辑。

随着各个角色准备好参与到模式中,剩下来要做的就是把它们连接起来。

启动观察关系

为了让 BillingService 开始观察 SongPlaylist,需要添加胶水代码,由它调用 song.addObserver(billingService)。这个要求与 第 1 部分 中描述的适配器和修饰器的胶水代码的要求类似。除了影响参与者之外,模式还要求对系统中不确定的部分进行修改,以便激活模式。清单 2 包含的代码模拟了客户与系统的交互,并整合了这个胶水代码,胶水代码是突出显示的。清单 2 还显示了示例系统的输出。


清单 2. 连接主体和观察者(并试验系统)的客户代码
public class ObserverClient {
                        public static void main(String[] args) {
                        BillingService basicBilling = new BillingService();
                        BillingService premiumBilling = new BillingService();
                        Song song = new Song("Kris Kringle Was A Cat Thief");
                        song.addObserver(basicBilling);
                        Song song2 = new Song("Rock n Roll McDonald's");
                        song2.addObserver(basicBilling);
                        //this song is billed by two services,
                        //perhaps the label demands an premium for online access?
                        song2.addObserver(premiumBilling);
                        Playlist favorites = new Playlist("Wesley Willis - Greatest Hits");
                        favorites.addObserver(basicBilling);
                        List songs = new ArrayList();
                        songs.add(song);
                        songs.add(song2);
                        favorites.setSongs(songs);
                        favorites.play();
                        song.showLyrics();
                        }
                        }
                        //Output:
                        playing playlist Favorites
                        Playing song Kris Kringle Was A Cat Thief
                        generating charge for : Kris Kringle Was A Cat Thief
                        Playing song Rock n Roll McDonald's
                        generating charge for : Rock n Roll McDonald's
                        generating charge for : Rock n Roll McDonald's
                        generating charge for : Favorites
                        Displaying lyrics for Kris Kringle Was A Cat Thief
                        generating charge for : Kris Kringle Was A Cat Thief
                        

Java 语言观察者的分析

从实现的步骤中,您可能感觉到,观察者(Observer)模式是一个重量级的模式。这里的分析将探索该模式对系统当前和潜在的冲击:

  • 横切:在音乐商店应用程序中,记费的观察关注点(observation concern)既包含 记帐人员SongPlaylist),又包含 开票人员BillingService)。Java 语言实现还附加了胶水代码的位置。因为它影响三个不同的类,这些类的目标不仅仅是这个观察关系,所以记费观察就代表了一个横切关注点。

  • 易于理解:先从领域对象的视角开始查看系统。观察者的 OO 实现要求对受模式影响的领域类进行修改。正如在实现类中看到的,这些修改的工作量并不小。对于 Subject 来说,需要给每个类添加多个方法,才能让它在模式中发挥作用。如果领域概念(播放列表、歌曲等)的简单抽象被观察者(Observer)模式的机制阻塞,又会怎么样呢?(请参阅“有没有更好的 OO 观察者?”,了解为什么有些非 Java 语言可能让这项工作更容易。)

    也可以从模式的视角研究系统。在 Java 语言实现中,模式冒着“消失在代码中”的风险。开发人员必须检查模式的三个方面( ObserverSubject 和连接两者的客户代码),才能有意识地构建出模式的坚固模型。如果发现模式的一部分,聪明的开发人员会寻找其他部分,但是没有可以把这些部分集中在一起是“BillingObserver”模块。

  • 重用:要重用这个模式,必须从头开始重新实现它。通常,可以利用预先构建好的支持类(例如,java.util 包含 ObserverObservable )填充模式的某些部分,但是大部分工作仍然需要自己来做。

  • 维护:为了帮助您思考观察者(Observer)模式的维护成本,要先考虑避免双重计费的需求。如果看一眼模式第一个实现的输出(请参阅 清单 2),就会看到应用程序对播放列表上下文中播放歌曲的计费。这并不令人感到惊讶,因为这就是编写这段代码要做的工作。但是,市场部门想为播放列表提供一个总体报价,以鼓励批量购买。换句话说,他们想对播放列表计费,而不是对播放列表中的歌曲计费。

    很容易就可以想像得到,这会给传统的观察者实现带来麻烦。需要修改每个 play() 方法,让它们接受一个参数,表明它是由另一个 play() 方法调用的。或者,可以维护一个 ThreadLocal,用它来跟踪这方面的信息。不管属于哪种情况,都只在调用上下文环境得到保证的情况下才调用 notifyObservers()。这些修改可能会给已经饱受围困的模式参与者再添加了一些负担和复杂性。因为变化将影响多个文件,所以在重新设计的过程中就可能出现 bug。

  • 组合:需要考虑的另一个场景就是不同观察者的观察。从设置中您可能会想起,音乐服务应当跟踪最流行的歌曲。但是统计搜集只被应用于歌曲播放,而不涉及歌词查看或播放列表播放。这意味着 SongCountObserver 观察的操作集合与记费观察者观察的略有不同。

    要实现这点,就不得不修改 Song,让它维护一个独立的 Observer 列表,只对 play() 通知感兴趣(对歌词操作没有兴趣)。然后,play() 方法会独立于记费事件触发这个事件。这样 OO 模式就保证了具体的 Subject 对具体的 Observer 的直接依赖。但是这种情况看起来是不可能的,因为 Song 必须为每种类型的 Observer 触发不同的事件。面对着另一种 Observer 类型,Java 语言的实现就会崩溃。

正如您可能想到的,AspectJ 观察者为这个分析所提出的场景提供了圆满的解决方案。在研究它们之前,我将介绍 AspectJ 如何处理基本模式。







AspectJ 观察者

就像使用我在这篇文章中考虑的其他模式一样,观察者(Observer)模式的目的,甚至是观察者(Observer)模式的基本结构,在用 AspectJ 实现时保持不变。但是,有一个关键的区别。通过使用面向方面的继承,在对模式进行定制,从而满足个人需求时,可以重用模式中对于所有实现都公用的那些部分。请记住,观察者(Observer)模式实现必须回答以下 4 个问题:

  1. 哪个对象是主体,哪个对象是观察者?
  2. 什么时候主体应当向它的观察者发送通知?
  3. 当接收到通知时,观察者应该做什么?
  4. 观察关系应当在什么时候开始,什么时候终止?

抽象的模式方面

首先我要回顾抽象方面的工作方式。与抽象类非常相似,在使用抽象方面之前,必须扩展它。抽象方面可以定义抽象的切入点(切入点有名称,但是没有主体),然后定义这些切入点上的具体的通知(advice)。这意味着由父方面指定行为,子方面控制在哪里应用行为。另外,抽象方面还能定义子方面必须覆盖的抽象方法,就像抽象类能够做到的那样。记住这些事实,现在来看一下来自 设计模式项目ObserverProtocol 方面。

角色定义

就像使用其他实现一样,角色由标记器接口指定。但是,在 AspectJ 实现的情况下,接口是空的(可能是被 AspectJ 5 中的注释代替)。接口以 ObserverProtocol 成员的形式出现:

/**
                        * This interface is used by extending aspects to say what types
                        * can be Subjects.
                        */
                        public interface Subject{}
                        /**
                        * This interface is used by extending aspects to say what types
                        * can be Observers.
                        */
                        public interface Observer{}
                        

AOP 设计模式项目

我对设计模式和 AspectJ 的狂热开始于不列颠哥伦比亚大学赞助的面向方面设计模式实现项目(请参阅 参考资料)。正如我在 这篇文章的第一部分 提到的,Jan Hanneman 和 Gregor Kiczales 调查了 23 个 GoF 模式,并用 Java 语言和 AspectJ 为每一种模式创建了参考实现。结果非常迷人,值得研究,在原始的模式中,有 17 个模式在使用 AOP 实现时得到了改进。最令人兴奋的是模式库。项目的作者能够从 13 个模式中提取出可重用的抽象方面,并在 Mozilla 公共许可下免费提供它们。本文介绍的 ObserverProtocol 方面就取自该项目。请参阅 参考资料 一节,以下载这些模式。

跟踪观察者

方面没有强迫参与者跟踪观察者,而是把这个职责集中起来。清单 3 包含的代码实现了模式的这一部分。从 第 1 部分 的修饰器 AOP 实现中,您应当了解了这个通用术语。同样,方面使用消极初始化的 Map 对特定于对象的状态进行跟踪。(同样,这个模式也有包含初始化模型的替代品,但是它们超出了本文的范畴。)请注意清单 3 中的一个有趣的元素 —— addObserverremoveObserver 方法是公共的。这意味着系统中任何地方的代码都可以用编程的方式决定是否参与到这个模式中。


清单 3. ObserverProtocol 管理观察主角的观察者
/**
                        * Stores the mapping between Subjects and
                        * Observers. For each Subject, a LinkedList
                        * is of its Observers is stored.
                        */
                        private WeakHashMap perSubjectObservers;
                        /**
                        * Returns a Collection of the Observers of
                        * a particular subject. Used internally.
                        */
                        protected List getObservers(Subject subject) {
                        if (perSubjectObservers == null) {
                        perSubjectObservers = new WeakHashMap();
                        }
                        List observers = (List)perSubjectObservers.get(subject);
                        if ( observers == null ) {
                        observers = new LinkedList();
                        perSubjectObservers.put(subject, observers);
                        }
                        return observers;
                        }
                        /**
                        * Adds an Observer to a Subject.
                        */
                        public void addObserver(Subject subject, Observer observer) {
                        getObservers(subject).add(observer);
                        }
                        /**
                        * Removes an observer from a Subject.
                        */
                        public void removeObserver(Subject subject, Observer observer) {
                        getObservers(subject).remove(observer);
                        }
                        //aspect continues...
                        

通知观察者

要实现对观察者的实际更新,ObserverProtocol 需要使用一个循环,这非常类似于它在 Java 语言实现中的使用。但是,它激活通知的方式非常不同。首先,方面定义了一个抽象切入点:

protected abstract pointcut subjectChange(Subject s);
                        

这个切入点由子方面负责具体化。ObserverProtocol 接着定义了一条通知,该通知对应着 subjectChange() 选中的连接点:

after(Subject subject) returning : subjectChange(subject) {
                        for (Observer observer : getObservers(subject)) {
                        updateObserver(subject, observer);
                        }
                        }
                        protected abstract void updateObserver(Subject subject, Observer observer);
                        

对于发生变化的主角的每个 Observer,方面都需要调用 updateObserver(),这是它定义的一个抽象方法。通过这种方式,具体的方面可以定义它接收到更新时的意义。

具体的子方面

目前,您已经看到 ObserverProtocol 提供的所有内容。要应用这个模式,则需要扩展方面,并对这篇讨论开始提出的 4 个核心的观察者(Observer)模式问题提供具体的答案。下载了 ObserverProtocol 之后,就可以开始创建自己的方面,并声明它扩展了 ObserverProtocol。正如您可能想到的,AspectJ 帮助器会提醒您没有完成方面的定义。必须具体化 subjectChange 切入点,并实现 updateObserver 方法。这些提醒可以充当将模式应用到系统中的指导。

角色分配

使用 AspectJ,不用直接修改 SongPlaylistBillingService 类,就可以把角色分配给模式的参与者,如下所示。

declare parents : Playable extends Subject;
                        declare parents : BillingService implements Observer;
                        

编译器不会强迫您插入这些声明,但是您会发现如果没有它们,就无法使用其余的方面机制,因为 ObserverProtocol 将根据这些标记器接口定义它的切入点和方法。

触发事件

这可能是这个方面最重要的部分:具体化 subjectChange 切入点,定义那些操作(构成值得通知的事件)。在这种情况下,切入点指定 play() 方法可以在任何实现 Playable 接口的类上执行(包括 SongPlaylist都可以这么做)。切入点也选择执行 Song 上的 showLyrics() 方法:

                        pointcut titleUse(Playable playable) :
                        this(playable)
                        && (
                        execution(public void Playable+.play()) ||
                        execution(public void Song.showLyrics())
                        );
                        public pointcut subjectChange(Subject subject) :
                        titleUse(Playable) && this(subject);
                        

响应更新

非常类似于 Java 语言版本,为了响应更新,需要实现更新方法。区别在于,在这里是向方面添加方法,而不是直接向 BillingService 添加,后者不知道自己参与到了模式中:

                        public void updateObserver(Subject s, Observer o){
                        BillingService service = (BillingService)o;
                        service.generateChargeFor((Playable)s);
                        }
                        

启动观察关系

正如我在前面所提到的,由于方面上存在公共的 addObserverremoveObserver,所以可以用编程方式对方面进行配置。要启动对歌曲和播放列表的观察,需要复制 Java 语言的客户实现,并用 ObserverBillingPolicy.aspectOf().addObserver(song, basicBilling) 替代出现 song.addObserver(basicBilling) 的地方。

但是,为了让模式的存在尽可能保持在不显眼的状态,所以,使用通知自动地开始这种关系是有意义的,如下所示:

                        //in the concrete aspect
                        //could be injected by a dependency injection framework like Spring
                        //see the Resources section for an excellent blog entry on this topic
                        private BillingService defaultBillingService =
                        new BillingService();
                        pointcut playableCreation(Subject s) :
                        execution(public Playable+.new(..))
                        && this(s);
                        after(Subject s) returning : playableCreation(s){
                        addObserver(s, defaultBillingService);
                        }
                        

通过使用方面中配置的默认 BillingService,该切入点和通知在创建 Playable 时立即开始计费观察。但是,ObserverProtocol 使得这方面具有灵活性。请进一步想像一下:通知会将辅助计费应用到特定歌曲上,或者检索与系统当前用法有关的计费计划。类似的,可以用通知在歌曲不符合未来计费要求时自动终止关系。







AspectJ 观察者的分析

我要做的某些设计观察听起来应当很熟悉,因为它们反映了对本文中讨论的前两个模式所做的观察(请参阅 参考资料)。但是,您会注意到,观察者中的设计对比特别突出:

  • 易于理解:从参与者的视角来看,AOP 版本的观察者更简单,因为它不需要 SongPlaylistBillingService 有什么特殊的东西。从参与者那里消除了这种依赖性,从主要抽象角度来说,让它们更容易理解,同时也使它们更加灵活、更具重用性。例如,它们可以重新用在其他不要求观察者(Observer)模式的系统中。它们也可以参与到其他模式中,而不用担心大量的模式代码会淹没类的核心职责。

    从模式的视角来看,AOP 版本还是更简单一些。AspectJ 实现不要求人们把模式片断汇集在头脑中,而是提供了一个 ObserverProtocol 方面,安排好了模式的结构。然后由具体的方面定义模式应用到这个系统的方式。模块都是很实际的,而且从模式视角来看,模块也很完整。所以,如果正在使用 AJDT,并且注意到(比如)某个观察者方面通知说使用 play() 方法,那么就可以沿着 AJDT 提供的线索理解总体情况。( 参考资料 一节包含一篇优秀文章,其中介绍了 AJDT 如何有助于导航和理解 AspectJ 代码。)

    通知 “事件”的生成在 AspectJ 实现中获得很好的模块化。在传统实现中,这些调用存在于三个不同的位置。不存在能说明为什么某个操作会触发事件,或者其他哪个操作也会这么做的直接线索。在 AOP 模式中,命名切入点沟通了操作的公共属性(标题的用法)。切入点规范指出了模式影响的其他连接点。

  • 重用ObserverProtocol 方面的重用潜力非常高。实际上,可重用的模式方面是我编写这篇文章的一半原因。作为开发人员,只要能依赖框架作者的经验,而不是折腾自己的实现,我就会很高兴。方面提供了重用横切代码的能力,这些代码以前纠缠在实现的细节中,根本不是独立存在的。在我分析的组合模式一节中,您会看到重用的一个很好的示例。

  • 维护:要查看方面让系统更容易发展的具体示例,请回到在系统的 Java 版本中将歌曲从双重计费中排除时遇到的困难上。要用 AspectJ 满足这个需求,只需要编辑 subjectChange() 切入点即可。以下代码可以确保 cflowbelow() 切入点会排除另一个 play() 的控制流程中发生的 play() 执行:

                                //the original titleUsePointcut
                                pointcut titleUse(Playable playable) :
                                this(playable)
                                && (
                                execution(public void Playable+.play()) ||
                                execution(public void Song.showLyrics())
                                );
                                //exclude title usage that occurs as a result
                                //of another title use
                                pointcut topLevelTitleUse(Playable playable) :
                                titleUse(playable) && ! cflowbelow(titleUse(Playable));
                                //define subjectChange in terms of the more restrictive
                                //pointcut
                                public pointcut subjectChange(Subject subject) :
                                topLevelTitleUse(Playable) && this(subject);
                                



    这个修改是微不足道的,特别是因为它只影响一个文件。而且,它还让新策略的意图变得很清楚。其他修改,例如添加新 Subjects 或主角变化操作,都只需像这样对方面进行简单的修改即可,不需要协调多个文件的修改。

  • 组合:可以回想一下介绍过的内容,在与应用到(某些)相同的参与者的第二个模式协作时,Java 语言实现的观察者也存在问题。要用 AspectJ 实现这个需求,只要再次用新的切入点和抽象方法定义扩展抽象方面即可。由于每个方面都管理自己的 Observer 列表和 Subject 列表,而且方面也定义了自己的 subjectChange 切入点,所以两个方面不会冲突。每个方面操作时实际上都独立于另一个方面。可以检查清单 4 中 SongCountObserver 的代码:


    清单 4.影响同一个参与者的第二个观察者方面
    public aspect SongCountObserver extends ObserverProtocol {
                                declare parents : Song extends Subject;
                                declare parents : SongPlayCounter implements Observer;
                                pointcut titleUse(Song song) :
                                this(song)
                                && execution(public void Song.play());
                                public pointcut subjectChange(Subject subject) :
                                titleUse(Song) && this(subject);
                                public void updateObserver(Subject s, Observer o) {
                                SongPlayCounter counter = (SongPlayCounter) o;
                                counter.incrementPlays((Song) s);
                                }
                                // could be injected by a dependency injection
                                // framework like Spring
                                private SongPlayCounter defaultCounter = new SongPlayCounter();
                                pointcut songCreation(Subject s) :
                                execution(public Song.new(..))
                                && this(s);
                                after(Subject s) returning : songCreation(s){
                                // different songs could be tracked in different statistical sets...
                                addObserver(s, defaultCounter);
                                }
                                }
                                



    因为在 AspectJ 系统中,多个模式(甚至同一模式的多个实例)可以透明地组合在一起,所以 AspectJ 系统避免了来自“模式密度”的一些问题。这就能够更多地使用设计模式所拥有的最佳实践,而不用担心实现的重量会压跨代码。(请继续等待 Wes Isberg 在 AOP@Work 系列中即将推出的文章,获取更多使用面向方面技术来避免 OO 设计模式中的模式密度问题的内容。)






结束语

从核心上看,设计模式实际是设计问题。因为程序员都很聪明,所以这些问题已经被解决了许多次。但也因为程序员的懒惰(用一种好的方式懒惰!),他们不愿意一次又一次地重复解决这些设计问题的工作。GoF 撰写的书籍(以及以前的模式工作)的贡献就是把为那些用今天的语言还不能更好表达的问题提供了通用的解决方案。这些麻烦的(如果是无价的)解决方案几乎可以解释为一种挑战,一种寻找更好(更易理解、更易重用、更易维护和更易组合)解决底层问题的方法的挑战。

有些 OO 模式很难使用,因为它们解决的问题是横切的。对于这些问题,AOP 提供的解决方案正中问题核心。通过将横切行为搜集到单独的一个模块中,使行为变得更容易理解、修改和重用。因为这种类型的面向方面模式中的参与者不依赖模式代码,所以它们也变得更灵活、更易重用。在本文中,我指出了实际的示例场景和修改,从中可以看出,在用面向方面的方式解决模式问题时,要容易得多。

新的解决方案采用了不同的形式。AOP 能在根本上把某些模式(例如修饰器)简化到很少观察到模式状态的程度。其他模式(例如观察者(Observer)模式)以可重用库模块的形式获得了新生命。总有一天您会发现,在扩展和实现模式方面,会自然得就像使用今天集合框架中的类一样。

有没有特定于 AOP 的模式?

值得进一步研究的一个迷人领域就是解决面向方面语言中重复出现问题的设计模式。从本文中您可以看到一点提示:两个方面采用消极初始化的 map,把状态松散地耦合到对象。除此之外,特定于 AOP 的模式(或候选模式)开始出现在用户社区中。Ramnivas Laddad 在他撰写的书籍 AspectJ 实战中(请参阅 参考资料)描述了几个这样的模式 —— 例如“工人对象”(Worker Object)和“ 虫子洞”(Wormhole)。Stefan Hanenberg 和 Arno Schmidmeier 在他们的论文“Idioms for Building Software Frameworks in AspectJ” 已经提出了几个有趣的候选模式,例如“模板通知”和“切入点方法”。只有时间会告诉我们这些正在出现的模式是否有用,是否是一些简单的术语,或者只是“处理 AspectJ 中破损事物的修理厂”。例如,在今年的 AOSD 会议上,我参与的一次会话讨论了对“消极初始化 map”这一概念提供更好的语言支持的可能性。对今天模式的研究,可能就会促进明天的语言特性的生成。

致谢

非常感谢 Wes Isberg、Mik Kersten、Ron Bodkin 和 Ramnivas Laddad,他们审阅了早期的草稿,并提供了非常有帮助的意见和更正。

支持复杂模式的透明组合和代码级重用

developerWorks
文档选项
将打印机的版面设置成横向打印模式

打印本页

将此页作为电子邮件发送

将此页作为电子邮件发送

样例代码


级别: 中级

Nicholas Lesiecki (ndlesiecki@yahoo.com), 软件工程师/编程教师, Google

2005 年 7 月 17 日

Nicholas Lesiecki 用这篇深入研究观察者(Observer)模式的文章,继续他对使用面向方面技术实现设计模式的好处的讨论。他演示了 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)模式的用途是

定义对象之间的一对多依赖关系,因此,当一个对象的状态发生改变时,其所有依赖项都会得到通知,并自动更新。

这使得观察者适用于所有类型的通知需要。请考虑以下情况:

关于本系列

AOP@Work 系列是为具有一定面向方面的编程背景、并准备扩展或者加深其知识的开发人员准备的。与大多数 developerWorks 文章一样,本系列具有很高实用性:从每一篇文章中学到的知识立刻就能使用得上。

所挑选的为这个系列撰稿的每一位作者,都在面向方面编程方面处于领导地位或者拥有这方面的专业知识。许多作者都是本系列中讨论的项目或者工具的开发人员。每篇文章都经过仔细审查,以确保所表达的观点的公平和准确。

关于文章的意见和问题,请直接与文章的作者联系。如果对整个系列有意见,可以与本系列的组织者 Nicholas Lesiecki 联系。更多关于 AOP 的背景知识,请参阅 参考资料

  • 条形图可以观察它显示的数据对象,以便在这些对象变化时对它们进行重新绘制。

  • AccountManager 对象能够观察 Account,这样,在帐户状态改变时,它们可以向销售人员发送一封电子邮件。

  • 支付服务能够观察在线音乐商店中的歌曲播放事件,以便向客户收费。

我将把重点放在最后一个场景上,将它作为这篇文章的示例。(这种针对问题域的想法是从 “在 .NET 中实现观察者”中借鉴的;有关的细节,请参阅 参考资料 一节。)假设您要向在线音乐商店添加一些特性。商店已经拥有逐个播放 Songs 的能力,还有把它们组合成在线 Playlists 的能力。客户还可以查看指定歌曲的歌词。现在需要添加收费和统计功能。首先,系统应当跟踪播放和歌词显示事件,以便对客户进行适当的收费。第二,系统该当更新播放最频繁的歌曲列表,用来在“最流行”部分显示。清单 1 包含系统中已经存在的核心对象的代码:


清单 1. 在线音乐服务的核心领域对象
//common interface for items that
                        //can be played
                        public interface Playable {
                        String getName();
                        void play();
                        }
                        public class Song implements Playable{
                        private String name;
                        public Song(String name) {
                        this.name = name;
                        }
                        public String getName() {
                        return name;
                        }
                        public void play() {
                        System.out.println("Playing song " + getName());
                        }
                        public void showLyrics(){
                        System.out.println("Displaying lyrics for " + getName());
                        }
                        }
                        public class Playlist implements Playable {
                        private String name;
                        private List songs = new ArrayList();
                        public Playlist(String name) {
                        this.name = name;
                        }
                        public void setSongs(List songs) {
                        this.songs = songs;
                        }
                        public void play() {
                        System.out.println("playing album " + getName());
                        for (Song song : songs) {
                        song.play();
                        }
                        }
                        public String getName() {
                        return name;
                        }
                        }
                        public class BillingService{
                        public void generateChargeFor(Playable playable) {
                        System.out.println("generating charge for : " + playable.getName());
                        }
                        }
                        public class SongPlayCounter {
                        public void incrementPlays(Song s){
                        System.out.println("Incrementing plays for " + s.getName());
                        }
                        }
                        

现在已经看到了核心系统,让我们来考虑一下 AOP 之前的观察者实现。







Java 语言的观察者

虽然实现的差异很明显,但在它们之间还是有一些相似之处。不论如何实现观察者,代码中都必须回答以下 4 个问题:

  1. 哪个对象是主体,哪个对象是观察者?
  2. 什么时候主体应当向它的观察者发送通知?
  3. 当接收到通知时,观察者应该做什么?
  4. 观察关系应当在什么时候开始,什么时候终止?

我将用这些问题作为框架,带您经历观察者(Observer)模式的 OO 实现。

角色定义

首先从标记器接口来分配角色开始。Observer 接口只定义了一个方法:update(),它对应着 Subject 发送通知时执行的操作。 Subject 承担着更多的职责。它的标记器接口定义了两个方法,一个用来跟踪观察者,另一个用来通知事件的那些观察者。

public interface Subject {
                        public void addObserver(Observer o);
                        public void removeObserver(Observer o);
                        public void notifyObservers();
                        }
                        

一旦定义了这些角色,就可以把它们应用到系统中对应的角色上。

应用观察者角色

可以修改 BillingService,用以下少量代码实现观察者接口:

public class BillingService implements Observer {
                        //...
                        public void update(Subject subject) {
                        generateChargeFor((Playable) subject);
                        }
                        }
                        

跟踪和通知观察者

一旦这项工作完成,就可以转移到两个 Subject。在这里,要对 Song 进行修改:

                        private Set observers = new HashSet();
                        public void addObserver(Observer o) {
                        observers.add(o);
                        }
                        public void removeObserver(Observer o) {
                        observers.remove(o);
                        }
                        public void notifyObservers() {
                        for (Observer o : observers) {
                        o.update(this);
                        }
                        }
                        

现在面临的是一个不太让人高兴的任务:必须把 Subject 的这个实现剪切、粘贴到 Playlist 中。将 Subject 实现的一部分摘录到一个助手类中,这有助于缓解设计的弊端,但是仍然不足以完全消除它。

有没有更好的 OO 观察者?

这篇文章侧重在 Java 语言的 OO 模式实现上。读者可能想知道是否有特性更丰富的 OO 语言能够更好地解决观察者角色带来的这些令人头痛的问题。给这篇文章带来灵感的 .NET 实现采用委托和事件,极大地减轻了 .NET 版本的 SongPlaylist 类的负担。(要比较两个实现,请参阅 参考资料 一节。)类似的,支持多继承的 OO 语言也能把 Song 的负担限制到在一个 Subject 支持类中的混合上。每种技术都有很大帮助,但是都不能处理触发通知或组合多个模式实例的问题。我将在本文的后面部分讨论这些问题的后果。

触发事件

现在已经把类调整到它们在模式中的角色上了。但是,还需要回过头来,在对应的事件发生时触发通知。Song 要求两个附加通知,而 Playlist 则需要一个:

 //...in Song
                        public void play() {
                        System.out.println("Playing song " + getName());
                        notifyObservers();
                        }
                        public void showLyrics(){
                        System.out.println("Displaying lyrics for " + getName());
                        notifyObservers();
                        }
                        //...in Playlist
                        public void play() {
                        System.out.println("playing album " + getName());
                        for (Song song : songs)  {
                        //...
                        }
                        notifyObservers();
                        }
                        

需要牢记的是,虽然示例系统只需要一个状态改变通知,但是实际的系统可能许多许多通知。例如,我曾经开发过一个应用程序,该应用程序要求在 更改购物车状态时发送观察者风格的通知。为了做到这一点,我在购物车和相关对象中的 10 个以上的位置应用了通知逻辑。

随着各个角色准备好参与到模式中,剩下来要做的就是把它们连接起来。

启动观察关系

为了让 BillingService 开始观察 SongPlaylist,需要添加胶水代码,由它调用 song.addObserver(billingService)。这个要求与 第 1 部分 中描述的适配器和修饰器的胶水代码的要求类似。除了影响参与者之外,模式还要求对系统中不确定的部分进行修改,以便激活模式。清单 2 包含的代码模拟了客户与系统的交互,并整合了这个胶水代码,胶水代码是突出显示的。清单 2 还显示了示例系统的输出。


清单 2. 连接主体和观察者(并试验系统)的客户代码
public class ObserverClient {
                        public static void main(String[] args) {
                        BillingService basicBilling = new BillingService();
                        BillingService premiumBilling = new BillingService();
                        Song song = new Song("Kris Kringle Was A Cat Thief");
                        song.addObserver(basicBilling);
                        Song song2 = new Song("Rock n Roll McDonald's");
                        song2.addObserver(basicBilling);
                        //this song is billed by two services,
                        //perhaps the label demands an premium for online access?
                        song2.addObserver(premiumBilling);
                        Playlist favorites = new Playlist("Wesley Willis - Greatest Hits");
                        favorites.addObserver(basicBilling);
                        List songs = new ArrayList();
                        songs.add(song);
                        songs.add(song2);
                        favorites.setSongs(songs);
                        favorites.play();
                        song.showLyrics();
                        }
                        }
                        //Output:
                        playing playlist Favorites
                        Playing song Kris Kringle Was A Cat Thief
                        generating charge for : Kris Kringle Was A Cat Thief
                        Playing song Rock n Roll McDonald's
                        generating charge for : Rock n Roll McDonald's
                        generating charge for : Rock n Roll McDonald's
                        generating charge for : Favorites
                        Displaying lyrics for Kris Kringle Was A Cat Thief
                        generating charge for : Kris Kringle Was A Cat Thief
                        

Java 语言观察者的分析

从实现的步骤中,您可能感觉到,观察者(Observer)模式是一个重量级的模式。这里的分析将探索该模式对系统当前和潜在的冲击:

  • 横切:在音乐商店应用程序中,记费的观察关注点(observation concern)既包含 记帐人员SongPlaylist),又包含 开票人员BillingService)。Java 语言实现还附加了胶水代码的位置。因为它影响三个不同的类,这些类的目标不仅仅是这个观察关系,所以记费观察就代表了一个横切关注点。

  • 易于理解:先从领域对象的视角开始查看系统。观察者的 OO 实现要求对受模式影响的领域类进行修改。正如在实现类中看到的,这些修改的工作量并不小。对于 Subject 来说,需要给每个类添加多个方法,才能让它在模式中发挥作用。如果领域概念(播放列表、歌曲等)的简单抽象被观察者(Observer)模式的机制阻塞,又会怎么样呢?(请参阅“有没有更好的 OO 观察者?”,了解为什么有些非 Java 语言可能让这项工作更容易。)

    也可以从模式的视角研究系统。在 Java 语言实现中,模式冒着“消失在代码中”的风险。开发人员必须检查模式的三个方面( ObserverSubject 和连接两者的客户代码),才能有意识地构建出模式的坚固模型。如果发现模式的一部分,聪明的开发人员会寻找其他部分,但是没有可以把这些部分集中在一起是“BillingObserver”模块。

  • 重用:要重用这个模式,必须从头开始重新实现它。通常,可以利用预先构建好的支持类(例如,java.util 包含 ObserverObservable )填充模式的某些部分,但是大部分工作仍然需要自己来做。

  • 维护:为了帮助您思考观察者(Observer)模式的维护成本,要先考虑避免双重计费的需求。如果看一眼模式第一个实现的输出(请参阅 清单 2),就会看到应用程序对播放列表上下文中播放歌曲的计费。这并不令人感到惊讶,因为这就是编写这段代码要做的工作。但是,市场部门想为播放列表提供一个总体报价,以鼓励批量购买。换句话说,他们想对播放列表计费,而不是对播放列表中的歌曲计费。

    很容易就可以想像得到,这会给传统的观察者实现带来麻烦。需要修改每个 play() 方法,让它们接受一个参数,表明它是由另一个 play() 方法调用的。或者,可以维护一个 ThreadLocal,用它来跟踪这方面的信息。不管属于哪种情况,都只在调用上下文环境得到保证的情况下才调用 notifyObservers()。这些修改可能会给已经饱受围困的模式参与者再添加了一些负担和复杂性。因为变化将影响多个文件,所以在重新设计的过程中就可能出现 bug。

  • 组合:需要考虑的另一个场景就是不同观察者的观察。从设置中您可能会想起,音乐服务应当跟踪最流行的歌曲。但是统计搜集只被应用于歌曲播放,而不涉及歌词查看或播放列表播放。这意味着 SongCountObserver 观察的操作集合与记费观察者观察的略有不同。

    要实现这点,就不得不修改 Song,让它维护一个独立的 Observer 列表,只对 play() 通知感兴趣(对歌词操作没有兴趣)。然后,play() 方法会独立于记费事件触发这个事件。这样 OO 模式就保证了具体的 Subject 对具体的 Observer 的直接依赖。但是这种情况看起来是不可能的,因为 Song 必须为每种类型的 Observer 触发不同的事件。面对着另一种 Observer 类型,Java 语言的实现就会崩溃。

正如您可能想到的,AspectJ 观察者为这个分析所提出的场景提供了圆满的解决方案。在研究它们之前,我将介绍 AspectJ 如何处理基本模式。







AspectJ 观察者

就像使用我在这篇文章中考虑的其他模式一样,观察者(Observer)模式的目的,甚至是观察者(Observer)模式的基本结构,在用 AspectJ 实现时保持不变。但是,有一个关键的区别。通过使用面向方面的继承,在对模式进行定制,从而满足个人需求时,可以重用模式中对于所有实现都公用的那些部分。请记住,观察者(Observer)模式实现必须回答以下 4 个问题:

  1. 哪个对象是主体,哪个对象是观察者?
  2. 什么时候主体应当向它的观察者发送通知?
  3. 当接收到通知时,观察者应该做什么?
  4. 观察关系应当在什么时候开始,什么时候终止?

抽象的模式方面

首先我要回顾抽象方面的工作方式。与抽象类非常相似,在使用抽象方面之前,必须扩展它。抽象方面可以定义抽象的切入点(切入点有名称,但是没有主体),然后定义这些切入点上的具体的通知(advice)。这意味着由父方面指定行为,子方面控制在哪里应用行为。另外,抽象方面还能定义子方面必须覆盖的抽象方法,就像抽象类能够做到的那样。记住这些事实,现在来看一下来自 设计模式项目ObserverProtocol 方面。

角色定义

就像使用其他实现一样,角色由标记器接口指定。但是,在 AspectJ 实现的情况下,接口是空的(可能是被 AspectJ 5 中的注释代替)。接口以 ObserverProtocol 成员的形式出现:

/**
                        * This interface is used by extending aspects to say what types
                        * can be Subjects.
                        */
                        public interface Subject{}
                        /**
                        * This interface is used by extending aspects to say what types
                        * can be Observers.
                        */
                        public interface Observer{}
                        

AOP 设计模式项目

我对设计模式和 AspectJ 的狂热开始于不列颠哥伦比亚大学赞助的面向方面设计模式实现项目(请参阅 参考资料)。正如我在 这篇文章的第一部分 提到的,Jan Hanneman 和 Gregor Kiczales 调查了 23 个 GoF 模式,并用 Java 语言和 AspectJ 为每一种模式创建了参考实现。结果非常迷人,值得研究,在原始的模式中,有 17 个模式在使用 AOP 实现时得到了改进。最令人兴奋的是模式库。项目的作者能够从 13 个模式中提取出可重用的抽象方面,并在 Mozilla 公共许可下免费提供它们。本文介绍的 ObserverProtocol 方面就取自该项目。请参阅 参考资料 一节,以下载这些模式。

跟踪观察者

方面没有强迫参与者跟踪观察者,而是把这个职责集中起来。清单 3 包含的代码实现了模式的这一部分。从 第 1 部分 的修饰器 AOP 实现中,您应当了解了这个通用术语。同样,方面使用消极初始化的 Map 对特定于对象的状态进行跟踪。(同样,这个模式也有包含初始化模型的替代品,但是它们超出了本文的范畴。)请注意清单 3 中的一个有趣的元素 —— addObserverremoveObserver 方法是公共的。这意味着系统中任何地方的代码都可以用编程的方式决定是否参与到这个模式中。


清单 3. ObserverProtocol 管理观察主角的观察者
/**
                        * Stores the mapping between Subjects and
                        * Observers. For each Subject, a LinkedList
                        * is of its Observers is stored.
                        */
                        private WeakHashMap perSubjectObservers;
                        /**
                        * Returns a Collection of the Observers of
                        * a particular subject. Used internally.
                        */
                        protected List getObservers(Subject subject) {
                        if (perSubjectObservers == null) {
                        perSubjectObservers = new WeakHashMap();
                        }
                        List observers = (List)perSubjectObservers.get(subject);
                        if ( observers == null ) {
                        observers = new LinkedList();
                        perSubjectObservers.put(subject, observers);
                        }
                        return observers;
                        }
                        /**
                        * Adds an Observer to a Subject.
                        */
                        public void addObserver(Subject subject, Observer observer) {
                        getObservers(subject).add(observer);
                        }
                        /**
                        * Removes an observer from a Subject.
                        */
                        public void removeObserver(Subject subject, Observer observer) {
                        getObservers(subject).remove(observer);
                        }
                        //aspect continues...
                        

通知观察者

要实现对观察者的实际更新,ObserverProtocol 需要使用一个循环,这非常类似于它在 Java 语言实现中的使用。但是,它激活通知的方式非常不同。首先,方面定义了一个抽象切入点:

protected abstract pointcut subjectChange(Subject s);
                        

这个切入点由子方面负责具体化。ObserverProtocol 接着定义了一条通知,该通知对应着 subjectChange() 选中的连接点:

after(Subject subject) returning : subjectChange(subject) {
                        for (Observer observer : getObservers(subject)) {
                        updateObserver(subject, observer);
                        }
                        }
                        protected abstract void updateObserver(Subject subject, Observer observer);
                        

对于发生变化的主角的每个 Observer,方面都需要调用 updateObserver(),这是它定义的一个抽象方法。通过这种方式,具体的方面可以定义它接收到更新时的意义。

具体的子方面

目前,您已经看到 ObserverProtocol 提供的所有内容。要应用这个模式,则需要扩展方面,并对这篇讨论开始提出的 4 个核心的观察者(Observer)模式问题提供具体的答案。下载了 ObserverProtocol 之后,就可以开始创建自己的方面,并声明它扩展了 ObserverProtocol。正如您可能想到的,AspectJ 帮助器会提醒您没有完成方面的定义。必须具体化 subjectChange 切入点,并实现 updateObserver 方法。这些提醒可以充当将模式应用到系统中的指导。

角色分配

使用 AspectJ,不用直接修改 SongPlaylistBillingService 类,就可以把角色分配给模式的参与者,如下所示。

declare parents : Playable extends Subject;
                        declare parents : BillingService implements Observer;
                        

编译器不会强迫您插入这些声明,但是您会发现如果没有它们,就无法使用其余的方面机制,因为 ObserverProtocol 将根据这些标记器接口定义它的切入点和方法。

触发事件

这可能是这个方面最重要的部分:具体化 subjectChange 切入点,定义那些操作(构成值得通知的事件)。在这种情况下,切入点指定 play() 方法可以在任何实现 Playable 接口的类上执行(包括 SongPlaylist都可以这么做)。切入点也选择执行 Song 上的 showLyrics() 方法:

                        pointcut titleUse(Playable playable) :
                        this(playable)
                        && (
                        execution(public void Playable+.play()) ||
                        execution(public void Song.showLyrics())
                        );
                        public pointcut subjectChange(Subject subject) :
                        titleUse(Playable) && this(subject);
                        

响应更新

非常类似于 Java 语言版本,为了响应更新,需要实现更新方法。区别在于,在这里是向方面添加方法,而不是直接向 BillingService 添加,后者不知道自己参与到了模式中:

                        public void updateObserver(Subject s, Observer o){
                        BillingService service = (BillingService)o;
                        service.generateChargeFor((Playable)s);
                        }
                        

启动观察关系

正如我在前面所提到的,由于方面上存在公共的 addObserverremoveObserver,所以可以用编程方式对方面进行配置。要启动对歌曲和播放列表的观察,需要复制 Java 语言的客户实现,并用 ObserverBillingPolicy.aspectOf().addObserver(song, basicBilling) 替代出现 song.addObserver(basicBilling) 的地方。

但是,为了让模式的存在尽可能保持在不显眼的状态,所以,使用通知自动地开始这种关系是有意义的,如下所示:

                        //in the concrete aspect
                        //could be injected by a dependency injection framework like Spring
                        //see the Resources section for an excellent blog entry on this topic
                        private BillingService defaultBillingService =
                        new BillingService();
                        pointcut playableCreation(Subject s) :
                        execution(public Playable+.new(..))
                        && this(s);
                        after(Subject s) returning : playableCreation(s){
                        addObserver(s, defaultBillingService);
                        }
                        

通过使用方面中配置的默认 BillingService,该切入点和通知在创建 Playable 时立即开始计费观察。但是,ObserverProtocol 使得这方面具有灵活性。请进一步想像一下:通知会将辅助计费应用到特定歌曲上,或者检索与系统当前用法有关的计费计划。类似的,可以用通知在歌曲不符合未来计费要求时自动终止关系。






AspectJ 观察者的分析

我要做的某些设计观察听起来应当很熟悉,因为它们反映了对本文中讨论的前两个模式所做的观察(请参阅 参考资料)。但是,您会注意到,观察者中的设计对比特别突出:

  • 易于理解:从参与者的视角来看,AOP 版本的观察者更简单,因为它不需要 SongPlaylistBillingService 有什么特殊的东西。从参与者那里消除了这种依赖性,从主要抽象角度来说,让它们更容易理解,同时也使它们更加灵活、更具重用性。例如,它们可以重新用在其他不要求观察者(Observer)模式的系统中。它们也可以参与到其他模式中,而不用担心大量的模式代码会淹没类的核心职责。

    从模式的视角来看,AOP 版本还是更简单一些。AspectJ 实现不要求人们把模式片断汇集在头脑中,而是提供了一个 ObserverProtocol 方面,安排好了模式的结构。然后由具体的方面定义模式应用到这个系统的方式。模块都是很实际的,而且从模式视角来看,模块也很完整。所以,如果正在使用 AJDT,并且注意到(比如)某个观察者方面通知说使用 play() 方法,那么就可以沿着 AJDT 提供的线索理解总体情况。( 参考资料 一节包含一篇优秀文章,其中介绍了 AJDT 如何有助于导航和理解 AspectJ 代码。)

    通知 “事件”的生成在 AspectJ 实现中获得很好的模块化。在传统实现中,这些调用存在于三个不同的位置。不存在能说明为什么某个操作会触发事件,或者其他哪个操作也会这么做的直接线索。在 AOP 模式中,命名切入点沟通了操作的公共属性(标题的用法)。切入点规范指出了模式影响的其他连接点。

  • 重用ObserverProtocol 方面的重用潜力非常高。实际上,可重用的模式方面是我编写这篇文章的一半原因。作为开发人员,只要能依赖框架作者的经验,而不是折腾自己的实现,我就会很高兴。方面提供了重用横切代码的能力,这些代码以前纠缠在实现的细节中,根本不是独立存在的。在我分析的组合模式一节中,您会看到重用的一个很好的示例。

  • 维护:要查看方面让系统更容易发展的具体示例,请回到在系统的 Java 版本中将歌曲从双重计费中排除时遇到的困难上。要用 AspectJ 满足这个需求,只需要编辑 subjectChange() 切入点即可。以下代码可以确保 cflowbelow() 切入点会排除另一个 play() 的控制流程中发生的 play() 执行:

                                //the original titleUsePointcut
                                pointcut titleUse(Playable playable) :
                                this(playable)
                                && (
                                execution(public void Playable+.play()) ||
                                execution(public void Song.showLyrics())
                                );
                                //exclude title usage that occurs as a result
                                //of another title use
                                pointcut topLevelTitleUse(Playable playable) :
                                titleUse(playable) && ! cflowbelow(titleUse(Playable));
                                //define subjectChange in terms of the more restrictive
                                //pointcut
                                public pointcut subjectChange(Subject subject) :
                                topLevelTitleUse(Playable) && this(subject);
                                



    这个修改是微不足道的,特别是因为它只影响一个文件。而且,它还让新策略的意图变得很清楚。其他修改,例如添加新 Subjects 或主角变化操作,都只需像这样对方面进行简单的修改即可,不需要协调多个文件的修改。

  • 组合:可以回想一下介绍过的内容,在与应用到(某些)相同的参与者的第二个模式协作时,Java 语言实现的观察者也存在问题。要用 AspectJ 实现这个需求,只要再次用新的切入点和抽象方法定义扩展抽象方面即可。由于每个方面都管理自己的 Observer 列表和 Subject 列表,而且方面也定义了自己的 subjectChange 切入点,所以两个方面不会冲突。每个方面操作时实际上都独立于另一个方面。可以检查清单 4 中 SongCountObserver 的代码:


    清单 4.影响同一个参与者的第二个观察者方面
    public aspect SongCountObserver extends ObserverProtocol {
                                declare parents : Song extends Subject;
                                declare parents : SongPlayCounter implements Observer;
                                pointcut titleUse(Song song) :
                                this(song)
                                && execution(public void Song.play());
                                public pointcut subjectChange(Subject subject) :
                                titleUse(Song) && this(subject);
                                public void updateObserver(Subject s, Observer o) {
                                SongPlayCounter counter = (SongPlayCounter) o;
                                counter.incrementPlays((Song) s);
                                }
                                // could be injected by a dependency injection
                                // framework like Spring
                                private SongPlayCounter defaultCounter = new SongPlayCounter();
                                pointcut songCreation(Subject s) :
                                execution(public Song.new(..))
                                && this(s);
                                after(Subject s) returning : songCreation(s){
                                // different songs could be tracked in different statistical sets...
                                addObserver(s, defaultCounter);
                                }
                                }
                                



    因为在 AspectJ 系统中,多个模式(甚至同一模式的多个实例)可以透明地组合在一起,所以 AspectJ 系统避免了来自“模式密度”的一些问题。这就能够更多地使用设计模式所拥有的最佳实践,而不用担心实现的重量会压跨代码。(请继续等待 Wes Isberg 在 AOP@Work 系列中即将推出的文章,获取更多使用面向方面技术来避免 OO 设计模式中的模式密度问题的内容。)





结束语

从核心上看,设计模式实际是设计问题。因为程序员都很聪明,所以这些问题已经被解决了许多次。但也因为程序员的懒惰(用一种好的方式懒惰!),他们不愿意一次又一次地重复解决这些设计问题的工作。GoF 撰写的书籍(以及以前的模式工作)的贡献就是把为那些用今天的语言还不能更好表达的问题提供了通用的解决方案。这些麻烦的(如果是无价的)解决方案几乎可以解释为一种挑战,一种寻找更好(更易理解、更易重用、更易维护和更易组合)解决底层问题的方法的挑战。

有些 OO 模式很难使用,因为它们解决的问题是横切的。对于这些问题,AOP 提供的解决方案正中问题核心。通过将横切行为搜集到单独的一个模块中,使行为变得更容易理解、修改和重用。因为这种类型的面向方面模式中的参与者不依赖模式代码,所以它们也变得更灵活、更易重用。在本文中,我指出了实际的示例场景和修改,从中可以看出,在用面向方面的方式解决模式问题时,要容易得多。

新的解决方案采用了不同的形式。AOP 能在根本上把某些模式(例如修饰器)简化到很少观察到模式状态的程度。其他模式(例如观察者(Observer)模式)以可重用库模块的形式获得了新生命。总有一天您会发现,在扩展和实现模式方面,会自然得就像使用今天集合框架中的类一样。

有没有特定于 AOP 的模式?

值得进一步研究的一个迷人领域就是解决面向方面语言中重复出现问题的设计模式。从本文中您可以看到一点提示:两个方面采用消极初始化的 map,把状态松散地耦合到对象。除此之外,特定于 AOP 的模式(或候选模式)开始出现在用户社区中。Ramnivas Laddad 在他撰写的书籍 AspectJ 实战中(请参阅 参考资料)描述了几个这样的模式 —— 例如“工人对象”(Worker Object)和“ 虫子洞”(Wormhole)。Stefan Hanenberg 和 Arno Schmidmeier 在他们的论文“Idioms for Building Software Frameworks in AspectJ” 已经提出了几个有趣的候选模式,例如“模板通知”和“切入点方法”。只有时间会告诉我们这些正在出现的模式是否有用,是否是一些简单的术语,或者只是“处理 AspectJ 中破损事物的修理厂”。例如,在今年的 AOSD 会议上,我参与的一次会话讨论了对“消极初始化 map”这一概念提供更好的语言支持的可能性。对今天模式的研究,可能就会促进明天的语言特性的生成。

致谢

非常感谢 Wes Isberg、Mik Kersten、Ron Bodkin 和 Ramnivas Laddad,他们审阅了早期的草稿,并提供了非常有帮助的意见和更正。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多