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)模式。在每个实现之后,我将讨论是什么造成模式的横切,模式的这个版本在理解、维护、重用和组合代码方面有什么效果。
在继续后面的讨论之前,请单击本页顶部或底部的 代码 图标,下载本文的源代码。
根据 Portland Pattern Repository Wiki(请参阅
参考资料 一节,获得有关的细节),观察者(Observer)模式的用途是
定义对象之间的一对多依赖关系,因此,当一个对象的状态发生改变时,其所有依赖项都会得到通知,并自动更新。
这使得观察者适用于所有类型的通知需要。请考虑以下情况:
- 条形图可以观察它显示的数据对象,以便在这些对象变化时对它们进行重新绘制。
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 之前的观察者实现。
虽然实现的差异很明显,但在它们之间还是有一些相似之处。不论如何实现观察者,代码中都必须回答以下 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 实现的一部分摘录到一个助手类中,这有助于缓解设计的弊端,但是仍然不足以完全消除它。
触发事件
现在已经把类调整到它们在模式中的角色上了。但是,还需要回过头来,在对应的事件发生时触发通知。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 开始观察 Song
或 Playlist ,需要添加胶水代码,由它调用 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)既包含
记帐人员(
Song 和 Playlist ),又包含
开票人员(BillingService )。Java 语言实现还附加了胶水代码的位置。因为它影响三个不同的类,这些类的目标不仅仅是这个观察关系,所以记费观察就代表了一个横切关注点。
- 易于理解:先从领域对象的视角开始查看系统。观察者的 OO 实现要求对受模式影响的领域类进行修改。正如在实现类中看到的,这些修改的工作量并不小。对于
Subject 来说,需要给每个类添加多个方法,才能让它在模式中发挥作用。如果领域概念(播放列表、歌曲等)的简单抽象被观察者(Observer)模式的机制阻塞,又会怎么样呢?(请参阅“有没有更好的
OO 观察者?”,了解为什么有些非 Java 语言可能让这项工作更容易。)
也可以从模式的视角研究系统。在 Java 语言实现中,模式冒着“消失在代码中”的风险。开发人员必须检查模式的三个方面(
Observer 、Subject 和连接两者的客户代码),才能有意识地构建出模式的坚固模型。如果发现模式的一部分,聪明的开发人员会寻找其他部分,但是没有可以把这些部分集中在一起是“BillingObserver”模块。
- 重用:要重用这个模式,必须从头开始重新实现它。通常,可以利用预先构建好的支持类(例如,
java.util
包含 Observer 和 Observable
)填充模式的某些部分,但是大部分工作仍然需要自己来做。
- 维护:为了帮助您思考观察者(Observer)模式的维护成本,要先考虑避免双重计费的需求。如果看一眼模式第一个实现的输出(请参阅
清单 2),就会看到应用程序对播放列表上下文中播放歌曲的计费。这并不令人感到惊讶,因为这就是编写这段代码要做的工作。但是,市场部门想为播放列表提供一个总体报价,以鼓励批量购买。换句话说,他们想对播放列表计费,而不是对播放列表中的歌曲计费。
很容易就可以想像得到,这会给传统的观察者实现带来麻烦。需要修改每个 play()
方法,让它们接受一个参数,表明它是由另一个 play() 方法调用的。或者,可以维护一个
ThreadLocal ,用它来跟踪这方面的信息。不管属于哪种情况,都只在调用上下文环境得到保证的情况下才调用
notifyObservers() 。这些修改可能会给已经饱受围困的模式参与者再添加了一些负担和复杂性。因为变化将影响多个文件,所以在重新设计的过程中就可能出现
bug。
- 组合:需要考虑的另一个场景就是不同观察者的观察。从设置中您可能会想起,音乐服务应当跟踪最流行的歌曲。但是统计搜集只被应用于歌曲播放,而不涉及歌词查看或播放列表播放。这意味着
SongCountObserver 观察的操作集合与记费观察者观察的略有不同。
要实现这点,就不得不修改 Song ,让它维护一个独立的 Observer
列表,只对 play() 通知感兴趣(对歌词操作没有兴趣)。然后,play()
方法会独立于记费事件触发这个事件。这样 OO 模式就保证了具体的 Subject
对具体的 Observer 的直接依赖。但是这种情况看起来是不可能的,因为
Song 必须为每种类型的 Observer 触发不同的事件。面对着另一种
Observer 类型,Java 语言的实现就会崩溃。
正如您可能想到的,AspectJ 观察者为这个分析所提出的场景提供了圆满的解决方案。在研究它们之前,我将介绍
AspectJ 如何处理基本模式。
就像使用我在这篇文章中考虑的其他模式一样,观察者(Observer)模式的目的,甚至是观察者(Observer)模式的基本结构,在用
AspectJ 实现时保持不变。但是,有一个关键的区别。通过使用面向方面的继承,在对模式进行定制,从而满足个人需求时,可以重用模式中对于所有实现都公用的那些部分。请记住,观察者(Observer)模式实现必须回答以下
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{}
|
跟踪观察者
方面没有强迫参与者跟踪观察者,而是把这个职责集中起来。清单 3 包含的代码实现了模式的这一部分。从
第 1 部分 的修饰器 AOP 实现中,您应当了解了这个通用术语。同样,方面使用消极初始化的 Map
对特定于对象的状态进行跟踪。(同样,这个模式也有包含初始化模型的替代品,但是它们超出了本文的范畴。)请注意清单
3 中的一个有趣的元素 —— addObserver 和 removeObserver
方法是公共的。这意味着系统中任何地方的代码都可以用编程的方式决定是否参与到这个模式中。
清单 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,不用直接修改 Song 、Playlist
和 BillingService 类,就可以把角色分配给模式的参与者,如下所示。
declare parents : Playable extends Subject;
declare parents : BillingService implements Observer;
|
编译器不会强迫您插入这些声明,但是您会发现如果没有它们,就无法使用其余的方面机制,因为 ObserverProtocol
将根据这些标记器接口定义它的切入点和方法。
触发事件
这可能是这个方面最重要的部分:具体化 subjectChange 切入点,定义那些操作(构成值得通知的事件)。在这种情况下,切入点指定
play() 方法可以在任何实现 Playable
接口的类上执行(包括 Song 和 Playlist 都可以这么做)。切入点也选择执行
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);
}
|
启动观察关系
正如我在前面所提到的,由于方面上存在公共的 addObserver 和
removeObserver ,所以可以用编程方式对方面进行配置。要启动对歌曲和播放列表的观察,需要复制
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
使得这方面具有灵活性。请进一步想像一下:通知会将辅助计费应用到特定歌曲上,或者检索与系统当前用法有关的计费计划。类似的,可以用通知在歌曲不符合未来计费要求时自动终止关系。
我要做的某些设计观察听起来应当很熟悉,因为它们反映了对本文中讨论的前两个模式所做的观察(请参阅 参考资料)。但是,您会注意到,观察者中的设计对比特别突出:
从核心上看,设计模式实际是设计问题。因为程序员都很聪明,所以这些问题已经被解决了许多次。但也因为程序员的懒惰(用一种好的方式懒惰!),他们不愿意一次又一次地重复解决这些设计问题的工作。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,他们审阅了早期的草稿,并提供了非常有帮助的意见和更正。
名字 |
大小 |
下载方法 |
j-aopwork56code.zip |
142 KB |
HTTP |
|