观察者模式:订阅与推送

Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.——GoF
在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。
一般情况下,被依赖的对象叫作被观察者(Observable),依赖的对象叫作观察者(Observer)。不过,在实际的项目开发中两种对象的称呼是比较灵活的,有各种不同的叫法,比如:Subject-Observer、Publisher-Subscriber、Producer-Consumer、EventEmitter-EventListener、Dispatcher-Listener。不管怎么称呼,只要应用场景符合刚刚给出的定义,都可以看作观察者模式。
Observer 本来的意思是「观察者」,但实际上
Observer角色并非主动地去观察,而是被动地接受来自Subject角色的通知。因此,观察者模式也被称为 Publish-Subscribe(发布 - 订阅)模式。《图解设计模式》的作者也认为 Publish(发布) 和 Subscribe(订阅) 这个名字可能更加合适。

登场角色:
Subject(被观察对象):Subject 角色表示被观察对象。Subject角色定义了注册观察者和删除观察者的方法。此外,它还声明了「获取现在的状态」的方法。ConcreteSubject(具体的观察对象):ConcreteSubject角色表示具体的被观察对象。当自身状态发生变化后,它会通知所有已经注册的Observer角色。Observer(观察者):Observer角色负责接收来自Subject角色的状态变化的通知。为此,它声明了update方法。ConcreteObserver(具体的观察者):ConcreteObserver角色表示具体的Observer。当它的update方法被调用后,会去获取要观察的对象的最新状态。

方法说明:
getSubjectStatus是给Observer获取状态用的。notifyObserver调用Observer的update方法时,是把自己this传了过去。比如示例程序中notifyObserver是这样写的:
一种经典的实现方式(同步阻塞实现方式):
1 | public interface Subject { |
观察者模式的实现方法各式各样,函数、类的命名等会根据业务场景的不同有很大的差别,比如 register 函数还可以叫作 attach,remove 函数还可以叫作 detach 等等。
拓展思路:
- 可替换性。具体的被观察者不需要知道正在观察自己的是哪一个具体观察者。可替换性的设计思想:
- 利用抽象类和接口从具体类中抽出抽象方法
- 在将实例作为参数传递至类中,或者在类的字段中保存实例时,不使用具体类型,而是使用抽象类型和接口
- 注意
Observer的调用顺序。必须要保证 update 方法调用顺序改变时不会发生问题。 Observer角色也有可能会触发Subject角色调用update方法,所以要注意不要导致循环调用。观察者不要改变被观察者。- MVC 中的 Model 和 View 的关系与
Subject角色和Observer角色的关系相对应。Model 是指操作「不依赖于显示形式的内部模型」的部分,View 则是管理 Model「怎样显示」的部分。通常情况下,一个 Model 对应多个 View。
| 优点 | 缺点 |
|---|---|
| 开闭原则。无需修改发布者代码就能引入新的订阅者类(如果是发布者接口则可轻松引入发布者类)。 | 订阅者的通知顺序是随机的。 |
| 可以在运行时建立对象之间的联系。 |
相关的设计模式:站内文章中介者模式
- 在中介者模式中,有时会使用
Observer模式来实现Mediator角色与Colleague角色之间的通信。 - 就「发送状态变化通知」这一点而言,Mediator 模式与 Observer 模式是类似的。不过,两种模式中,通知的目的和视角不同。
- 在中介者模式中,虽然也会发送通知,不过那不过是为了对
Colleague角色进行仲裁而已。 - 而在观察者模式中,将
Subject角色的状态变化通知给Observer角色的目的则主要是为了使 Subject 角色和 Observer 角色同步。
- 在中介者模式中,虽然也会发送通知,不过那不过是为了对
观察者模式的实现方式
观察者模式的应用场景非常广泛,小到代码层面的解耦,大到架构层面的系统解耦,再或者一些产品的设计思路,都有这种模式的影子,比如,邮件订阅、RSS Feeds,本质上都是观察者模式。
根据应用场景的不同,观察者模式会对应不同的代码实现方式:有同步阻塞的实现方式,也有异步非阻塞的实现方式;有进程内的实现方式,也有跨进程的实现方式。
异步非阻塞的实现方式
上一个章节介绍的示例模板代码是同步阻塞实现方式,这也是最经典的实现方式,他的作用是代码解耦。观察者和被观察者代码在同一个线程内执行,被观察者一直阻塞,直到所有的观察者代码都执行完成之后,才执行后续的代码。
我们可以改造 notifyObservers(Message message),使其成为异步非阻塞的实现方式:
- 多线程实现方式
- 引入框架,如 Google Guava EventBus
异步非阻塞除了可以实现代码解耦外,还能提高代码的执行效率。
多线程实现方式
多线程实现方式 1:观察者使用多线程处理消息。这种方式的缺点为频繁地创建和销毁线程比较耗时,并且并发线程数无法控制,创建过多的线程会导致堆栈溢出。
1 | public class ConcreteObserverX implements Observer { |
多线程实现方式 2:使用线程池通知观察者。尽管利用了线程池解决了第一种实现方式的问题,但线程池、异步执行逻辑都耦合在了 notifyObservers() 函数中,增加了这部分业务代码的维护成本。
1 | public class ConcreteSubject implements Subject { |
如果我们的业务更加复杂,需要在同步阻塞和异步非阻塞之间灵活切换,上面的代码显然无法满足这个需求。我们可以使用 Google Guava EventBus 框架解决这个问题。使用新的框架可以隐藏实现细节,降低开发难度,实现代码的复用,解耦业务与非业务代码。
使用 Google Guava EventBus
在 站内文章EventBus 框架的简易实现 这篇文章中,我们可以模仿 Google Guava EventBus,动手实现一个简单的框架,加深对观察者模式的理解。
其他实现方式
在上面讲的场景中,不管是同步阻塞的实现方式还是异步非阻塞的实现方式,都是进程内的实现方式。对于跨进程、跨系统的观察者模式,我们可以使用 站内文章RPC 接口执行通知操作。
或者使用更优雅、更常用的方式——引入消息队列。消息队列将观察者和被观察者解耦得更加彻底,被观察者完全不感知观察者,同理,观察者也完全不感知被观察者。被观察者只管发送消息到消息队列,观察者只管从消息队列中读取消息来执行相应的逻辑。不过这样会增加维护成本。
java.util.Observer 接口
Java 类库中的 java.util.Observer 接口和 java.util.Observable 类就是一种观察者模式。
java.util.Observer 接口中定义了以下方法。
1 | public void update (Observable obj, Object arg) |
而 update 方法的参数则接收到了如下内容:
Observable类的实例是被观察的Subject角色。Object类的实例是附加信息
我们一般不直接用这些接口实现 Observer 模式,因为 java.util.Observer 接口和 java.util.observable 类并不好用。理由很简单,传递给 java.util.Observer 接口的 Subject 角色必须是 java.util.Observable 类型(或者它的子类型)的。但 Java 只能单一继承,也就说如果 Subject 角色已经是某个类的子类了,那么它将无法继承 java.util.observable 类。
Coad 书讲解了这个问题的解决办法。在该书介绍的观察者模式中,Subject 角色和 Observer 接口都被定义为 Java 的接口,这种观察者模式更容易使用。
本文参考
- 《图解设计模式》17 章
- 本科生课程笔记《程序设计中级实践&设计模式》 - TJU 🍐⚱️
- 极客时间专栏 - 设计模式之美 - 王争
- 观察者设计模式





