观察者模式(Observer Pattern)
1. 概述
观察者模式,是在对象间定义一个一对多的关系。
当“一个” 对象的改变状态,依赖它的对象都会收到通知,并自动更新。
其中,“一个” 对象称作主题、可观察者、被观察者;
“多个” 对象被称作观察者、倾听者、订阅者。
这有点类似报社和读者的关系:发生新闻后,报社通过报纸来通知读者。
实际上就是一个典型的观察者模式。
事实上,观察者模式是 JDK 乃至实际程序和库中使用得最多的设计模式。
基本上所有的 Java GUI 均实现了观察者模式。(也就是
Listener
)
2. 实现方式
2.1 实现思路
-
封装变化
观察者模式中,主题的状态和观察者的种类和数量都会变化。
所以,需要对观察者进行封装。
同理,观察者也可能订阅多个主题,所以主题也需要进行封装。
-
针对接口编程,不针对实现编程
所以,我们使用接口,分别将主题和观察者进行封装。
实际上就是让各个主题实现统一的
Subject
接口;观察者实现统一的Observer
接口。 -
多用组合,少用继承
观察者模式是一个一对多的依赖关系,这意味着,主题必须维护一个观察者列表。
状态改变后,通过调用通知方法来逐个通知观察者。
实际上,这意味着将观察者组合进了主题中。
2.2 图解
3. 新设计原则——松耦合
为了交互对象之间的松耦合设计而努力!
所谓的松耦合,指的就是两个类、函数、模块之间相关度不高,改变其中的一个类不会造成另一个类的大幅变化。
观察者模式通过接口的形式来进行交互,主题可以随时增加和删除观察者列表中的观察者;
观察者也不需要关心主题的内容,它只需要接受主题的通知就可以了。
4. 气象站例子的 UML 图解
5. 推和拉
实际上,主题向观察者发送通知并不只有主题向观察者推送这一个方法;
我们还可以让观察者主动从主题拉取数据。
它们的主要区别在于:推的通知方法包含数据,而拉的不包含,只负责传输主题的引用。
// Push
public void notifyObservers() {
for (observer : list) {
observer.update: 2016-11-25
}
}
// Pull
public void notifyObservers() {
for (observer : list) {
observer.update: 2016-11-25
}
}
拉的方法实现起来也很方便:
- 首先主题提供 getter
- 随后将主题本身作为参数传递给观察者即可。
采用拉的好处在于,观察者种类繁多,需要的数据不尽相同,这样一来,观察者只需要获取自己感兴趣的数据即可,而不需要同时拿到一大堆自己不想要的数据。
书中提到
如果采用拉,当扩展功能的时候,就不必要更新和修改观察者的调用,而只需改变自己来允许更多的 getter 方法来取得新增的状态。
这个观点固然不错,但是实际上,我们可以通过将数据封装成一个类来解决调用的问题。
事实上,根据 OO 设计的原则,应该 Tell, Don’t Ask ,所以使用推的方法会更好。
6. Java 内置的观察者模式
Java API 中内置了一个观察者模式,包含一个基本的 Observer
接口和一个 Observable
类。
我们可以使用 Java 的内置 API 来快速的实现观察者模式,而不需要自己再造轮子。
基本的类图如下:
其中,setChanged()
方法是用来指示状态改变的。在调用 notifyObservers()
之前,需要先调用这个方法。
同时,Java 也实现了推和拉的方式。
不带参数的 notifyObservers()
使用的是拉的方法,而带参数的使用的是推。
public notifyObservers(Objecgt arg) {
if (changed) {
for (observer : list) {
observer.update: 2016-11-25
}
}
}
public notifyObservers() {
notifyObservers(null);
}
7. Java 内置观察者模式的缺陷
-
违反面对接口编程原则
由于
Observable
是一个类,并且实现了自己的通知方法,我们的通知途径就被绑定在了Observable
的具体实现上,无法轻易改变。这也导致了对观察者的通知次序被绑定而无法改变。同时,由于 Java 禁止多重继承,所以无法对
Observable
进行复用。 -
违反多用组合,少用继承
Observable
中的setChanged()
方法是protected
的,这意味着如果不继承
Observable
就无法修改setChanged()
方法。
所以,如果应用要求弹性高,那么更好的方法应该是:
自己重新造轮子!