观察者模式 (Observer Pattern)
概述
观察者模式定义了对象间的一对多依赖关系。当一个对象(称为Subject)的状态改变时,它的所有依赖者(称为Observers)都会自动收到通知并更新。
核心原则:
- 🎯 一对多订阅:一个发布者,多个订阅者
- 📢 自动通知:状态改变时主动通知所有观察者
- 🔌 松耦合:发布者和订阅者相互独立
- ⚙️ 动态关系:运行时动态添加/移除观察者
何时使用
完美适用场景
| 场景 | 说明 | 优先级 |
|---|---|---|
| 事件处理系统 | GUI 按钮点击、文本变更等事件 | ⭐⭐⭐ |
| MVC 架构 | Model 变更→View 自动更新 | ⭐⭐⭐ |
| 响应式编程 | 数据绑定、实时流处理 | ⭐⭐⭐ |
| 消息系统 | 消息队列、事件总线 | ⭐⭐⭐ |
| 属性变更通知 | 对象属性变化通知 | ⭐⭐ |
| 模型-视图绑定 | Web 框架中的数据绑定 | ⭐⭐⭐ |
触发信号(何时考虑使用)
✅ 以下信号表明应该使用观察者:
- "状态改变时需要通知多个对象"
- "对象间存在一对多的依赖关系"
- "需要松耦合的事件通知机制"
- "需要动态添加/移除监听器"
- "事件驱动的应用架构"
❌ 以下情况不应该使用:
- "只有单向的命令式调用"
- "通知链太长导致性能问题"
- "观察者间存在复杂的依赖"
观察者模式的优缺点
优点 ✅
高度解耦
- 发布者和订阅者完全解耦
- 可以独立修改
- 易于单元测试
动态关系
- 运行时动态添加/移除观察者
- 支持一对多的灵活关系
- 无需修改发布者代码
自动通知
- 状态改变时自动通知
- 不需要主动查询
- 反应式编程的基础
支持广播通信
- 一个消息发送给多个接收者
- 简化事件处理逻辑
- 易于实现事件总线
缺点 ❌
无法控制通知顺序
- 多个观察者的通知顺序不确定
- 可能导致竞态条件
- 依赖关系复杂时难以调试
内存泄漏风险
- ❌ 观察者未正确反注册会持有引用
- ❌ 导致内存泄漏
- ❌ 特别是在 Web/移动应用中
性能问题
- 观察者过多时性能下降
- 同步通知会阻塞发布者
- 异步通知增加复杂性
调试困难
- 控制流不明显
- 难以追踪执行路径
- 出现问题难以定位
观察者模式的 3 种实现方式
1️⃣ 推模式(Push)- 发布者主动推送数据 ⭐⭐⭐
发布者将状态数据直接传给观察者。
/**
* 推模式 - 发布者将状态数据推送给观察者
* 优点: 观察者获取完整信息,无需查询
* 缺点: 观察者必须接受所有发送的数据
*/
// 事件数据对象
public class StockEvent {
private String stockCode;
private double price;
private double change;
public StockEvent(String code, double price, double change) {
this.stockCode = code;
this.price = price;
this.change = change;
}
// Getters...
}
// 观察者接口
public interface StockObserver {
void onStockUpdate(StockEvent event);
}
// 具体观察者
public class InvestorObserver implements StockObserver {
private String name;
public InvestorObserver(String name) {
this.name = name;
}
@Override
public void onStockUpdate(StockEvent event) {
System.out.printf("%s: 股票 %s 价格 %.2f, 涨幅 %.2f%%\n",
name, event.getStockCode(), event.getPrice(), event.getChange());
}
}
// 发布者
public class StockMarket {
private List<StockObserver> observers = new ArrayList<>();
public void attach(StockObserver observer) {
observers.add(observer);
}
public void detach(StockObserver observer) {
observers.remove(observer);
}
public void notifyObservers(String code, double price, double change) {
StockEvent event = new StockEvent(code, price, change);
for (StockObserver observer : observers) {
observer.onStockUpdate(event);
}
}
}
// 使用
StockMarket market = new StockMarket();
StockObserver investor1 = new InvestorObserver("张三");
StockObserver investor2 = new InvestorObserver("李四");
market.attach(investor1);
market.attach(investor2);
market.notifyObservers("AAPL", 150.5, 2.5); // 通知所有观察者
特点:
- ✅ 轻松获得完整数据
- ✅ 实现简单
- ❌ 发送不关心的数据浪费资源
- 🎯 适合: 数据更新需要完整信息的场景
2️⃣ 拉模式(Pull)- 观察者主动查询状态 ⭐⭐
发布者只通知观察者有更新,具体数据由观察者主动查询。
/**
* 拉模式 - 发布者仅通知更新,观察者主动查询
* 优点: 观察者只获取需要的信息
* 缺点: 观察者需要主动查询,增加复杂度
*/
public interface Subject {
void attach(Observer observer);
void detach(Observer observer);
void notifyObservers();
// 观察者查询的方法
String getState();
}
public interface Observer {
void update(Subject subject); // 仅通知,不传数据
}
public class ConcreteSubject implements Subject {
private String state;
private List<Observer> observers = new ArrayList<>();
@Override
public void attach(Observer observer) {
if (!observers.contains(observer)) {
observers.add(observer);
}
}
@Override
public void detach(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(this); // 只通知,传递 Subject 引用
}
}
@Override
public String getState() {
return state;
}
public void setState(String newState) {
this.state = newState;
notifyObservers();
}
}
public class ConcreteObserver implements Observer {
private String name;
public ConcreteObserver(String name) {
this.name = name;
}
@Override
public void update(Subject subject) {
// 主动查询状态
System.out.println(name + "观察到状态变更: " + subject.getState());
}
}
特点:
- ✅ 观察者按需获取信息
- ✅ 减少数据传输
- ❌ 需要多次查询,性能一般
- 🎯 适合: 少量观察者关心少量数据的场景
3️⃣ 事件总线(Event Bus)- 中介者模式结合 ⭐⭐⭐
使用中央事件总线,解耦发布者和订阅者完全。
/**
* 事件总线模式 - 最灵活且最常用
* 优点: 完全解耦发布者和订阅者,易于扩展
* 缺点: 多了一个中介,稍微增加复杂度
*/
// 事件接口
public interface Event {}
// 具体事件
public class UserLoginEvent implements Event {
public final String userId;
public final String timestamp;
public UserLoginEvent(String userId, String timestamp) {
this.userId = userId;
this.timestamp = timestamp;
}
}
public class UserLogoutEvent implements Event {
public final String userId;
public UserLogoutEvent(String userId) {
this.userId = userId;
}
}
// 事件监听器
public interface EventListener {
void onEvent(Event event);
}
// 事件总线
public class EventBus {
private final Map<Class<?>, List<EventListener>> listeners = new ConcurrentHashMap<>();
@SuppressWarnings("unchecked")
public <T extends Event> void subscribe(Class<T> eventType, EventListener listener) {
listeners.computeIfAbsent(eventType, k -> new CopyOnWriteArrayList<>())
.add(listener);
}
public <T extends Event> void unsubscribe(Class<T> eventType, EventListener listener) {
List<EventListener> eventListeners = listeners.get(eventType);
if (eventListeners != null) {
eventListeners.remove(listener);
}
}
public void publish(Event event) {
List<EventListener> eventListeners = listeners.get(event.getClass());
if (eventListeners != null) {
for (EventListener listener : eventListeners) {
listener.onEvent(event);
}
}
}
}
// 使用
EventBus bus = new EventBus();
bus.subscribe(UserLoginEvent.class, event -> {
System.out.println("用户 " + event.userId + " 登录");
});
bus.subscribe(UserLogoutEvent.class, event -> {
System.out.println("用户 " + event.userId + " 登出");
});
// 发布事件
bus.publish(new UserLoginEvent("user123", "2024-03-29"));
bus.publish(new UserLogoutEvent("user123"));
特点:
- ✅ 完全解耦
- ✅ 高度灵活可扩展
- ✅ 现代化设计
- ✅ 支持多种事件类型
- 🎯 最推荐的实现方式
常见问题与解决方案
问题 1: 内存泄漏 - 观察者未反注册
现象: 应用内存不断增长,最终 OutOfMemory
原因: 观察者被添加后未被移除,发布者持有对观察者的强引用
解决方案:
// ❌ 错误做法 - 强引用导致内存泄漏
public class View {
private EventBus bus = new EventBus();
public View() {
bus.subscribe(UserEvent.class, (event) -> {
update();
});
// 没有注销监听器,View 销毁时内存被占用
}
}
// ✅ 正确做法1 - 显式注销
public class View implements Closeable {
private EventBus bus;
private EventListener listener;
public View(EventBus bus) {
this.bus = bus;
this.listener = (event) -> update();
bus.subscribe(UserEvent.class, listener);
}
@Override
public void close() {
bus.unsubscribe(UserEvent.class, listener);
}
}
// ✅ 正确做法2 - 弱引用观察者
public class WeakObserverHolder<T extends Event> {
private WeakReference<EventListener> listenerRef;
private Class<T> eventType;
public WeakObserverHolder(EventListener listener, Class<T> eventType) {
this.listenerRef = new WeakReference<>(listener);
this.eventType = eventType;
}
public void onEvent(Event event) {
EventListener listener = listenerRef.get();
if (listener != null) {
listener.onEvent(event);
}
}
}
// ✅ 正确做法3 - 使用 try-with-resources
try (EventSubscription sub = bus.subscribe(UserEvent.class, listener)) {
// 事件处理
} // 自动注销
最佳实践:
- 使用 try-with-resources 或显式 close() 方法
- 在 Activity/Fragment/Component 销毁时注销
- 考虑使用弱引用观察者
问题 2: 通知顺序不确定
现象: 多个观察者的通知顺序随意,导致依赖的业务逻辑出错
解决方案:
// ❌ 错误做法 - 顺序不确定
eventBus.publish(new StateChangeEvent()); // 多个监听器顺序不确定
// ✅ 正确做法1 - 指定优先级
public class PriorityEventBus {
private static class ListenerWrapper {
int priority;
EventListener listener;
}
public void subscribe(Class<?> eventType, EventListener listener, int priority) {
// 按优先级排序
}
}
// ✅ 正确做法2 - 顺序观察链
public class OrderedEventBus {
public void publish(Event event, EventListener... listeners) {
for (EventListener listener : listeners) {
listener.onEvent(event); // 按指定顺序执行
}
}
}
// ✅ 正确做法3 - 异步处理避免时序问题
public class AsyncEventBus {
private ExecutorService executor = Executors.newFixedThreadPool(4);
public void publishAsync(Event event) {
executor.submit(() -> {
// 异步处理,避免阻塞
});
}
}
问题 3: 性能问题 - 观察者过多
现象: 观察者增加后,通知性能明显下降
原因: 需要遍历所有观察者,如果观察者处理缓慢会阻塞发布者
解决方案:
// ❌ 同步通知可能阻塞
for (Observer observer : observers) {
observer.update(event); // 如果某个观察者卡住,会阻塞所有后续观察者
}
// ✅ 使用线程池异步通知
ExecutorService executor = Executors.newFixedThreadPool(4);
for (Observer observer : observers) {
executor.submit(() -> observer.update(event));
}
// ✅ 分组通知 - 将观察者分组,不同组并行处理
Map<String, List<Observer>> groupedObservers = observers.stream()
.collect(Collectors.groupingBy(Observer::getGroup));
groupedObservers.forEach((group, listeners) -> {
executor.submit(() -> {
for (Observer observer : listeners) {
observer.update(event);
}
});
});
// ✅ 性能计数 - 监控观察者执行时间
long startTime = System.nanoTime();
observer.update(event);
long duration = System.nanoTime() - startTime;
if (duration > 1_000_000_000) { // 超过1秒
logger.warn("观察者 {} 执行耗时 {}ms", observer.getClass(), duration / 1_000_000);
}
问题 4: 异常处理
现象: 一个观察者抛出异常,其他观察者不被通知
解决方案:
// ❌ 错误做法 - 异常会中断后续通知
for (Observer observer : observers) {
observer.update(event); // 如果抛出异常,后续优先级下降的观察者无法执行
}
// ✅ 正确做法 - 为每个观察者独立处理异常
for (Observer observer : observers) {
try {
observer.update(event);
} catch (Exception e) {
logger.error("观察者 {} 处理事件时出错", observer.getClass().getName(), e);
// 继续处理其他观察者
}
}
// ✅ 最佳实践 - 使用异步处理隔离异常
ExecutorService executor = Executors.newCachedThreadPool();
for (Observer observer : observers) {
executor.submit(() -> {
try {
observer.update(event);
} catch (Exception e) {
logger.error("观察者异常", e);
}
});
}
最佳实践 TOP 5
1. ✅ 明确发布者和订阅者的职责
// 发布者 - 仅发布事件,不关心订阅者
public class UserService {
private EventBus eventBus;
public void registerUser(String username) {
// ... 注册逻辑 ...
eventBus.publish(new UserRegisteredEvent(username));
}
}
// 订阅者 - 监听相关事件
eventBus.subscribe(UserRegisteredEvent.class, event -> {
// 发送邮件等后续操作
sendWelcomeEmail(event.username);
});
2. ✅ 使用强类型事件,避免字符串键
// ❌ 避免这样做 - 容易出错
eventBus.publish("user.registered", userData);
// ✅ 使用强类型
eventBus.publish(new UserRegisteredEvent(userId));
3. ✅ 使用异步处理防止阻塞
// 异步发布
public void publishAsync(Event event) {
executor.submit(() -> {
try {
publish(event);
} catch (Exception e) {
logger.error("事件发布异常", e);
}
});
}
4. ✅ 提供反注册机制,防止内存泄漏
// 使用 AutoCloseable 简化反注册
try (Subscription sub = eventBus.subscribe(UserEvent.class, listener)) {
// 在这个作用域内监听事件
} // 自动取消订阅
5. ✅ 监控和日志
public class LoggingEventBus extends EventBus {
@Override
public void publish(Event event) {
logger.info("发布事件: {}", event.getClass().getSimpleName());
long startTime = System.nanoTime();
super.publish(event);
long duration = System.nanoTime() - startTime;
logger.info("事件处理耗时: {}ms", duration / 1_000_000);
}
}
与其他模式的关系
| 模式 | 关系 | 场景 |
|---|---|---|
| Mediator | 中介者是观察者的特例(中央控制) | 复杂对象间的通信 |
| Command | 可结合使用(命令作为事件) | 命令队列+事件通知 |
| Strategy | 观察者可使用不同策略处理 | 灵活事件处理 |
| Model-View | MVC 中核心模式 | 模型变更自动更新视图 |
性能对比
| 实现方式 | 通知延迟 | 内存占用 | 易用性 | 推荐场景 |
|---|---|---|---|---|
| 推模式 | 最短 | 中等 | ⭐⭐⭐ | 简单事件 |
| 拉模式 | 较长 | 最低 | ⭐⭐ | 性能关键 |
| 事件总线 | 中等 | 中等 | ⭐⭐⭐ | 现代应用 |
何时应避免使用
❌ 以下情况不应使用观察者:
同步命令式调用足够
// ✅ 简单场景,直接调用更清晰 userService.register(user); emailService.send(user.getEmail());观察者链过长或循环依赖
// ❌ 避免:A→B→C→...→A 的循环需要精确控制执行顺序
// ❌ 观察者顺序不确定,不适合
相关文件:
- 应用规划: forms.md
- 完整参考: reference.md
## 典型应用场景
### 1. GUI 事件系统
```java
button.addEventListener("click", event -> {
// 处理点击
});
2. MVC 框架
// 模型变化通知视图
dataModel.addListener(view);
3. 异步任务通知
task.addProgressListener(progressBar::update);
最佳实践
- ✅ 使用弱引用避免内存泄漏
- ✅ 提供 add/remove 观察者方法
- ✅ 异常观察者不影响其他观察者
- ✅ 文档说明通知顺序和内容
Java 内置观察者
Observable/Observer
public class Model extends Observable {
public void setState(String state) {
setChanged();
notifyObservers(state);
}
}
PropertyChangeListener
bean.addPropertyChangeListener("name", evt -> {
System.out.println("Name changed to " + evt.getNewValue());
});
何时避免使用
- 只有一个发布者和订阅者
- 通知关系简单
- 不需要解耦