【编者按】本文作者是 BAE 系统公司的软件工程师 Justin Albano。在本篇文章中,作者通过在 Java8 环境下实现观察者模式的实例,进一步介绍了什么是观察者模式、专业化及其命名规则,供大家参考学习。
观察者(Observer)模式又名发布-订阅(Publish/Subscribe)模式,是四人组(GoF,即 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides)在1994合著的《设计模式:可复用面向对象软件的基础》中提出的(详见书中293-313页)。尽管这种模式已经有相当长的历史, 它仍然广泛适用于各种场景,甚至成为了标准Java库的一个组成部分。目前虽然已经有大量关于观察者模式的文章,但它们都专注于在 Java 中的实现,却忽视了开发者在Java中使用观察者模式时遇到的各种问题。
本文的写作初衷就是为了填补这一空白:本文主要介绍通过使用 Java8 架构实现观察者模式,并在此基础上进一步探讨关于经典模式的复杂问题,包括匿名内部类、lambda 表达式、线程安全以及非平凡耗时长的观察者实现。本文内容虽然并不全面,很多这种模式所涉及的复杂问题,远不是一篇文章就能说清的。但是读完本文,读者能 了解什么是观察者模式,它在Java中的通用性以及如何处理在 Java 中实现观察者模式时的一些常见问题。
观察者模式
根据 GoF 提出的经典定义,观察者模式的主旨是:
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
什么意思呢?很多软件应用中,对象之间的状态都是互相依赖的。例如,如果一个应用专注于数值数据加工,这个数据也许会通过图形用户界面(GUI)的表格或图 表来展现或者两者同时使用,也就是说,当底层数据更新时,相应的 GUI 组件也要更新。问题的关键在于如何做到底层数据更新时 GUI 组件也随之更新,同时尽量减小 GUI 组件和底层数据的耦合度。
一种简单且不可扩展的解决方案是给管理这些底层数据的对象该表格和图像 GUI 组件的引用,使得对象可以在底层数据变化时能够通知 GUI 组件。显然,对于处理有更多 GUI 组件的复杂应用,这个简单的解决方案很快显示出其不足。例如,有20个 GUI 组件都依赖于底层数据,那么管理底层数据的对象就需要维护指向这20个组件的引用。随着依赖于相关数据的对象数量的增加,数据管理和对象之间的耦合度也变 得难以控制。
另一个更好的解决方案是允许对象注册获取感兴趣数据更新的权限,当数据变化时,数据管理器就会通知这些对象。通俗地说就是,让感兴趣的数据对象告诉管理器:“当数据变化时请通知我”。此外,这些对象不仅可以注册获取更新通知,也可以取消注册,保证数据管理器在数据变化时不再通知 该对象。在 GoF 的原始定义中,注册获取更新的对象叫作“观察者”(observer),对应的数据管理器叫作“目标”(Subject),观察者感兴趣的数据叫作“目标 状态”,注册过程叫“添加”(attach),撤销观察的过程叫“移除”(detach)。前文已经提到观察者模式又叫发布-订阅模式,可以理解为客户订 阅关于目标的观察者,当目标状态更新时,目标把这些更新发布给订阅者(这种设计模式扩展为通用架构,称为发布——订阅架构)。这些概念可以用下面的类图表 示:
具体观察者(ConcereteObserver)用来接收更新的状态变化,同时将指向具体主题(ConcereteSubject)的引用传递给它的构造 函数。这为具体观察者提供了指向具体主题的引用,在状态变化时可由此获得更新。简单来说,具体观察者会被告知主题更新,同时用其构造函数中的引用来获取具 体主题的状态,最后将这些检索状态对象存储在具体观察者的观察状态(observerState)属性下。这一过程如下面的序列图所示:
经典模式的专业化
尽管观察者模式是通用的,但也有很多专业化的模式,最常见是以下两种:
- 为State对象提供一个参数,传给观察者调用的Update方法。在经典模式下,当观察者被通知Subject状态发生变化后,会直接从Subject获得其更新后状态。这要求观察者保存指向获取状态的对象引用。这样就形成了一个循环引用,ConcreteSubject的引用指向其观察 者列表,ConcreteObserver的引用指向能获得主题状态的ConcreteSubject。除了获得更新的状态,观察者和其注册监听的 Subject间并没有联系,观察者关心的是State对象,而非Subject本身。也就是说,很多情况下都将ConcreteObserver和 ConcreteSubject强行联系一起,相反,当ConcreteSubject调用Update函数时,将State对象传递给 ConcreteObserver,二者就无需关联。ConcreteObserver和State对象之间关联减小了观察者和State之间的依赖程度 (关联和依赖的更多区别请参见Martin Fowler’s的文章)。
- 将Subject抽象类和ConcreteSubject合并到一个 singleSubject类中。多数情况下,Subject使用抽象类并不会提升程序的灵活性和可扩展性,因此,将这一抽象类和具体类合并简化了设计。
这两个专业化的模式组合后,其简化类图如下:
在这些专业化的模式中,静态类结构大大简化,类之间的相互作用也得以简化。此时的序列图如下:
专业化模式另一特点是删除了 ConcreteObserver 的成员变量 observerState。有时候具体观察者并不需要保存Subject的最新状态,而只需要监测状态更新时 Subject 的状态。例如,如果观察者将成员变量的值更新到标准输出上,就可以删除 observerState,这样一来就删除了ConcreteObserver和State类之间的关联。
更常见的命名规则
经典模式甚至是前文提到的专业化模式都用的是attach,detach和observer等术语,而Java实现中很多都是用的不同的词典,包括 register,unregister,listener等。值得一提的是State是listener需要监测变化的所有对象的统称,状态对象的具体 名称需要看观察者模式用到的场景。例如,在listener监听事件发生场景下的观察者模式,已注册的listener将会在事件发生时收到通知,此时的 状态对象就是event,也就是事件是否发生。
平时实际应用中目标的命名很少包含Subject。例如,创建一个关于动物园的应用,注册多个监听器用于观察Zoo类,并在新动物进入动物园时收到通知。该案例中的目标是Zoo类,为了和所给问题域保持术语一致,将不会用到Subject这样的 词汇,也就是说Zoo类不会命名为ZooSubject。
监听器的命名一般都会跟着Listener后缀,例如前文提到的监测新动物加入的监听器会命名为AnimalAddedListener。类似的,register,、unregister和notify等函数命名常会以其对应的监听 器名作后缀,例如AnimalAddedListener的register、unregister、notify函数会被命名为 registerAnimalAddedListener、 unregisterAnimalAddedListener和notifyAnimalAddedListeners,需要注意的是notify函数名 的s,因为notify函数处理的是多个而非单一监听器。
这种命名方式会显得冗长,而且通常一个subject会注册多个类型的监听器,如前面提到的动物园的例子,Zoo内除了注册监听动物新增的监听器,还需注册监听动物减少监听器,此时就会有两种register函数: (registerAnimalAddedListener和 registerAnimalRemovedListener,这种方式处理,监听器的类型作为一个限定符,表示其应观察者的类型。另一解决方案是创建一 个registerListener函数然后重载,但是方案一能更方便的知道哪个监听器正在监听,重载是比较小众的做法。
另一惯用语法是用on前缀而不是update,例如update函数命名为onAnimalAdded而不是updateAnimalAdded。这种情况在监听器获得一个序列的通知时更常见,如向list中新增一个动物,但很少用于更新一个单独的数据,比如动物的名字。
接下来本文将使用Java的符号规则,虽然符号规则不会改变系统的真实设计和实现,但是使用其他开发者都熟悉的术语是很重要的开发准则,因此要熟悉上文描述的Java中的观察者模式符号规则。下文将在Java8环境下用一个简单例子来阐述上述概念。
一个简单的实例
还是前面提到的动物园的例子,使用Java8的API接口实现一个简单的系统,说明观察者模式的基本原理。问题描述为:
创建一个系统zoo,允许用户监听和撤销监听添加新对象animal的状态,另外再创建一个具体监听器,负责输出新增动物的name。
根据前面对观察者模式的学习知道实现这样的应用需要创建4个类,具体是:
- Zoo类:即模式中的主题,负责存储动物园中的所有动物,并在新动物加入时通知所有已注册的监听器。
- Animal类:代表动物对象。
- AnimalAddedListener类:即观察者接口。
- PrintNameAnimalAddedListener:具体的观察者类,负责输出新增动物的name。
首先我们创建一个Animal类,它是一个包含name成员变量、构造函数、getter和setter方法的简单Java对象,代码如下:
public class Animal {
private String name; public Animal (String name) { this.name = name;
} public String getName () { return this.name;
} public void setName (String name) { this.name = name;
}
}
用这个类代表动物对象,接下来就可以创建AnimalAddedListener接口了:
public interface AnimalAddedListener {
public void onAnimalAdded (Animal animal);
}
前面两个类很简单,就不再详细介绍,接下来创建Zoo类:
public class Zoo {
private List<animal> animals = new ArrayList<>(); private List<animaladdedlistener> listeners = new ArrayList<>(); public void addAnimal (Animal animal) { // Add the animal to the list of animals
this.animals.add(animal); // Notify the list of registered listeners
this.notifyAnimalAddedListeners(animal);
} public void registerAnimalAddedListener (AnimalAddedListener listener) { // Add the listener to the list of registered listeners
this.listeners.add(listener);
} public void unregisterAnimalAddedListener (AnimalAddedListener listener) { // Remove the listener from the list of the registered listeners
this.listeners.remove(listener);
} protected void notifyAnimalAddedListeners (Animal animal) { // Notify each of the listeners in the list of registered listeners
this.listeners.forEach(listener -> listener.updateAnimalAdded(animal));
}
}
这个类比前面两个都复杂,其包含两个list,一个用来存储动物园中所有动物,另一个用来存储所有的监听器,鉴于animals和listener集合存储的对象都很简单,本文选择了ArrayList来存储。存储监听器的具体数据结构要视问题而定,比如对于这里的 动物园问题,如果监听器有优先级,那就应该选择其他的数据结构,或者重写监听器的register算法。
注册和移除的实现都是简单的委托方式:各个监听器作为参数从监听者的监听列表增加或者移除。notify函数的实现与观察者模式的标准格式稍微偏离,它包括输入参数:新增加的 animal,这样一来notify函数就可以把新增加的animal引用传递给监听器了。用streams API的forEach函数遍历监听器,对每个监听器执行theonAnimalAdded函数。
在addAnimal函数中,新增的animal对象和监听器各自添加到对应list。如果不考虑通知过程的复杂性,这一逻辑应包含在方便调用的方法中,只需要传入指向新增animal对象 的引用即可,这就是通知监听器的逻辑实现封装在notifyAnimalAddedListeners函数中的原因,这一点在addAnimal的实现中 也提到过。
除了notify函数的逻辑问题,需要强调一下对notify函数可见性的争议问题。在经典的观察者模型中,如GoF在设计模式一书中第301页所说,notify函数是public型的,然而尽管在经典模式中用到,这并不意味着必须是public的。选择可见性应该基于应用,例 如本文的动物园的例子,notify函数是protected类型,并不要求每个对象都可以发起一个注册观察者的通知,只需保证对象能从父类继承该功能即 可。当然,也并非完全如此,需要弄清楚哪些类可以激活notify函数,然后再由此确定函数的可见性。
接下来需要实现PrintNameAnimalAddedListener类,这个类用System.out.println方法将新增动物的name输出,具体代码如下:
public class PrintNameAnimalAddedListener implements AnimalAddedListener {
@Override
public void updateAnimalAdded (Animal animal) { // Print the name of the newly added animal
System.out.println("Added a new animal with name '" + animal.getName() + "'");
}
}
最后要实现驱动应用的主函数:
public class Main {
public static void main (String[] args) { // Create the zoo to store animals
Zoo zoo = new Zoo(); // Register a listener to be notified when an animal is added
zoo.registerAnimalAddedListener(new PrintNameAnimalAddedListener()); // Add an animal notify the registered listeners
zoo.addAnimal(new Animal("Tiger"));
}
}
主函数只是简单的创建了一个zoo对象,注册了一个输出动物name的监听器,并新建了一个animal对象以触发已注册的监听器,最后的输出为:
Added a new animal with name 'Tiger'
新增监听器
当监听器重新建立并将其添加到Subject时,观察者模式的优势就充分显示出来。例如,想添加一个计算动物园中动物总数的监听器,只需要新建一个具体的监 听器类并注册到Zoo类即可,而无需对zoo类做任何修改。添加计数监听器CountingAnimalAddedListener代码如下:
public class CountingAnimalAddedListener implements AnimalAddedListener {
private static int animalsAddedCount = 0; @Override
public void updateAnimalAdded (Animal animal) { // Increment the number of animals
animalsAddedCount++; // Print the number of animals
System.out.println("Total animals added: " + animalsAddedCount);
}
}
修改后的main函数如下:
public class Main {
public static void main (String[] args) { // Create the zoo to store animals
Zoo zoo = new Zoo(); // Register listeners to be notified when an animal is added
zoo.registerAnimalAddedListener(new PrintNameAnimalAddedListener());
zoo.registerAnimalAddedListener(new CountingAnimalAddedListener()); // Add an animal notify the registered listeners
zoo.addAnimal(new Animal("Tiger"));
zoo.addAnimal(new Animal("Lion"));
zoo.addAnimal(new Animal("Bear"));
}
}
输出结果为:
Added a new animal with name 'Tiger'Total animals added: 1Added a new animal with name 'Lion'Total animals added: 2Added a new animal with name 'Bear'Total animals added: 3
使用者可在仅修改监听器注册代码的情况下,创建任意监听器。具有此可扩展性主要是因为Subject和观察者接口关联,而不是直接和ConcreteObserver关联。只要接口不被修改,调用接口的Subject就无需修改。
匿名内部类,Lambda函数和监听器注册
Java8的一大改进是增加了功能特性,如增加了lambda函数。在引进lambda函数之前,Java通过匿名内部类提供了类似的功能,这些类在很多已有的应用 中仍在使用。在观察者模式下,随时可以创建新的监听器而无需创建具体观察者类,例如,PrintNameAnimalAddedListener类可以在 main函数中用匿名内部类实现,具体实现代码如下:
public class Main {
public static void main (String[] args) { // Create the zoo to store animals
Zoo zoo = new Zoo(); // Register listeners to be notified when an animal is added
zoo.registerAnimalAddedListener(new AnimalAddedListener() { @Override
public void updateAnimalAdded (Animal animal) { // Print the name of the newly added animal
System.out.println("Added a new animal with name '" + animal.getName() + "'");
}
}); // Add an animal notify the registered listeners
zoo.addAnimal(new Animal("Tiger"));
}
}
类似的,lambda函数也可以用以完成此类任务:
public class Main {
public static void main (String[] args) { // Create the zoo to store animals
Zoo zoo = new Zoo(); // Register listeners to be notified when an animal is added
zoo.registerAnimalAddedListener(
(animal) -> System.out.println("Added a new animal with name '" + animal.getName() + "'")
); // Add an animal notify the registered listeners
zoo.addAnimal(new Animal("Tiger"));
}
}
需要注意的是lambda函数仅适用于监听器接口只有一个函数的情况,这个要求虽然看起来严格,但实际上很多监听器都是单一函数的,如示例中的AnimalAddedListener。如果接口有多个函数,可以选择使用匿名内部类。
隐式注册创建的监听器存在此类问题:由于对象是在注册调用的范围内创建的,所以不可能将引用存储一个到具体监听器。这意味着,通过lambda函数或者匿名 内部类注册的监听器不可以撤销注册,因为撤销函数需要传入已经注册监听器的引用。解决这个问题的一个简单方法是在 registerAnimalAddedListener函数中返回注册监听器的引用。如此一来,就可以撤销注册用lambda函数或匿名内部类创建的监 听器,改进后的方法代码如下:
public AnimalAddedListener registerAnimalAddedListener (AnimalAddedListener listener) { // Add the listener to the list of registered listeners
this.listeners.add(listener);
return listener;
}
重新设计的函数交互的客户端代码如下:
public class Main {
public static void main (String[] args) { // Create the zoo to store animals
Zoo zoo = new Zoo(); // Register listeners to be notified when an animal is added
AnimalAddedListener listener = zoo.registerAnimalAddedListener(
(animal) -> System.out.println("Added a new animal with name '" + animal.getName() + "'")
); // Add an animal notify the registered listeners
zoo.addAnimal(new Animal("Tiger")); // Unregister the listener
zoo.unregisterAnimalAddedListener(listener); // Add another animal, which will not print the name, since the listener
// has been previously unregistered
zoo.addAnimal(new Animal("Lion"));
}
}
此时的结果输出只有Added a new animal with name ‘Tiger’,因为在第二个animal加入之前监听器已经撤销了:
Added a new animal with name 'Tiger'
如果采用更复杂的解决方案,register函数也可以返回receipt类,以便unregister监听器调用,例如:
public class AnimalAddedListenerReceipt {
private final AnimalAddedListener listener; public AnimalAddedListenerReceipt (AnimalAddedListener listener) { this.listener = listener;
} public final AnimalAddedListener getListener () { return this.listener;
}
}
receipt会作为注册函数的返回值,以及撤销注册函数输入参数,此时的zoo实现如下所示:
public class ZooUsingReceipt {
// ...Existing attributes and constructor...
public AnimalAddedListenerReceipt registerAnimalAddedListener (AnimalAddedListener listener) { // Add the listener to the list of registered listeners
this.listeners.add(listener); return new AnimalAddedListenerReceipt(listener);
} public void unregisterAnimalAddedListener (AnimalAddedListenerReceipt receipt) { // Remove the listener from the list of the registered listeners
this.listeners.remove(receipt.getListener());
} // ...Existing notification method...}
上面描述的接收实现机制允许保存信息供监听器撤销时调用的,也就是说如果撤销注册算法依赖于Subject注册监听器时的状态,则此状态将被保存,如果撤销注册只需要指向之前注册监听器的引用,这样的话接收技术则显得麻烦,不推荐使用。
除了特别复杂的具体监听器,最常见的注册监听器的方法是通过lambda函数或通过匿名内部类注册。当然,也有例外,那就是包含subject实现观察者接口的类和注册一个包含调用该引用目标的监听器。如下面代码所示的案例:
public class ZooContainer implements AnimalAddedListener {
private Zoo zoo = new Zoo(); public ZooContainer () { // Register this object as a listener
this.zoo.registerAnimalAddedListener(this);
} public Zoo getZoo () { return this.zoo;
} @Override
public void updateAnimalAdded (Animal animal) {
System.out.println("Added animal with name '" + animal.getName() + "'");
} public static void main (String[] args) { // Create the zoo container
ZooContainer zooContainer = new ZooContainer(); // Add an animal notify the innerally notified listener
zooContainer.getZoo().addAnimal(new Animal("Tiger"));
}
}
这种方法只适用于简单情况而且代码看起来不够专业,尽管如此,它还是深受现代Java开发人员的喜爱,因此了解这个例子的工作原理很有必要。因为ZooContainer实现了AnimalAddedListener接口,那么ZooContainer的实例(或者说对 象)就可以注册为AnimalAddedListener。ZooContainer类中,该引用代表当前对象即ZooContainer的一个实例,所 以可以被用作AnimalAddedListener。
通常,不是要求所有的container类都实现此类功能,而且实现监听器接口的container类只能调用Subject的注册函数,只是简单把该引用作为监听器的对象传给register函数。在接下来的章节中,将介绍多线程环境的常见问题和解决方案。
线程安全的实现
前面章节介绍了在现代Java环境下的实现观察者模式,虽然简单但很完整,但这一实现忽略了一个关键性问题:线程安全。大多数开放的Java应用都是多线程 的,而且观察者模式也多用于多线程或异步系统。例如,如果外部服务更新其数据库,那么应用也会异步地收到消息,然后用观察者模式通知内部组件更新,而不是 内部组件直接注册监听外部服务。
观察者模式的线程安全主要集中在模式的主体上,因为修改注册监听器集合时很可能发生线程冲突,比如,一个线程试图添加一个新的监听器,而另一线程又试图添加一个新的animal对象,这将触发对所有注册监听器的通知。鉴于先后顺序,在已注册的监听器收到新增动 物的通知前,第一个线程可能已经完成也可能尚未完成新监听器的注册。这是一个经典的线程资源竞争案例,正是这一现象告诉开发者们需要一个机制来保证线程安 全。
这一问题的最简单的解决方案是:所有访问或修改注册监听器list的操作都须遵循Java的同步机制,比如:
public synchronized AnimalAddedListener registerAnimalAddedListener (AnimalAddedListener listener) { /*...*/ }public synchronized void unregisterAnimalAddedListener (AnimalAddedListener listener) { /*...*/ }public synchronized void notifyAnimalAddedListeners (Animal animal) { /*...*/ }
这样一来,同一时刻只有一个线程可以修改或访问已注册的监听器列表,可以成功地避免资源竞争问题,但是新问题又出现了,这样的约束太过严格 (synchronized关键字和Java并发模型的更多信息,请参阅官方网页)。通过方法同步,可以时刻观测对监听器list的并发访问,注册和撤销 监听器对监听器list而言是写操作,而通知监听器访问监听器list是只读操作。由于通过通知访问是读操作,因此是可以多个通知操作同时进行的。
因此,只要没有监听器注册或撤销注册,任意多的并发通知都可以同时执行,而不会引发对注册的监听器列表的资源争夺。当然,其他情况下的资源争夺现象存在已 久,为了解决这一问题,设计了ReadWriteLock用以分开管理读写操作的资源锁定。Zoo类的线程安全ThreadSafeZoo实现代码如下:
public class ThreadSafeZoo {
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); protected final Lock readLock = readWriteLock.readLock(); protected final Lock writeLock = readWriteLock.writeLock(); private List<animal> animals = new ArrayList<>(); private List<animaladdedlistener> listeners = new ArrayList<>(); public void addAnimal (Animal animal) { // Add the animal to the list of animals
this.animals.add(animal); // Notify the list of registered listeners
this.notifyAnimalAddedListeners(animal);
} public AnimalAddedListener registerAnimalAddedListener (AnimalAddedListener listener) { // Lock the list of listeners for writing
this.writeLock.lock(); try { // Add the listener to the list of registered listeners
this.listeners.add(listener);
} finally { // Unlock the writer lock
this.writeLock.unlock();
} return listener;
} public void unregisterAnimalAddedListener (AnimalAddedListener listener) { // Lock the list of listeners for writing
this.writeLock.lock(); try { // Remove the listener from the list of the registered listeners
this.listeners.remove(listener);
} finally { // Unlock the writer lock
this.writeLock.unlock();
}
} public void notifyAnimalAddedListeners (Animal animal) { // Lock the list of listeners for reading
this.readLock.lock(); try { // Notify each of the listeners in the list of registered listeners
this.listeners.forEach(listener -> listener.updateAnimalAdded(animal));
} finally { // Unlock the reader lock
this.readLock.unlock();
}
}
}
通过这样部署,Subject的实现能确保线程安全并且多个线程可以同时发布通知。但尽管如此,依旧存在两个不容忽略的资源竞争问题:
- 对每个监听器的并发访问。多个线程可以同时通知监听器要新增动物了,这意味着一个监听器可能会同时被多个线程同时调用。
- 对animal list的并发访问。多个线程可能会同时向animal list添加对象,如果通知的先后顺序存在影响,那就可能导致资源竞争,这就需要一个并发操作处理机制来避免这一问题。如果注册的监听器列表在收到通知添 加animal2后,又收到通知添加animal1,此时就会产生资源竞争。但是如果animal1和animal2的添加由不同的线程执行,也是有可能 在animal2前完成对animal1添加操作,具体来说就是线程1在通知监听器前添加animal1并锁定模块,线程2添加animal2并通知监听 器,然后线程1通知监听器animal1已经添加。虽然在不考虑先后顺序时,可以忽略资源竞争,但问题是真实存在的。
对监听器的并发访问
并发访问监听器可以通过保证监听器的线程安全来实现。秉承着类的“责任自负”精神,监听器有“义务”确保自身的线程安全。例如,对于前面计数的监听器,多线程的递增或递减动物数量可能导致线程安全问题,要避免这一问题,动物数的计算必须是原子操作 (原子变量或方法同步),具体解决代码如下:
public class ThreadSafeCountingAnimalAddedListener implements AnimalAddedListener {
private static AtomicLong animalsAddedCount = new AtomicLong(0); @Override
public void updateAnimalAdded (Animal animal) { // Increment the number of animals
animalsAddedCount.incrementAndGet(); // Print the number of animals
System.out.println("Total animals added: " + animalsAddedCount);
}
}
方法同步解决方案代码如下:
public class CountingAnimalAddedListener implements AnimalAddedListener {
private static int animalsAddedCount = 0; @Override
public synchronized void updateAnimalAdded (Animal animal) { // Increment the number of animals
animalsAddedCount++; // Print the number of animals
System.out.println("Total animals added: " + animalsAddedCount);
}
}
要强调的是监听器应该保证自身的线程安全,subject需要理解监听器的内部逻辑,而不是简单确保对监听器的访问和修改的线程安全。否则,如果多个subject共用同一个监听器,那每个subject类都要重写一遍线程安全的代码,显然这样的代码不够简洁,因此需要 在监听器类内实现线程安全。
监听器的有序通知
当要求监听器有序执行时,读写锁就不能满足需求了,而需要引入一个新的机制,可以保证notify函数的调用顺序和animal添加到zoo的顺序一致。有人尝试过用方法同步来实现,然而根据Oracle文档中的方法同步介 绍,可知方法同步并不提供操作执行的顺序管理。它只是保证原子操作,也就是说操作不会被打断,并不能保证先来先执行(FIFO)的线程顺序。 ReentrantReadWriteLock可以实现这样的执行顺序,代码如下:
public class OrderedThreadSafeZoo {
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true); protected final Lock readLock = readWriteLock.readLock(); protected final Lock writeLock = readWriteLock.writeLock(); private List<animal> animals = new ArrayList<>(); private List<animaladdedlistener> listeners = new ArrayList<>(); public void addAnimal (Animal animal) { // Add the animal to the list of animals
this.animals.add(animal); // Notify the list of registered listeners
this.notifyAnimalAddedListeners(animal);
} public AnimalAddedListener registerAnimalAddedListener (AnimalAddedListener listener) { // Lock the list of listeners for writing
this.writeLock.lock(); try { // Add the listener to the list of registered listeners
this.listeners.add(listener);
} finally { // Unlock the writer lock
this.writeLock.unlock();
} return listener;
} public void unregisterAnimalAddedListener (AnimalAddedListener listener) { // Lock the list of listeners for writing
this.writeLock.lock(); try { // Remove the listener from the list of the registered listeners
this.listeners.remove(listener);
} finally { // Unlock the writer lock
this.writeLock.unlock();
}
} public void notifyAnimalAddedListeners (Animal animal) { // Lock the list of listeners for reading
this.readLock.lock(); try { // Notify each of the listeners in the list of registered listeners
this.listeners.forEach(listener -> listener.updateAnimalAdded(animal));
} finally { // Unlock the reader lock
this.readLock.unlock();
}
}
}
这样的实现方式,register, unregister和notify函数将按照先进先出(FIFO)的顺序获得读写锁权限。例如,线程1注册一个监听器,线程2在开始执行注册操作后试图 通知已注册的监听器,线程3在线程2等待只读锁的时候也试图通知已注册的监听器,采用fair-ordering方式,线程1先完成注册操作,然后线程2 可以通知监听器,最后线程3通知监听器。这样保证了action的执行顺序和开始顺序一致。
如果采用方法同步,虽然线程2先排队等待占用资源,线程3仍可能比线程2先获得资源锁,而且不能保证线程2比线程3先通知监听器。问题的关键所在:fair-ordering方式可以保证线程按照申请 资源的顺序执行。读写锁的顺序机制很复杂,应参照ReentrantReadWriteLock的官方文档以确保锁的逻辑足够解决问题。
截止目前实现了线程安全,在接下来的章节中将介绍提取主题的逻辑并将其mixin类封装为可重复代码单元的方式优缺点。
主题逻辑封装到Mixin类
把上述的观察者模式设计实现封装到目标的mixin类中很具吸引力。通常来说,观察者模式中的观察者包含已注册的监听器的集合;负责注册新的监听器的 register函数;负责撤销注册的unregister函数和负责通知监听器的notify函数。对于上述的动物园的例子,zoo类除动物列表是问题 所需外,其他所有操作都是为了实现主题的逻辑。
Mixin类的案例如下所示,需要说明的是为使代码更为简洁,此处去掉关于线程安全的代码:
public abstract class ObservableSubjectMixin<listenertype> {
private List<listenertype> listeners = new ArrayList<>(); public ListenerType registerListener (ListenerType listener) { // Add the listener to the list of registered listeners
this.listeners.add(listener); return listener;
} public void unregisterAnimalAddedListener (ListenerType listener) { // Remove the listener from the list of the registered listeners
this.listeners.remove(listener);
} public void notifyListeners (Consumer<!--? super ListenerType--> algorithm) { // Execute some function on each of the listeners
this.listeners.forEach(algorithm);
}
}
正因为没有提供正在注册的监听器类型的接口信息,不能直接通知某个特定的监听器,所以正需要保证通知功能的通用性,允许客户端添加一些功能,如接受泛型参数类型的参数匹配,以适用于每个监听器,具体实现代码如下:
public class ZooUsingMixin extends ObservableSubjectMixin<animaladdedlistener> {
private List<animal> animals = new ArrayList<>(); public void addAnimal (Animal animal) { // Add the animal to the list of animals
this.animals.add(animal); // Notify the list of registered listeners
this.notifyListeners((listener) -> listener.updateAnimalAdded(animal));
}
}
Mixin类技术的最大优势是把观察者模式的Subject封装到一个可重复调用的类中,而不是在每个subject类中都重复写这些逻辑。此外,这一方法使得zoo类的实现更为简洁,只需要存储动物信息,而不用再考虑如何存储和通知监听器。
然而,使用mixin类并非只有优点。比如,如果要存储多个类型的监听器怎么办?例如,还需要存储监听器类型AnimalRemovedListener。 mixin类是抽象类,Java中不能同时继承多个抽象类,而且mixin类不能改用接口实现,这是因为接口不包含state,而观察者模式中state 需要用来保存已经注册的监听器列表。
其中的一个解决方案是创建一个动物增加和减少时都会通知的监听器类型ZooListener,代码如下所示:
public interface ZooListener {
public void onAnimalAdded (Animal animal); public void onAnimalRemoved (Animal animal);
}
这样就可以使用该接口实现利用一个监听器类型对zoo状态各种变化的监听了:
public class ZooUsingMixin extends ObservableSubjectMixin<zoolistener> {
private List<animal> animals = new ArrayList<>(); public void addAnimal (Animal animal) { // Add the animal to the list of animals
this.animals.add(animal); // Notify the list of registered listeners
this.notifyListeners((listener) -> listener.onAnimalAdded(animal));
} public void removeAnimal (Animal animal) { // Remove the animal from the list of animals
this.animals.remove(animal); // Notify the list of registered listeners
this.notifyListeners((listener) -> listener.onAnimalRemoved(animal));
}
}
将多个监听器类型合并到一个监听器接口中确实解决了上面提到的问题,但仍旧存在不足之处,接下来的章节会详细讨论。
Multi-Method监听器和适配器
在上述方法,监听器的接口中实现的包含太多函数,接口就过于冗长,例如,Swing MouseListener就包含5个必要的函数。尽管可能只会用到其中一个,但是只要用到鼠标点击事件就必须要添加这5个函数,更多可能是用空函数体来 实现剩下的函数,这无疑会给代码带来不必要的混乱。
其中一种解决方案是创建适配器(概念来自GoF提出的适配器模式),适配器中以抽象函数的形式实现监听器接口的操作,供具体监听器类继承。这样一来,具体监听器类就可以选择其需要的函数,对adapter不需要的函数采用默认操作即可。例如 上面例子中的ZooListener类,创建ZooAdapter(Adapter的命名规则与监听器一致,只需要把类名中的Listener改为 Adapter即可),代码如下:
public class ZooAdapter implements ZooListener {
@Override
public void onAnimalAdded (Animal animal) {} @Override
public void onAnimalRemoved (Animal animal) {}
}
乍一看,这个适配器类微不足道,然而它所带来的便利却是不可小觑的。比如对于下面的具体类,只需选择对其实现有用的函数即可:
public class NamePrinterZooAdapter extends ZooAdapter {
@Override
public void onAnimalAdded (Animal animal) { // Print the name of the animal that was added
System.out.println("Added animal named " + animal.getName());
}
}
有两种替代方案同样可以实现适配器类的功能:一是使用默认函数;二是把监听器接口和适配器类合并到一个具体类中。默认函数是Java8新提出的,在接口中允许开发者提供默认(防御)的实现方法。
Java库的这一更新主要是方便开发者在不改变老版本代码的情况下,实现程序扩展,因此应该慎用这个方法。部分开发者多次使用后,会感觉这样写的代码不够专业,而 又有开发者认为这是Java8的特色,不管怎样,需要明白这个技术提出的初衷是什么,再结合具体问题决定是否要用。使用默认函数实现的 ZooListener接口代码如下示:
public interface ZooListener {
default public void onAnimalAdded (Animal animal) {} default public void onAnimalRemoved (Animal animal) {}
}
通过使用默认函数,实现该接口的具体类,无需在接口中实现全部函数,而是选择性实现所需函数。虽然这是接口膨胀问题一个较为简洁的解决方案,开发者在使用时还应多加注意。
第二种方案是简化观察者模式,省略了监听器接口,而是用具体类实现监听器的功能。比如ZooListener接口就变成了下面这样:
public class ZooListener {
public void onAnimalAdded (Animal animal) {} public void onAnimalRemoved (Animal animal) {}
}
这一方案简化了观察者模式的层次结构,但它并非适用于所有情况,因为如果把监听器接口合并到具体类中,具体监听器就不可以实现多个监听接口了。例如,如果AnimalAddedListener和AnimalRemovedListener接口写在同一个具体类中,那么 单独一个具体监听器就不可以同时实现这两个接口了。此外,监听器接口的意图比具体类更显而易见,很显然前者就是为其他类提供接口,但后者就并非那么明显 了。
如果没有合适的文档说明,开发者并不会知道已经有一个类扮演着接口的角色,实现了其对应的所有函数。此外,类名不包含adapter,因为类并不适配于某一个接口,因此类名并没有特别暗示此意图。综上所述,特定问题需要选择特定的方法,并没有哪个方法是万能的。
在开始下一章前,需要特别提一下,适配器在观察模式中很常见,尤其是在老版本的Java代码中。Swing API正是以适配器为基础实现的,正如很多老应用在Java5和Java6中的观察者模式中所使用的那样。zoo案例中的监听器或许并不需要适配器,但需 要了解适配器提出的目的以及其应用,因为我们可以在现有的代码中对其进行使用。下面的章节,将会介绍时间复杂的监听器,该类监听器可能会执行耗时的运算或 进行异步调用,不能立即给出返回值。
Complex & Blocking监听器
关于观察者模式的一个假设是:执行一个函数时,一系列监听器会被调用,但假定这一过程对调用者而言是完全透明的。例如,客户端代码在Zoo中添加animal时,在返回添加成功之前, 并不知道会调用一系列监听器。如果监听器的执行需要时间较长(其时间受监听器的数量、每个监听器执行时间影响),那么客户端代码将会感知这一简单增加动物 操作的时间副作用。
本文不能面面俱到的讨论这个话题,下面几条是开发者调用复杂的监听器时应该注意的事项:
- 监听器启动新线程。新线程启动后,在新线程中执行监听器逻辑的同时,返回监听器函数的处理结果,并运行其他监听器执行。
2. Subject启动新线程。与传统的线性迭代已注册的监听器列表不同,Subject的notify函数重启一个新的线程,然后在新线程中迭代监听器列 表。这样使得notify函数在执行其他监听器操作的同时可以输出其返回值。需要注意的是需要一个线程安全机制来确保监听器列表不会进行并发修改。
3. 队列化监听器调用并采用一组线程执行监听功能。将监听器操作封装在一些函数中并队列化这些函数,而非简单的迭代调用监听器列表。这些监听器存储到队列中 后,线程就可以从队列中弹出单个元素并执行其监听逻辑。这类似于生产者-消费者问题,notify过程产生可执行函数队列,然后线程依次从队列中取出并执 行这些函数,函数需要存储被创建的时间而非执行的时间供监听器函数调用。例如,监听器被调用时创建的函数,那么该函数就需要存储该时间点,这一功能类似于 Java中的如下操作:
public class AnimalAddedFunctor {
private final AnimalAddedListener listener; private final Animal parameter; public AnimalAddedFunctor (AnimalAddedListener listener, Animal parameter) { this.listener = listener; this.parameter = parameter;
} public void execute () { // Execute the listener with the parameter provided during creation
this.listener.updateAnimalAdded(this.parameter);
}
}
函数创建并保存在队列中,可以随时调用,这样一来就无需在遍历监听器列表时立即执行其对应操作了。一旦每个激活监听器的函数都压入队列中,“消费者线程”就会给客户端代码返回操作权。之后某个时间点“消费者线程”将会执行这些函数,就像在监听器被notify函数激活时 执行一样。这项技术在其他语言中被叫作参数绑定,刚好适合上面的例子,技术的实质是保存监听器的参数,execute()函数再直接调用。如果监听器接收 多个参数,处理方法也类似。
需要注意的是如果要保存监听器的执行顺序,则需要引入综合排序机制。方案一中,监听器按照正常的顺序激活新线程,这样可以确保监听器按照注册的顺序执行。方案二中,队列支持排序,其中的函数会按照进入队列的顺序执行。简单来说就是,开发者需要重视监听器多线程执 行的复杂程度,加以小心处理以确保实现所需的功能。
结束语
观察者模式在1994年被写进书中以前,就已经是主流的软件设计模式了,为软件设计中经常出现的问题提供了很多令人满意的解决方案。Java一直是使用该模式的引领者,在其标准库中封装了这一模式,但是鉴于Java 更新到了版本8,十分有必要重新考查经典模式在其中的使用。随着lambda表达式和其他新结构的出现,这一“古老的”模式又有了新的生机。无论是处理旧 程序还是使用这一历史悠久的方法解决新问题,尤其对经验丰富的Java开发者来说,观察者模式都是开发者的主要工具。