19、观察者模式(Observer):事件驱动架构
观察者模式,说白了就是「我变了,你得知道」。
定义上写的是:对象间一对多的依赖关系,一个对象状态改变,所有依赖它的对象都得到通知并自动更新。嗯,这个定义很标准,但我觉得不够形象。我更喜欢这么理解——你关注了一个公众号,号主一发文章,你就收到推送。你就是观察者,号主就是被观察者。
模式结构:谁在观察谁?
观察者模式的核心角色就四个:
- Subject(主题):被观察的对象,它维护一个观察者列表
- Observer(观察者):接收通知的接口
- ConcreteSubject(具体主题):真正干活的对象,状态变了就通知
- ConcreteObserver(具体观察者):收到通知后执行自己的逻辑
我画了一张图,帮你理清它们之间的关系:
业务场景一:Spring 中的 ApplicationListener
Spring 的事件机制,就是观察者模式的经典实现。我个人特别喜欢这个设计,因为它让业务代码解耦得干干净净。
举个例子,用户注册成功后,需要发邮件、送积分、记录日志。如果把这些逻辑全写在注册方法里,那代码就成了一锅粥。用事件驱动的方式,注册方法只管发布一个「UserRegisteredEvent」,剩下的让监听器去处理。
// 1. 定义事件
public class UserRegisteredEvent extends ApplicationEvent {
private String userId;
private String email;
public UserRegisteredEvent(Object source, String userId, String email) {
super(source);
this.userId = userId;
this.email = email;
}
// getters...
}
// 2. 发布事件
@Service
public class UserService {
@Autowired
private ApplicationEventPublisher publisher;
public void register(String username, String email) {
// 注册逻辑...
String userId = saveUser(username, email);
// 发布事件
publisher.publishEvent(new UserRegisteredEvent(this, userId, email));
}
}
// 3. 监听事件
@Component
public class EmailNotificationListener
implements ApplicationListener<UserRegisteredEvent> {
@Override
public void onApplicationEvent(UserRegisteredEvent event) {
sendWelcomeEmail(event.getEmail());
}
}
// 4. 或者用 @EventListener 注解(更简洁)
@Component
public class BonusService {
@EventListener
public void handleUserRegistered(UserRegisteredEvent event) {
grantSignUpBonus(event.getUserId());
}
}
核心要点:Spring 的事件机制默认是同步的。也就是说,监听器执行完,发布者的代码才继续往下走。如果你需要异步处理,记得加 @Async 注解。
我的经验:我曾经在一个项目中,把所有业务逻辑都塞进监听器里,结果一个事件触发了十几个监听器,接口响应时间直接飙到 5 秒。后来我把耗时的操作(比如发邮件、推送通知)改成异步执行,响应时间降到了 200 毫秒。记住:监听器里不要做重活。
业务场景二:消息队列的发布-订阅模型
Kafka、RabbitMQ 这些消息中间件,本质上就是观察者模式在分布式环境下的延伸。生产者(被观察者)发布消息,消费者(观察者)订阅并处理消息。
为什么需要消息队列?说白了,就是解决「同步调用」的痛点。你想想看,如果 A 服务调用 B 服务,B 挂了,A 也跟着挂了。用消息队列,A 只管往队列里丢消息,B 自己来取,B 挂了也不影响 A。
// Kafka 生产者示例
public class OrderService {
private KafkaTemplate<String, Order> kafkaTemplate;
public void createOrder(Order order) {
// 保存订单...
// 发送消息到 "order.created" 主题
kafkaTemplate.send("order.created", order.getOrderId(), order);
}
}
// Kafka 消费者示例
@Component
public class InventoryService {
@KafkaListener(topics = "order.created", groupId = "inventory-group")
public void handleOrderCreated(Order order) {
// 扣减库存
deductInventory(order.getProductId(), order.getQuantity());
}
}
@Component
public class NotificationService {
@KafkaListener(topics = "order.created", groupId = "notification-group")
public void handleOrderCreated(Order order) {
// 发送通知
sendOrderConfirmation(order.getUserId(), order.getOrderId());
}
}
避坑指南:我曾经遇到过一个线上事故——消费者处理消息时抛了异常,结果消息一直重试,把数据库连接池打满了。后来我养成了一个习惯:在消费者入口处加 try-catch,把异常消息记录到死信队列,而不是无限重试。
业务场景三:前端框架的双向数据绑定
Vue.js 的双向绑定,底层就用到了观察者模式。Vue 通过 Object.defineProperty() 把数据变成「响应式」的,数据一变,视图自动更新。
我简单模拟一下这个机制:
// 一个极简的观察者模式实现
class Dep {
constructor() {
this.subscribers = new Set();
}
depend() {
if (activeWatcher) {
this.subscribers.add(activeWatcher);
}
}
notify() {
this.subscribers.forEach(watcher => watcher.update());
}
}
let activeWatcher = null;
class Watcher {
constructor(updateFn) {
this.update = updateFn;
}
// 收集依赖时,把自己设为 activeWatcher
collect() {
activeWatcher = this;
this.update(); // 触发 getter,完成依赖收集
activeWatcher = null;
}
}
// 使用示例
const data = { name: '张三' };
const dep = new Dep();
// 把 data.name 变成响应式
let value = data.name;
Object.defineProperty(data, 'name', {
get() {
dep.depend(); // 收集依赖
return value;
},
set(newValue) {
value = newValue;
dep.notify(); // 通知更新
}
});
// 创建一个 watcher,当 data.name 变化时更新视图
const watcher = new Watcher(() => {
console.log('视图更新:', data.name);
});
watcher.collect(); // 收集依赖
data.name = '李四'; // 输出:视图更新:李四
设计精髓:观察者模式的核心就是「依赖收集」和「变更通知」。Vue 在编译模板时,会解析出哪些数据被模板使用了,然后建立 watcher 和数据的对应关系。数据一变,watcher 就触发视图更新。
观察者模式的优缺点
| 优点 | 缺点 |
|---|---|
| 解耦:主题和观察者之间是松耦合的 | 通知顺序不可控:多个观察者时,执行顺序不确定 |
| 支持广播通信:一对多通知 | 性能问题:观察者太多时,通知开销大 |
| 符合开闭原则:新增观察者无需修改主题 | 循环依赖:观察者更新又触发主题变化,可能死循环 |
我的建议:观察者模式虽然好用,但别滥用。如果一个事件只有一两个观察者,直接用方法调用更简单。观察者模式的价值在于「一对多」和「解耦」,用对了地方才是好模式。
好了,观察者模式就聊到这儿。记住它的核心思想:状态变化时,自动通知所有关注者。Spring 的事件、Kafka 的消息、Vue 的数据绑定,都是这个思路。下次你在项目中遇到「某个动作发生后,需要触发一系列后续操作」的场景,不妨想想观察者模式。