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 模板方法模式:什么时候用哪个?

这两个模式虽然不同,但经常一起出现。我画了一张图帮你理清它们的定位:

观察者模式 vs 模板方法模式 观察者模式 核心:一对多通知 场景:状态变化 → 触发多个动作 重构目标:消除硬编码通知 典型应用: • 下单后发短信、邮件、微信 • 数据变更后更新缓存、索引 • 事件驱动架构中的本地实现 模板方法模式 核心:固定流程 + 可变步骤 场景:流程相同,实现细节不同 重构目标:消除重复流程代码 典型应用: • 数据导出(CSV/Excel/PDF) • 报表生成(不同格式) • 工作流引擎中的节点执行 共同点:都遵循开闭原则,对扩展开放,对修改关闭

简单总结一下:

  • 观察者模式解决的是“一个变化,多个响应”的问题。它让通知者和被通知者之间没有直接依赖。
  • 模板方法模式解决的是“流程固定,步骤可变”的问题。它把重复的流程代码收拢到父类,子类只关心差异部分。

这两个模式在重构中经常配合使用。比如在模板方法的某个步骤里,可以用观察者模式来通知多个监听器。我做过的一个支付系统中,支付流程用模板方法定义,而支付成功后的通知(短信、邮件、积分)则用观察者模式处理。两者结合,代码既清晰又灵活。

我的建议:不要为了用模式而用模式。先写能跑的代码,等发现重复代码或硬编码通知时,再考虑引入观察者或模板方法。重构的节奏应该是:闻到坏味道 → 识别模式适用场景 → 小步重构 → 验证。一步到位反而容易出问题。

好了,观察者模式和模板方法模式就讲到这里。这两种模式在实战中非常实用,尤其是当你面对“通知扩散”和“流程重复”这两类坏味道时,它们几乎是标准答案。下一讲我们会聊更进阶的重构技巧,敬请期待。


公众号:蓝海资料掘金营,微信deep3321