模板方法模式:骨架与钩子的艺术

模板方法模式,说白了就是「把不变的流程固定下来,把可变的部分留给子类」。我刚开始接触设计模式时,觉得这玩意儿太简单了——不就是抽象类里写个方法,再让子类去实现吗?后来在项目中踩过坑才明白,真正用好它,关键在于理解「钩子方法」的灵活运用。

模式结构:骨架与步骤

模板方法模式的核心结构其实很清晰。它由两部分组成:

  • 抽象模板类:定义算法的骨架,包含一个或多个模板方法
  • 具体实现类:重写抽象方法,提供具体实现

我习惯把模板方法想象成「做菜的菜谱」。菜谱告诉你:先洗菜、再切菜、然后炒菜、最后装盘。这个流程是固定的。但具体洗什么菜、怎么切、用什么调料炒,这些细节可以自由发挥。模板方法模式就是这种思想在代码中的体现。

核心要点:模板方法模式定义了操作中的算法骨架,而将一些步骤延迟到子类中实现。它让子类在不改变算法结构的情况下,重新定义算法中的某些特定步骤。

来看一个典型的模板方法模式结构图:

AbstractTemplate + templateMethod() + step1() + step2() + step3() + hookMethod() // 钩子方法,默认空实现 ConcreteTemplateA + step1() { // 具体实现A } + step2() { // 具体实现A } + step3() { // 具体实现A } + hookMethod() { // 重写钩子 } ConcreteTemplateB + step1() { // 具体实现B } + step2() { // 具体实现B } + step3() { // 具体实现B } + hookMethod() { // 不重写,使用默认 } templateMethod() 调用步骤 模板方法模式结构:抽象类定义骨架,子类实现具体步骤

模板方法的核心机制

模板方法模式中有几种关键的方法类型:

方法类型 说明 示例
模板方法 定义算法骨架,通常用 final 修饰 templateMethod()
抽象方法 子类必须实现的步骤 step1(), step2()
具体方法 所有子类共享的逻辑 commonStep()
钩子方法 可选扩展点,默认空实现 hookMethod()

我的经验:钩子方法是最容易被忽视但最有价值的部分。我曾经在一个支付系统中,用钩子方法实现了「是否发送通知」的控制。默认不发送,但某些子类可以重写钩子来开启通知。这样既保持了流程的统一,又给了子类灵活的选择权。

钩子方法:灵活性的开关

钩子方法,说白了就是「给你一个机会,在流程的某个节点插一脚」。它通常是一个空实现的方法,子类可以选择性地重写它。

为什么会需要钩子方法?你想想看,有时候子类并不想完全改变某个步骤,只是想在某些条件下做点额外的事情。比如:

  • 在流程开始前检查某个条件
  • 在流程结束后执行清理工作
  • 控制流程中的某个分支是否执行

来看一个代码示例:

public abstract class DataProcessor {
    // 模板方法
    public final void process() {
        loadData();
        if (shouldValidate()) {  // 钩子方法控制
            validateData();
        }
        processData();
        if (shouldLog()) {       // 钩子方法控制
            logResult();
        }
        cleanup();
    }

    protected abstract void loadData();
    protected abstract void processData();

    // 具体方法
    private void validateData() {
        System.out.println("验证数据...");
    }

    private void cleanup() {
        System.out.println("清理资源...");
    }

    // 钩子方法 - 默认实现
    protected boolean shouldValidate() {
        return true;  // 默认验证
    }

    protected boolean shouldLog() {
        return false; // 默认不记录日志
    }

    protected void logResult() {
        // 空实现,子类可重写
    }
}

避坑指南:我曾经在一个项目中,把钩子方法设计成了抽象方法,结果每个子类都得实现它,哪怕大多数子类根本不需要。这完全违背了钩子方法的初衷。记住:钩子方法应该是可选的,不是强制的。

Spring JdbcTemplate 中的模板方法

说到模板方法模式的实际应用,Spring 的 JdbcTemplate 是个绝佳的例子。我刚开始用 Spring 时,只觉得它封装了 JDBC 的繁琐操作,后来看源码才发现,它骨子里就是模板方法模式。

JdbcTemplate 的核心流程是这样的:

  1. 获取数据库连接
  2. 创建 PreparedStatement
  3. 设置参数(由回调处理)
  4. 执行 SQL
  5. 处理结果集(由回调处理)
  6. 清理资源

这个流程是固定的,但「设置参数」和「处理结果」这两个步骤是变化的。Spring 用回调接口来处理这种变化:

// JdbcTemplate 的核心方法(简化版)
public <T> T execute(PreparedStatementCreator psc,
                     PreparedStatementCallback<T> action) {
    // 1. 获取连接
    Connection con = DataSourceUtils.getConnection(dataSource);
    PreparedStatement ps = null;
    try {
        // 2. 创建 PreparedStatement
        ps = psc.createPreparedStatement(con);
        // 3. 执行并处理结果(回调)
        return action.doInPreparedStatement(ps);
    } catch (SQLException ex) {
        // 异常处理
        throw translateException(ex);
    } finally {
        // 4. 清理资源
        JdbcUtils.closeStatement(ps);
        DataSourceUtils.releaseConnection(con, dataSource);
    }
}

你发现了吗?JdbcTemplate 的模板方法把「获取连接」和「清理资源」这些固定操作封装好了,而把「创建语句」和「处理结果」这些变化的部分通过回调暴露出去。这就是模板方法模式的精髓——把不变的和变的分开。

关键洞察:JdbcTemplate 用的是「回调式模板方法」,它没有用继承,而是用组合。这种方式更灵活,因为调用方不需要继承某个类,只需要实现一个接口。这是模板方法模式的一种变体,在 Spring 中非常常见。

模板方法 vs 策略模式

很多初学者会把模板方法和策略模式搞混。我简单说说它们的区别:

维度 模板方法模式 策略模式
关注点 算法的骨架和流程 算法的选择和替换
变化方式 子类重写部分步骤 替换整个策略对象
关系 继承关系(is-a) 组合关系(has-a)
控制权 父类控制流程(好莱坞原则) 客户端选择策略

说白了,模板方法模式是「我给你一个框架,你在框架里填空」。策略模式是「我给你一个接口,你随便换实现」。两者各有适用场景,没有谁优谁劣。

我的建议:当你发现多个类有相似的流程,只是某些步骤不同时,优先考虑模板方法模式。当你发现某个行为有多种实现方式,且需要在运行时切换时,用策略模式。我在实际项目中,经常把两者结合使用——模板方法定义流程,策略模式处理流程中的某个复杂步骤。

实战中的注意事项

最后,分享几个我在实战中总结的经验:

  • 模板方法不要太大:一个模板方法里不要塞太多步骤,否则子类实现起来会很痛苦。我一般控制在 5-7 个步骤以内。
  • 钩子方法要有默认行为:要么是空实现,要么是合理的默认值。不要让子类必须重写钩子方法。
  • 考虑用回调代替继承:如果子类只需要实现一两个方法,用回调接口可能比继承更灵活。JdbcTemplate 就是这么做的。
  • 注意好莱坞原则:「不要打电话给我们,我们会打电话给你」。模板方法模式中,父类控制流程,子类不要反过来调用父类的方法。

曾经踩过的坑:我曾经在一个订单处理系统中,把模板方法设计得太「死」了。所有步骤都用 final 修饰,子类完全无法扩展。后来有个新需求要在订单处理中间加一个步骤,我不得不修改父类代码。这违反了开闭原则。从那以后,我学会了在模板方法中适当留一些「扩展点」——用钩子方法或者回调接口。

模板方法模式看起来简单,但用好它需要你对业务流程有深刻的理解。记住:它的核心不是「复用代码」,而是「复用流程」。当你把流程固定下来,变化的部分自然就清晰了。


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