深入浅出设计模式【十九、观察者模式】
一、观察者模式介绍
在软件系统中,经常存在这样的场景:一个对象(目标对象)的状态发生改变,需要通知到其他多个对象(观察者对象),并让它们做出相应的响应。例如,用户点击一个按钮(目标),需要通知事件处理器(观察者);商品价格发生变化(目标),需要通知所有关注该商品的用户(观察者)。
如果让目标对象直接持有所有观察者对象的引用并调用它们的方法,会导致:
- 目标对象与观察者对象高度耦合: 目标对象需要知道所有具体的观察者。
- 难以动态添加或删除观察者: 需要在目标对象内部修改代码。
- 违反开闭原则: 新增观察者类型需要修改目标对象的代码。
观察者模式通过引入一个抽象的“通知”层,完美地解决了上述问题。它让目标对象只依赖于观察者的抽象接口,从而实现了两者之间的松耦合。
二、核心概念与意图
-
核心概念:
- 主题/目标 (Subject): 也被称为“被观察者”(Observable)。它维护一个观察者列表,并提供添加(
attach)、删除(detach)观察者的方法。它知道当自身状态改变时,需要通知哪些观察者。 - 具体主题/具体目标 (Concrete Subject): 实现主题接口。当它的状态发生改变时,会遍历其观察者列表,调用每个观察者的更新方法。
- 观察者 (Observer): 定义一个更新接口,供主题在通知时调用。
- 具体观察者 (Concrete Observer): 实现观察者接口。它通常会维护一个对主题对象的引用,用于在接收到通知时,从主题中“拉取”所需的数据。它实现更新逻辑以响应主题的状态变化。
- 主题/目标 (Subject): 也被称为“被观察者”(Observable)。它维护一个观察者列表,并提供添加(
-
意图:
- 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
- 将主题与观察者解耦,使得它们可以独立地改变和复用。
三、适用场景剖析
观察者模式在以下场景中非常有效:
- 当一个对象的改变需要同时改变其他对象,且不知道具体有多少对象有待改变时: 这有效地减少了对象间的耦合。
- 当一个抽象模型有两个方面,其中一个方面依赖于另一个方面时: 将这二者封装在独立的对象中,使它们可以各自独立地改变和复用。
- 需要建立一种触发链机制时: 一个对象的改变会触发一系列操作,但这些操作并不需要集中式代码来处理。
- 跨系统或模块的事件通知机制: 例如,微服务架构中的领域事件发布、应用内的全局事件总线。
四、UML 类图解析(Mermaid)
以下UML类图清晰地展示了观察者模式的结构和角色间的关系:
Subject(主题接口): 声明了attach,detach,notifyObservers等方法,用于管理观察者列表。ConcreteSubject(具体主题):- 维护一个观察者列表 (
-observers: List~Observer~)。 - 维护一个对自身有意义的状态 (
-state: Object)。 - 当状态改变时(通常在
setState方法中),调用notifyObservers()方法。 notifyObservers()方法会遍历observers集合,调用每个观察者的update()方法。
- 维护一个观察者列表 (
Observer(观察者接口): 声明一个update()方法。这是主题通知观察者的唯一方式。ConcreteObserver(具体观察者):- 实现
update()方法。当被主题通知时,此方法被调用。 - 通常持有(或可以访问)一个对主题对象的引用 (
-subject: Subject)。这样,在update()方法中,它就可以从主题“拉取” 它所需要的任何数据(例如调用subject.getState()),而不是被动地接收主题“推送”过来的数据。
- 实现
- 调用流程:
ConcreteObserver向ConcreteSubject注册自身 (attach)。ConcreteSubject状态变更 (setState)。ConcreteSubject调用notifyObservers()。notifyObservers()遍历所有Observer,调用每个Observer的update()方法。- 在
update()方法中,ConcreteObserver从ConcreteSubject获取新状态并执行相应操作。
五、各种实现方式及其优缺点
观察者模式主要有两种实现变体:拉模型和推模型。
1. 拉模型 (Pull Model) - 更常用,更灵活
主题在通知观察者时,只发送一个简单的通知,而不包含改变的细节。观察者在接收到通知后,主动从主题对象中“拉取”所需的数据。
- 实现: 正如上述UML所描述的,观察者的
update()方法不带参数或只带一个主题引用。观察者需要自己调用主题的getState()等方法来获取详细信息。 - 优点:
- 主题无需知道观察者的具体需求: 主题只是广播一个变化通知,观察者自己决定需要什么数据,这使得主题接口更简洁、稳定。
- 可复用性高: 不同的观察者可以从同一个主题中获取自己关心的不同部分的数据。
- 缺点:
- 效率可能稍低: 观察者可能需要多次调用主题的getter方法。
- 观察者必须持有主题的引用。
2. 推模型 (Push Model)
主题在通知观察者时,将改变的细节作为参数传递给观察者的 update() 方法。
- 实现: 观察者的
update()方法签名可能类似于update(Event event, Object data)。主题将相关的数据封装后直接“推送”给观察者。 - 优点:
- 高效: 一次调用传递所有数据。
- 观察者可能无需持有主题引用。
- 缺点:
- 主题需要知道观察者的需求: 这可能导致主题接口变得复杂,或者传递的数据是某些观察者不需要的,造成浪费。
- 降低了主题和观察者的复用性: 主题和观察者通过具体的数据类型耦合在一起。
最佳实践: 通常优先选择“拉模型”,因为它提供了更好的松耦合性。主题和观察者只依赖于抽象的接口,而不是具体的数据结构。
3. 实现方式的优缺点总结
- 优点:
- 实现了主题与观察者的松耦合: 主题只知道观察者实现了某个接口,而不知道其具体类。
- 支持广播通信: 主题一次通知,所有注册的观察者都会收到消息。
- 符合开闭原则: 可以轻松地增加新的观察者,而无需修改主题的代码。
- 缺点:
- 通知顺序的不确定性: 观察者被通知的顺序可能是任意的,不应依赖于特定的顺序。
- 意外的更新: 如果观察者的更新操作非常耗时,或者嵌套调用了主题的方法,可能会导致循环调用或性能问题。
六、最佳实践
- 考虑线程安全: 在并发环境中,主题的
attach,detach,notifyObservers方法以及状态变更方法都应该是线程安全的。通常可以使用同步机制(如synchronized)或使用线程安全的集合(如CopyOnWriteArrayList)来管理观察者列表。 - 避免在观察者中修改主题: 在观察者的
update()方法中,应尽量避免调用会改变主题状态的方法,这可能导致复杂的递归调用链,难以理解和调试。 - 定义清晰的事件对象: 在推模型中,可以定义一个通用的事件对象(如
ValueChangedEvent)来封装变化信息,而不是传递一堆松散参数,这更易于扩展。 - 与中介者模式结合: 当观察者之间的关系非常复杂时,可以考虑引入一个中介者来管理它们之间的交互,避免观察者相互直接调用。
- 处理异步通知: 对于耗时较长的观察者处理逻辑,可以考虑使用异步方式通知观察者(例如,将通知任务提交到线程池),以避免阻塞主题线程。
七、在开发中的演变和应用
观察者模式的思想是现代事件驱动架构 (EDA) 和响应式编程 (Reactive Programming) 的核心:
- 消息中间件与事件总线: 在微服务架构中,消息队列(如Kafka, RabbitMQ) 和事件总线 是系统级别的观察者模式实现。服务(主题)发布事件到消息主题(Topic),其他服务(观察者)订阅这些主题并做出反应,实现了服务间的完全解耦。
- 响应式流 (Reactive Streams): Project Reactor 和 RxJava 等响应式库将观察者模式推向极致。
Flux/Observable代表一个可观察的数据流(主题),开发者通过subscribe(相当于attach)并定义一系列操作(观察者链)来响应数据流中的元素(onNext)、错误(onError)和完成信号(onComplete)。 - 前端框架 (React, Vue, Angular): 这些框架的核心是数据驱动的视图。组件的状态(State/Data)是主题,视图(UI)是观察者。当状态发生变化时,框架会自动通知并更新视图(重新渲染)。
- JavaBean 绑定与属性变更监听: JavaFX 和许多UI框架使用属性绑定机制,其底层也是观察者模式。
八、真实开发案例(Java语言内部、知名开源框架、工具)
-
Java Swing / AWT 事件监听机制:
- 这是观察者模式最经典的案例。
JButton等UI组件是具体主题。 ActionListener是观察者接口,定义了actionPerformed(ActionEvent e)方法。- 开发者实现的
ActionListener是具体观察者。 - 通过
button.addActionListener(listener)(attach) 来注册观察者。 - 当按钮被点击时,它会通知(
notifyObservers)所有注册的监听器,调用它们的actionPerformed方法。
- 这是观察者模式最经典的案例。
-
Java.util.Observable 和 Observer (已弃用):
- Java早期在标准库中直接提供了对观察者模式的支持。
java.util.Observable类作为主题。java.util.Observer接口作为观察者。- 由于其实现不够灵活(例如,
Observable是一个类,需要继承,而不是实现接口),在Java 9中被标记为弃用。但它是一个很好的教学例子。
-
Spring Framework - ApplicationEvent 和 ApplicationListener:
- Spring的事件发布机制是观察者模式的工业级实现。
- 主题:
ApplicationEventPublisher接口(publishEvent(...)方法)。 - 观察者:
ApplicationListener接口(onApplicationEvent(...)方法)。 - 具体事件: 自定义事件需继承
ApplicationEvent。 - 工作流程: 任何Bean都可以注入
ApplicationEventPublisher来发布事件。任何Bean只要实现了ApplicationListener接口(或使用@EventListener注解),就会自动被注册为观察者,并在相应事件发布时被调用。 - 这完美实现了Spring容器内Bean之间的解耦。
-
Apache Kafka / RabbitMQ:
- 从架构层面看,消息队列是分布式的观察者模式实现。
- 生产者 (Producer) 充当主题,发布消息到特定的Topic或Exchange。
- 消费者 (Consumer) 充当观察者,订阅Topic或Queue,并在消息到达时得到通知和处理。
- 这实现了服务级别的解耦和异步通信。
-
ReactFX 和 JavaFX Property Change Listeners:
- JavaFX 的
Property对象(如SimpleStringProperty)允许添加变更监听器(InvalidationListener或ChangeListener),当属性值改变时自动通知。这是观察者模式在UI数据绑定中的直接应用。
- JavaFX 的
九、总结
| 方面 | 总结 |
|---|---|
| 模式类型 | 行为型设计模式 |
| 核心意图 | 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。 |
| 关键角色 | 主题(Subject), 具体主题(ConcreteSubject), 观察者(Observer), 具体观察者(ConcreteObserver) |
| 核心机制 | 1. 注册管理: 主题维护观察者列表,提供 attach/detach 方法。2. 状态变更通知: 主题状态变化时调用 notifyObservers。3. 更新响应: 观察者实现 update 方法以响应通知。 |
| 实现变体 | 拉模型 (灵活,松耦合),推模型 (高效,可能紧耦合)。 |
| 主要优点 | 1. 实现解耦: 主题和观察者抽象耦合,可独立变化和复用。 2. 支持广播: 一对多通信非常高效。 3. 遵守开闭原则: 易于增加新观察者。 |
| 主要缺点 | 1. 通知顺序不确定。 2. 可能引起性能问题(观察者处理慢)或循环调用。 3. 调试可能较复杂。 |
| 适用场景 | 一个对象的变化需要影响其他对象,且不知道具体有多少对象需要被影响。 |
| 最佳实践 | 优先选择拉模型;注意线程安全;避免在更新方法中修改主题;考虑异步通知。 |
| 现代应用 | 事件驱动架构 (EDA),响应式编程 (Reactive Streams),消息中间件,前端框架 (MVVM) 的基石。 |
| 真实案例 | Swing/AWT事件监听 (经典),Spring事件机制 (工业级),Kafka/RabbitMQ (分布式),JavaFX属性绑定。 |
观察者模式是解耦艺术的典范,是构建灵活、可扩展、可维护系统的关键工具。它从GUI事件处理到分布式系统通信,无处不在,是每一位架构师和开发者必须深刻理解并熟练运用的核心模式。其思想催生了现代的事件驱动和响应式编程范式,掌握了它,就掌握了构建响应式、松耦合应用的一把钥匙。
本文来自博客园,作者:NeoLshu,转载请注明原文链接:https://www.cnblogs.com/neolshu/p/19120415

浙公网安备 33010602011771号