模板方法模式:算法骨架、钩子方法、实战:JdbcTemplate

说实话,模板方法模式是我在日常开发中用得最顺手的模式之一。它不像单例、工厂那样被天天挂在嘴边,但很多框架的核心流程都靠它来兜底。你想想看,如果每次写数据库操作都要重复“获取连接、创建语句、处理结果、关闭资源”这一套,那代码得多崩溃?

模板方法模式的核心思想很简单:把不变的流程封装在父类,把可变的部分留给子类去实现。说白了,就是“骨架我做主,细节你填充”。

核心定义: 定义一个操作中的算法骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

算法骨架:不变的流程

我习惯把模板方法模式比作“做菜流程”。不管你是做红烧肉还是清蒸鱼,步骤都是:准备食材 → 烹饪 → 装盘。但具体怎么切、用什么调料、火候多大,那是子类的事。

来看一个简单的代码骨架:

public abstract class DataProcessor {
    // 模板方法,定义算法骨架
    public final void process() {
        loadData();
        validateData();
        processData();
        saveResult();
        cleanup();
    }

    // 具体步骤,子类可重写
    protected abstract void loadData();
    protected abstract void processData();

    // 可选步骤,子类可覆盖
    protected void validateData() {
        System.out.println("默认校验:数据非空");
    }

    // 固定步骤,子类不可修改
    private void saveResult() {
        System.out.println("保存结果到数据库");
    }

    // 钩子方法:子类可控制是否执行某一步
    protected boolean needCleanup() {
        return true;
    }

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

注意看,process() 方法加了 final,这就是在告诉子类:“流程顺序你别动,动了就乱套了。” 我在项目中见过有人把模板方法去掉 final,结果子类重写后把校验步骤跳过了,线上出了数据问题。嗯,从那以后我写模板方法必加 final。

钩子方法:给子类留个“开关”

钩子方法(Hook)是模板方法模式里一个很巧妙的设计。它允许子类在特定节点“插一脚”,甚至决定某个步骤是否执行。

上面的 needCleanup() 就是一个钩子。默认返回 true,子类如果不需要清理资源,可以返回 false。比如某些只读查询,根本不需要清理写锁,就可以关掉这个步骤。

我的经验: 钩子方法的名字最好以 "need"、"should"、"is" 开头,让人一眼就看出这是个条件判断。我曾经用 "hookBeforeProcess" 这种名字,结果团队里新人以为必须实现它,反而增加了理解成本。

再举个例子,假设你做一个报表生成器,有的报表需要打印表头,有的不需要。你可以在父类里加一个 isShowHeader() 钩子,默认返回 true,子类按需覆盖。这样既保持了流程统一,又给了灵活性。

实战:JdbcTemplate 中的模板方法

说到实战,Spring 的 JdbcTemplate 是模板方法模式的经典案例。我刚开始用 Spring 时,只觉得它用起来方便,后来读了源码才恍然大悟——原来底层就是模板方法。

JdbcTemplate 的核心方法是 execute(),它定义了数据库操作的固定流程:

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

其中,第3步和第5步是通过回调接口 PreparedStatementCallbackResultSetExtractor 来让调用者自定义的。这其实就是模板方法模式的变种——用接口回调代替抽象方法,但思想完全一致。

// JdbcTemplate 简化版核心代码
public class JdbcTemplate {
    public <T> T execute(ConnectionCallback<T> action) {
        Connection con = null;
        try {
            con = getConnection(); // 固定步骤
            return action.doInConnection(con); // 回调,由调用者实现
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            closeConnection(con); // 固定步骤
        }
    }
}

你看,getConnection()closeConnection() 是固定的骨架,而具体的 SQL 操作通过回调交给调用者。这样既避免了重复的 try-catch-finally,又保留了灵活性。

注意: 模板方法模式虽然好用,但别滥用。如果一个算法只有两步,而且两步都可能变化,那用策略模式可能更合适。我见过有人把简单的 get/set 也套上模板方法,结果代码膨胀得厉害。记住:模板方法适合“大部分固定,小部分变化”的场景。

模板方法 vs 策略模式:怎么选?

很多同学会混淆这两个模式。我简单总结一下:

维度 模板方法模式 策略模式
核心思想 固定算法骨架,子类重写部分步骤 将算法封装为独立策略,运行时切换
变化粒度 算法中的某一步或几步 整个算法
实现方式 继承 + 抽象方法 组合 + 接口
典型场景 JdbcTemplate、Servlet 的 doGet/doPost 支付方式选择、排序算法

说白了,如果你要控制流程的顺序,用模板方法;如果你只是要替换整个算法,用策略模式。两者也可以结合使用,比如模板方法内部调用策略接口,这在 Spring 源码里很常见。

SVG 流程图:模板方法模式的核心逻辑

下面这张图我画了好几次,力求把“骨架 + 钩子 + 子类实现”的关系讲清楚。你看一眼应该就能记住。

模板方法模式核心逻辑 AbstractClass (父类) + templateMethod() [final] + step1() [abstract] + step2() [abstract] + step3() [concrete] + hook() [钩子] ConcreteClassA + step1() { 具体实现A } + step2() { 具体实现A } + hook() { return false } ConcreteClassB + step1() { 具体实现B } + step2() { 具体实现B } + hook() { return true } 钩子控制 是否执行清理 算法骨架流程 1. step1() 2. step2() 3. step3() 4. hook() ? 清理 : 跳过

从图里可以清楚看到:父类定义了流程顺序,子类只负责实现抽象方法和覆盖钩子。流程的“方向盘”始终在父类手里。

避坑指南:我踩过的三个坑

  • 坑一:模板方法里调用可重写的方法 —— 我曾经在父类构造器里调用了抽象方法,结果子类还没初始化完,导致空指针。记住:构造器里别调可重写的方法,这是 Java 的经典坑。
  • 坑二:钩子方法太多 —— 有个项目我加了五六个钩子,结果子类代码里全是 if-else,反而比不用模板方法还乱。钩子一般控制在 1-2 个,多了就该考虑拆分类了。
  • 坑三:忘记 final 模板方法 —— 前面提过,不加 final 就等于把流程控制权交给了子类。团队里如果有人不小心重写了模板方法,整个骨架就变形了。
我的习惯: 写模板方法时,我会在父类注释里写明“此方法为模板方法,请勿重写”。虽然 final 已经强制了,但注释能让阅读代码的人更快理解设计意图。

好了,模板方法模式就聊到这儿。它不像那些花哨的模式,但胜在稳定、实用。下次你写重复的流程代码时,不妨想想:能不能把骨架抽出来?


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