观察者模式 (Observer) —— 这里的消息最灵通
前言
在深入探讨观察者模式之前,想向大家推荐一个非常棒的开源项目:design-patterns-23。这个项目用最现代的技术栈重写了 23 种设计模式,非常适合实战学习,还配套了在线交互演示站,可以边读文章边动手玩。本文的实战案例灵感也来源于此。
1. 观察者模式是什么?
观察者模式(Observer Pattern),又叫发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式。它定义了对象之间的一对多依赖,当一个对象(Subject/Observable)改变状态时,它的所有依赖者(Observer)都会收到通知并自动更新。
简单来说,就是“别打电话给我,有事我会通知你”。
1.1 核心概念
- Subject(主题/被观察者):维护一组观察者,提供添加、删除和通知观察者的方法。
- Observer(观察者):定义一个更新接口,当收到主题的通知时被调用。
- ConcreteSubject:具体的主题,状态发生改变时,向所有登记过的观察者发出通知。
- ConcreteObserver:具体的观察者,实现抽象观察者定义的更新接口。
1.2 为什么我们需要它?
场景: 假设你正在开发一个气象站应用。气象数据(温度、湿度、气压)由 WeatherData 对象获取。当数据更新时,你需要更新三个布告板:
- 当前状况布告板
- 气象统计布告板
- 天气预报布告板
痛点: 如果不使用观察者模式,你可能会在 WeatherData 类里直接调用这三个布告板的 update() 方法。
public void measurementsChanged() {
currentConditionsDisplay.update(temp, humidity, pressure);
statisticsDisplay.update(temp, humidity, pressure);
forecastDisplay.update(temp, humidity, pressure);
}这样做的问题是:
- 高耦合:
WeatherData必须知道具体的布告板类。 - 违反开闭原则:如果以后要增加一个“穿衣指南布告板”,你得修改
WeatherData的代码。
解决方案: 让 WeatherData 成为 Subject,布告板成为 Observer。WeatherData 只管 notifyObservers(),不用管具体是谁在听。
2. 观察者模式的两种模型:推 (Push) vs 拉 (Pull)
在通知观察者时,有两种传递数据的方式:
2.1 推模型 (Push Model)
Subject 主动将数据“推”给 Observer。
- 特点:
notifyObservers(Object data)带参数。 - 优点:Observer 不需要去 Subject 里取数据,直接拿到了。
- 缺点:Subject 必须要知道 Observer 需要什么数据。如果 Observer A 需要温度,Observer B 需要湿度,Subject 只能把所有数据都推过去,或者推一个包含所有数据的对象。
2.2 拉模型 (Pull Model) —— 更灵活
Subject 只告诉 Observer "我更新了",Observer 收到通知后,自己去 Subject 里“拉”取自己需要的数据。
- 特点:
notifyObservers()不带参数,或者只带this。Observer 的update(Subject subject)方法接收 Subject 引用。 - 优点:按需索取,Subject 不需要关心 Observer 到底要啥。
- 缺点:Observer 必须和 Subject 深度绑定(因为要调用 Subject 的 getter)。
3. Java 实战:电商订单通知系统
我们模拟一个电商场景:订单支付成功后,需要触发一系列动作:
- 短信服务:给用户发短信。
- 库存服务:扣减库存。
- 物流服务:通知仓库发货。
如果用观察者模式(使用拉模型),代码如下:
import java.util.ArrayList;
import java.util.List;
// 1. 观察者接口
interface OrderObserver {
void update(OrderSubject subject);
}
// 2. 主题接口
abstract class OrderSubject {
protected List<OrderObserver> observers = new ArrayList<>();
public void attach(OrderObserver observer) {
observers.add(observer);
}
public void detach(OrderObserver observer) {
observers.remove(observer);
}
public void notifyObservers() {
for (OrderObserver observer : observers) {
observer.update(this);
}
}
}
// 3. 具体主题:订单系统
class OrderSystem extends OrderSubject {
private String orderId;
private double amount;
private String status;
public void paySuccess(String orderId, double amount) {
this.orderId = orderId;
this.amount = amount;
this.status = "PAID";
System.out.println(">>> 订单 " + orderId + " 支付成功,金额:" + amount);
// 通知所有观察者
notifyObservers();
}
public String getOrderId() { return orderId; }
public double getAmount() { return amount; }
}
// 4. 具体观察者:短信服务
class SmsService implements OrderObserver {
@Override
public void update(OrderSubject subject) {
if (subject instanceof OrderSystem) {
OrderSystem order = (OrderSystem) subject;
System.out.println("[短信服务] 发送短信给用户:订单 " + order.getOrderId() + " 支付成功!");
}
}
}
// 5. 具体观察者:库存服务
class InventoryService implements OrderObserver {
@Override
public void update(OrderSubject subject) {
if (subject instanceof OrderSystem) {
System.out.println("[库存服务] 扣减库存...");
}
}
}
// 6. 客户端
public class Client {
public static void main(String[] args) {
OrderSystem orderSystem = new OrderSystem();
// 注册观察者
orderSystem.attach(new SmsService());
orderSystem.attach(new InventoryService());
// 触发事件
orderSystem.paySuccess("ORD-20231111", 99.00);
}
}输出:
>>> 订单 ORD-20231111 支付成功,金额:99.0
[短信服务] 发送短信给用户:订单 ORD-20231111 支付成功!
[库存服务] 扣减库存...4. 进阶:Spring 事件驱动模型 (ApplicationEvent)
在现代 Java 开发(尤其是 Spring Boot)中,我们很少手写 Observer 接口,而是直接使用 Spring 提供的事件发布/订阅机制。这是一种解耦更彻底的观察者模式。
4.1 核心组件
ApplicationEvent:事件对象(继承自 JDKEventObject)。ApplicationEventPublisher:事件发布者(通常直接注入ApplicationContext)。@EventListener:事件监听者(注解在方法上)。
4.2 代码示例
Step 1: 定义事件
import org.springframework.context.ApplicationEvent;
public class OrderPaidEvent extends ApplicationEvent {
private String orderId;
public OrderPaidEvent(Object source, String orderId) {
super(source);
this.orderId = orderId;
}
public String getOrderId() {
return orderId;
}
}Step 2: 发布事件
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void pay(String orderId) {
System.out.println(">>> 支付完成,写入数据库...");
// 发布事件,不用管谁在听
eventPublisher.publishEvent(new OrderPaidEvent(this, orderId));
}
}Step 3: 监听事件
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Component
public class OrderListener {
@EventListener
public void sendSms(OrderPaidEvent event) {
System.out.println("[短信] 发送短信通知:" + event.getOrderId());
}
@EventListener
@Async // 还可以异步执行!
public void notifyLogistics(OrderPaidEvent event) {
System.out.println("[物流] 通知仓库发货:" + event.getOrderId());
}
}优点:
- 完全解耦:发布者和监听者互不认识,只依赖 Event 类。
- 支持异步:配合
@Async,轻松实现异步处理,不会阻塞主线程(比如发短信慢,不能卡住支付接口)。 - 支持事务:使用
@TransactionalEventListener可以控制在事务提交后才发送通知。
5. 源码中的观察者模式
5.1 JDK java.util.Observer (已过时)
JDK 1.0 就有了 Observer 接口和 Observable 类。 为什么过时?
Observable是一个类而不是接口。Java 是单继承的,如果你的类已经继承了别的类(比如HttpServlet),就没法继承Observable了,这极大地限制了复用性。setChanged()方法是protected的,意味着你必须通过继承Observable才能调用它,无法通过组合的方式使用。
5.2 Java Beans PropertyChangeListener
Java GUI (Swing/AWT) 和 Java Beans 中大量使用了 PropertyChangeSupport,用于监听属性的变化。
5.3 Guava EventBus
Google Guava 库提供的 EventBus 是一个轻量级的进程内事件总线,使用 @Subscribe 注解标记监听方法,比 JDK 原生的更好用。
6. 观察者模式 vs 发布-订阅模式
虽然经常混用,但严格来说它们有区别:
| 特性 | 观察者模式 (Observer) | 发布-订阅模式 (Pub-Sub) |
|---|---|---|
| 耦合度 | 较松(Subject 知道 Observer 列表) | 极松(Publisher 不知道 Subscriber 存在) |
| 通信方式 | 直接调用(Subject -> Observer) | 通过中间人(Publisher -> Broker -> Subscriber) |
| 同步/异步 | 通常是同步的 | 通常是异步的(通过消息队列) |
| 应用场景 | 单体应用内部解耦 | 分布式系统、微服务解耦 |
总结:
- Observer:你是我的粉丝,我发动态直接推给你。(B站关注)
- Pub-Sub:我是广播电台,我把信号发到塔台(Broker),谁调到这个频道谁就听,我不知道谁在听。(RabbitMQ/Kafka)
7. 优缺点与避坑指南
7.1 优点
- 解除耦合:Subject 和 Observer 之间建立了一套触发机制,Subject 只依赖抽象的 Observer 接口。
- 支持广播通信:Subject 会向所有注册的 Observer 发送通知。
7.2 缺点
- 循环依赖:如果 A 观察 B,B 又观察 A,会触发死循环调用,导致系统崩溃。
- 性能问题:如果 Observer 非常多,或者 Observer 的 update 方法执行很慢,会阻塞 Subject 的运行(除非使用异步)。
- 不知“怎么变”:Observer 只知道 Subject 变了,但可能很难推断具体是哪里变了(尤其是在推模型下数据不全时)。
7.3 避坑指南:失效的监听器 (Lapsed Listener Problem)
问题: Subject 持有 Observer 的引用。如果 Observer 不再使用了,但忘记从 Subject 中 detach(取消注册),那么 Subject 依然强引用着 Observer,导致 Observer 无法被垃圾回收 (GC)。这就是内存泄漏。
解决:
- 手动注销:一定要在合适的时机调用
detach。 - 弱引用:Subject 内部使用
WeakReference保存 Observer(Java 的WeakHashMap),这样当 Observer 没有其他强引用时,GC 会自动回收它。
8. 总结
观察者模式是解耦神技。
- 在单体应用内,首选 Spring Events。
- 在分布式系统中,请升级为 消息队列 (MQ) 实现发布-订阅。
9. 实验实操
在 design-patterns-web 项目中,我们通过一个 YouTube 频道订阅的场景来演示观察者模式。
- 发布视频:点击 Channel 面板中的 "Post Video" 按钮,这就相当于 Subject 触发了一个事件。
- 实时通知:下方的 Subscriber A 和 Subscriber B 列表会同时收到 "New video uploaded" 的通知。
- 动态订阅:你可以随时点击 "Subscribe" 或 "Unsubscribe" 按钮,观察通知列表的变化,体验观察者列表的动态维护。 截图建议:点击 "Post Video" 后,截取包含 Channel 面板和下方多个 Subscriber 通知列表的完整界面,展示“一处变更,多处响应”的联动效果。