29、重构与设计模式(二):用观察者模式重构耦合的更新逻辑
上一讲我们聊了策略模式,这次来谈谈观察者模式。说实话,这个模式是我在实际项目中用得最频繁的之一。为什么?因为业务系统里到处都是「当A发生了,B、C、D都要跟着动」的场景。你想想看,如果每次新增一个「跟着动」的模块,都要去改A的代码,那得多痛苦。
我接手过一个老项目,里面有个订单状态变更的方法,足足有三百多行。里面混着发短信、发邮件、更新库存、通知物流、推送消息……每次改一个逻辑,都得小心翼翼,生怕碰坏了别的东西。嗯,这就是典型的「耦合的更新逻辑」。
什么是观察者模式?
观察者模式,说白了就是「发布-订阅」机制。一个对象(被观察者)维护一组依赖它的对象(观察者),当自身状态发生变化时,自动通知所有观察者。
核心角色就三个:
- Subject(被观察者):持有观察者列表,提供注册、移除、通知的方法
- Observer(观察者):定义一个更新接口,接收被观察者的通知
- ConcreteObserver(具体观察者):实现更新接口,执行具体的响应逻辑
核心思想:让被观察者不关心谁在观察它,只负责「通知」这件事。观察者自己决定收到通知后做什么。
重构前的代码:一团乱麻
先看看重构前的样子。这是一个订单服务,当订单状态变为「已支付」时,需要执行一堆操作:
public class OrderService {
public void payOrder(String orderId) {
// 1. 更新订单状态
Order order = orderRepository.findById(orderId);
order.setStatus(OrderStatus.PAID);
orderRepository.save(order);
// 2. 发短信通知用户
smsService.send(order.getUserId(), "您的订单已支付成功");
// 3. 发邮件通知
emailService.send(order.getUserId(), "订单支付确认", "您的订单已支付...");
// 4. 更新库存
inventoryService.deduct(order.getItems());
// 5. 通知物流系统
logisticsService.createDelivery(orderId);
// 6. 推送消息给运营后台
pushService.notify("order_paid", order);
// 7. 记录日志
logService.record("order_paid", orderId);
}
}
这段代码的问题很明显:
- 职责不单一:一个方法干了七八件事
- 扩展困难:每次新增一个「支付后要做的操作」,都得改这个方法
- 测试麻烦:要 mock 一堆服务,才能测这个核心逻辑
- 耦合度高:OrderService 依赖了所有具体服务
我曾经见过一个项目,类似的「状态变更后处理」方法里,有人直接写了调用第三方 API 的代码。结果第三方接口挂了,整个订单流程都卡住了。这就是耦合的代价——一个无关模块的问题,能拖垮核心流程。
重构:引入观察者模式
重构的思路很简单:把「支付完成」这个事件抽象出来,让各个模块自己去订阅这个事件。OrderService 只负责发布事件,不关心谁在处理。
先定义事件和观察者接口:
// 事件对象,携带上下文信息
public class OrderPaidEvent {
private String orderId;
private String userId;
private List<OrderItem> items;
// getter / setter 省略
}
// 观察者接口
public interface OrderObserver {
void onOrderPaid(OrderPaidEvent event);
}
然后改造 OrderService:
public class OrderService {
private List<OrderObserver> observers = new ArrayList<>();
public void attach(OrderObserver observer) {
observers.add(observer);
}
public void detach(OrderObserver observer) {
observers.remove(observer);
}
public void payOrder(String orderId) {
// 核心逻辑:只做订单状态变更
Order order = orderRepository.findById(orderId);
order.setStatus(OrderStatus.PAID);
orderRepository.save(order);
// 发布事件,通知所有观察者
OrderPaidEvent event = new OrderPaidEvent();
event.setOrderId(orderId);
event.setUserId(order.getUserId());
event.setItems(order.getItems());
for (OrderObserver observer : observers) {
observer.onOrderPaid(event);
}
}
}
各个观察者独立实现:
public class SmsObserver implements OrderObserver {
@Override
public void onOrderPaid(OrderPaidEvent event) {
smsService.send(event.getUserId(), "您的订单已支付成功");
}
}
public class EmailObserver implements OrderObserver {
@Override
public void onOrderPaid(OrderPaidEvent event) {
emailService.send(event.getUserId(), "订单支付确认", "您的订单已支付...");
}
}
public class InventoryObserver implements OrderObserver {
@Override
public void onOrderPaid(OrderPaidEvent event) {
inventoryService.deduct(event.getItems());
}
}
// 其他观察者类似...
最后在应用启动时注册观察者:
@Configuration
public class OrderObserverConfig {
@Bean
public OrderService orderService(
SmsObserver smsObserver,
EmailObserver emailObserver,
InventoryObserver inventoryObserver) {
OrderService service = new OrderService();
service.attach(smsObserver);
service.attach(emailObserver);
service.attach(inventoryObserver);
return service;
}
}
我个人习惯:在 Spring 项目中,可以用 @EventListener 注解配合 ApplicationEventPublisher,框架已经帮我们实现了观察者模式。但理解底层原理很重要,这样即使不用框架,你也能手写出来。
重构后的收益
| 维度 | 重构前 | 重构后 |
|---|---|---|
| 代码行数(核心方法) | 30+ 行,混杂多种逻辑 | 15 行,只做状态变更和事件发布 |
| 扩展性 | 改核心代码,有风险 | 新增观察者类即可,无需改核心 |
| 可测试性 | 需要 mock 所有依赖 | 只需验证事件是否发布 |
| 职责清晰度 | 一个方法做所有事 | 每个观察者只做一件事 |
避坑指南
我曾经踩过一个坑:观察者里又去调用了其他被观察者的方法,结果形成了循环通知,系统直接栈溢出。后来我加了个「事件去重」机制,每个事件带唯一 ID,观察者处理前先检查是否处理过。
还有几个要点要注意:
- 观察者的执行顺序:如果某些观察者有依赖关系,可以用优先级排序,或者拆成多个事件
- 异常隔离:一个观察者抛异常,不应该影响其他观察者。建议用 try-catch 包裹每个观察者的调用
- 异步处理:如果观察者逻辑耗时(如发邮件),可以考虑异步执行,避免阻塞主流程
- 不要滥用:如果只有一两个观察者,或者观察者逻辑极其简单,硬套模式反而增加复杂度
总结一下:观察者模式解决的核心问题,就是「一对多」的依赖关系。当被观察者的状态变化时,所有依赖它的对象都能自动得到通知。说白了,就是把「主动调用」变成「被动监听」,让代码的耦合度降下来,扩展性提上去。
我在项目中用这个模式重构过不下十次,每次都能让代码清爽不少。你想想看,如果下次产品经理说「支付成功后还要加个积分赠送」,你只需要新增一个积分观察者,注册进去就行了。核心的 OrderService 一行代码都不用改——这才是我们追求的可扩展性。