23、重构与设计模式(下):观察者模式、模板方法模式在重构中的应用
上一讲我们聊了策略模式和工厂模式在重构中的落地。今天继续往下走,把观察者模式和模板方法模式讲透。这两个模式在实战中出现的频率非常高,尤其是当你需要处理“通知”和“流程固化”这类场景时,它们几乎是首选。
我个人习惯把设计模式看作“重构的终点”,而不是“设计的起点”。什么意思呢?就是代码先跑起来,等闻到坏味道了,再用模式去收拾它。观察者和模板方法,就是两把非常好用的“收拾工具”。
观察者模式:从硬编码通知到事件驱动
先说说观察者模式。它的核心思想很简单:当一个对象状态发生变化时,所有依赖它的对象都能自动收到通知。说白了,就是“发布-订阅”机制。
我在项目中遇到过这样一个场景:用户下单成功后,需要同时做三件事——发短信、发邮件、更新库存。最初的代码长什么样呢?
public class OrderService {
public void placeOrder(Order order) {
// 保存订单
save(order);
// 硬编码通知
sendSms(order.getUserId(), "下单成功");
sendEmail(order.getUserId(), "下单成功");
updateStock(order.getProductId(), -1);
}
}
这段代码的问题很明显:每次新增一个通知动作,都要改 OrderService 类。比如后来要加个微信推送,就得在 placeOrder 方法里再加一行。这违反了开闭原则,也容易引发回归问题。
怎么重构?引入观察者模式。
// 观察者接口
public interface OrderObserver {
void onOrderPlaced(Order order);
}
// 主题(被观察者)
public class OrderSubject {
private List<OrderObserver> observers = new ArrayList<>();
public void attach(OrderObserver observer) {
observers.add(observer);
}
public void detach(OrderObserver observer) {
observers.remove(observer);
}
public void notifyObservers(Order order) {
for (OrderObserver observer : observers) {
observer.onOrderPlaced(order);
}
}
}
// 具体观察者
public class SmsObserver implements OrderObserver {
@Override
public void onOrderPlaced(Order order) {
sendSms(order.getUserId(), "下单成功");
}
}
public class EmailObserver implements OrderObserver {
@Override
public void onOrderPlaced(Order order) {
sendEmail(order.getUserId(), "下单成功");
}
}
// 重构后的 OrderService
public class OrderService {
private OrderSubject subject = new OrderSubject();
public OrderService() {
subject.attach(new SmsObserver());
subject.attach(new EmailObserver());
}
public void placeOrder(Order order) {
save(order);
subject.notifyObservers(order);
}
}
你看,现在要加微信推送,只需要新增一个 WechatObserver 实现 OrderObserver 接口,然后在构造函数里 attach 一下就行。OrderService 本身不用动。
核心变化:从“主动调用”变成了“被动通知”。OrderService 不再关心谁在监听,它只负责触发事件。这就是观察者模式带来的解耦效果。
我的经验:观察者模式特别适合用在“一对多”的通知场景。但要注意,如果观察者太多,通知顺序可能成为问题。我曾经在一个项目中,因为观察者执行顺序不对,导致短信先于库存扣减发出,用户收到短信但实际库存没扣成功。后来我加了一个优先级排序机制才解决。
模板方法模式:把不变的流程锁死,把可变的部分开放
模板方法模式,说白了就是定义一个操作中的算法骨架,而将一些步骤延迟到子类中实现。它解决的是“流程相同,细节不同”的问题。
举个例子。假设我们要做数据导出功能,支持导出 CSV、Excel、PDF 三种格式。导出流程其实是一样的:查询数据 → 格式化数据 → 写入文件 → 返回下载链接。但每种格式的格式化方式和写入方式不同。
最初的代码可能是这样的:
public class CsvExporter {
public String export() {
List<Data> data = queryData();
String csvContent = formatToCsv(data);
String filePath = writeToFile(csvContent, ".csv");
return generateDownloadLink(filePath);
}
}
public class ExcelExporter {
public String export() {
List<Data> data = queryData();
Workbook workbook = formatToExcel(data);
String filePath = writeToFile(workbook, ".xlsx");
return generateDownloadLink(filePath);
}
}
每个导出器都重复了“查询数据 → 写入文件 → 生成链接”这三步。只有中间那一步“格式化”不同。这就是典型的坏味道:重复的流程代码。
用模板方法模式重构:
public abstract class DataExporter {
// 模板方法,定义流程骨架
public final String export() {
List<Data> data = queryData();
Object formatted = format(data);
String filePath = writeToFile(formatted, getFileExtension());
return generateDownloadLink(filePath);
}
// 公共步骤,直接实现
private List<Data> queryData() {
// 从数据库查询数据
return database.query("SELECT * FROM export_data");
}
private String writeToFile(Object content, String extension) {
// 写入文件并返回路径
String fileName = UUID.randomUUID().toString() + extension;
FileUtil.write(fileName, content);
return fileName;
}
private String generateDownloadLink(String filePath) {
return "https://download.example.com/" + filePath;
}
// 可变步骤,由子类实现
protected abstract Object format(List<Data> data);
protected abstract String getFileExtension();
}
// CSV 导出器
public class CsvExporter extends DataExporter {
@Override
protected Object format(List<Data> data) {
StringBuilder sb = new StringBuilder();
sb.append("id,name,price\n");
for (Data d : data) {
sb.append(d.getId()).append(",")
.append(d.getName()).append(",")
.append(d.getPrice()).append("\n");
}
return sb.toString();
}
@Override
protected String getFileExtension() {
return ".csv";
}
}
// Excel 导出器
public class ExcelExporter extends DataExporter {
@Override
protected Object format(List<Data> data) {
Workbook workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet("数据");
// 填充数据到 sheet
return workbook;
}
@Override
protected String getFileExtension() {
return ".xlsx";
}
}
关键点:模板方法(export)用 final 修饰,防止子类篡改流程。子类只能覆盖抽象方法,不能改变整体步骤。这就是“把不变的流程锁死,把可变的部分开放”。
避坑指南:我曾经在一个项目中,有人把模板方法里的某个步骤也改成了 protected 并允许子类覆盖。结果子类覆盖后跳过了数据校验步骤,导致导出的数据全是脏数据。所以记住:模板方法本身一定要用 final,公共步骤用 private,只有可变步骤才用 protected abstract。
观察者模式 vs 模板方法模式:什么时候用哪个?
这两个模式虽然不同,但经常一起出现。我画了一张图帮你理清它们的定位:
简单总结一下:
- 观察者模式解决的是“一个变化,多个响应”的问题。它让通知者和被通知者之间没有直接依赖。
- 模板方法模式解决的是“流程固定,步骤可变”的问题。它把重复的流程代码收拢到父类,子类只关心差异部分。
这两个模式在重构中经常配合使用。比如在模板方法的某个步骤里,可以用观察者模式来通知多个监听器。我做过的一个支付系统中,支付流程用模板方法定义,而支付成功后的通知(短信、邮件、积分)则用观察者模式处理。两者结合,代码既清晰又灵活。
我的建议:不要为了用模式而用模式。先写能跑的代码,等发现重复代码或硬编码通知时,再考虑引入观察者或模板方法。重构的节奏应该是:闻到坏味道 → 识别模式适用场景 → 小步重构 → 验证。一步到位反而容易出问题。
好了,观察者模式和模板方法模式就讲到这里。这两种模式在实战中非常实用,尤其是当你面对“通知扩散”和“流程重复”这两类坏味道时,它们几乎是标准答案。下一讲我们会聊更进阶的重构技巧,敬请期待。