你写的 abstract class 里全是钩子方法——模板模式不是让你填空,是让你别越界

大多数 Java 开发者写过模板方法却不知道------每次你定义一个抽象类,里面一个具体方法调用若干抽象方法,就是模板方法。这是这个模式------一个方法定义骨架,子类填充步骤。

问题是大多数人写错了。他们让每个步骤 abstract,什么都不留 concrete,把模式当填空题用。基类变成了空壳------只有一个方法只是抽象调用的序列。子类覆盖一切、改顺序、跳步骤,"模板"就失去了意义。

模板方法不是让你填空。它是强制结构------定义必须发生什么、按什么顺序、什么不能改。模式的威力在于它让什么 final,不在它让什么 abstract。

空壳模板问题

这是错误写法:

java 复制代码
public abstract class DataProcessor {
    public void process(Data data) {
        validate(data);
        transform(data);
        persist(data);
    }

    protected abstract void validate(Data data);
    protected abstract void transform(Data data);
    protected abstract void persist(Data data);
}

每个步骤都是 abstract。process() 看起来像模板,其实只是建议。任何子类可以覆盖 process() 本身完全改变行为。抽象方法不是"步骤"------只是碰巧在这个实现里按这个顺序调用的方法。

出什么问题:

  • 子类覆盖 process() 跳过验证
  • 子类覆盖 process() 改顺序:先持久化再验证
  • 子类覆盖 validate() 抛 unchecked exception 打断模板
  • 两个子类用完全不相容的语义覆盖 persist(),模板无法强制一致性

基类没有权威。它建议了一个序列,但不能执行它。那不是模板方法------只是一个没人遵守的约定的抽象类。

真正的模板方法:什么保持固定

模板方法的目的是定义不变行为------必须始终发生的事、始终按此顺序、始终有这些保证。基类里的具体方法是模板,它应该 final

java 复制代码
public abstract class DataProcessor {
    public final void process(Data data) {
        validate(data);
        try {
            transform(data);
        } catch (TransformException e) {
            handleTransformFailure(data, e);
            return;
        }
        persist(data);
        notifyListeners(data);
    }

    protected abstract void validate(Data data);
    protected abstract void transform(Data data) throws TransformException;

    // Hook 方法有默认行为------子类可以覆盖
    protected void handleTransformFailure(Data data, TransformException e) {
        throw e; // 默认:传播
    }

    protected void notifyListeners(Data data) {
        // 默认:什么都不做(no-op hook)
    }

    protected abstract void persist(Data data);
}

差异:

  1. process()final------没有子类能改顺序
  2. 错误处理在模板里------handleTransformFailure 是有默认的 hook,不是 abstract
  3. notifyListeners 是 no-op hook------子类可以加行为,但可选
  4. transform 声明 checked exception------模板控制异常如何流动

这是模板方法的设计意图:基类拥有结构,子类在该结构内提供具体实现。模板是契约,不是建议。

Spring 哪里正确用了模板方法

Spring 的 AbstractApplicationContext.refresh() 是生产代码里模板方法的最佳示例。12 步模板初始化整个 Spring 容器:

java 复制代码
@Override
public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
        prepareRefresh();
        ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
        prepareBeanFactory(beanFactory);
        try {
            postProcessBeanFactory(beanFactory);
            invokeBeanFactoryPostProcessors(beanFactory);
            registerBeanPostProcessors(beanFactory);
            initMessageSource();
            initApplicationEventMulticaster();
            onRefresh();  // ← 给子类的 hook
            registerListeners();
            finishBeanFactoryInitialization(beanFactory);
            finishRefresh();
        } catch (BeansException ex) {
            destroyBeans();
            cancelRefresh(ex);
            throw ex;
        }
    }
}

Spring 做对了什么:

  • refresh() 不是 final,但 300 行精心排序的初始化,实践中没人覆盖它,因为结构够刚性,改了就全崩
  • onRefresh() 是 protected hook。AbstractRefreshableWebApplicationContext 覆盖它初始化 theme resolution。大多数子类不覆盖。可选。
  • 错误处理在模板里------destroyBeans()cancelRefresh() 在失败时调用。子类不需要操心清理。
  • 12 个步骤固定。你不能跳 invokeBeanFactoryPostProcessors 或重排 initMessageSource。模板强制生命周期。

这是 Spring 能工作的原因。容器初始化有不变结构,99% 的应用不该碰它。一个 hook(onRefresh)给 1% 的场景刚好够的灵活性。

JdbcTemplate:伪装成回调的模板方法

JdbcTemplate 技术上是模板方法,但用回调替代继承:

java 复制代码
@Override
public <T> T query(PreparedStatementCreator psc, ResultSetExtractor<T> rse)
    throws DataAccessException {
    try {
        Connection con = DataSourceUtils.getConnection(obtainDataSource());
        PreparedStatement ps = null;
        try {
            ps = psc.createPreparedStatement(con);  // ← 回调
            T result = rse.extractData(ps.executeQuery());  // ← 回调
            return result;
        } finally {
            JdbcUtils.closeStatement(ps);
        }
    } finally {
        DataSourceUtils.releaseConnection(con, obtainDataSource());
    }
}

模板处理连接获取、statement 清理和连接释放。可变部分------创建 statement 和提取结果------是回调。结构与模板方法一样:固定骨架 + 可变步骤。但不是抽象方法,而是函数式接口(PreparedStatementCreator, ResultSetExtractor)。

这其实是大多数场景更好的模板方法形式。继承式模板方法制造基类和子类的永久耦合。回调式模板方法让你每次调用都改变步骤,不用建子类层级。你可以在每次调用改"步骤",而不是每个子类实例改一次。

GoF 预见了这点:"Template methods are a common technique for factoring out common behavior in library classes." 他们没强制继承。模式是关于固定骨架 + 可变步骤------机制(abstract 方法、回调、lambda)是次要的。

Hook 方法的陷阱

Hook 方法------protected、有默认行为、子类可覆盖------是模板方法最危险的功能。看起来无害,但制造隐性契约:

java 复制代码
// 基类
protected boolean shouldRetry(Exception e) {
    return e instanceof TransientException;
}

// 子类 A
@Override
protected boolean shouldRetry(Exception e) {
    return true; // 重试一切
}

// 子类 B
@Override
protected boolean shouldRetry(Exception e) {
    return false; // 从不重试
}

基类的模板依赖 shouldRetry() 返回合理答案。但"合理"是什么,没有文档说明。子类 A 重试一切包括非瞬态错误。子类 B 从不重试包括瞬态错误。两者"能工作"但都违反模板的隐式契约。

修复:显式文档 hook 契约,关键 hook 考虑 final

java 复制代码
/**
 * 是否在给定异常上重试。
 * 默认:瞬态异常重试。
 * 仅在你有更严格或更具体的重试策略时覆盖。
 * 对非瞬态异常必须返回 false。
 */
protected boolean shouldRetry(Exception e) {
    return e instanceof TransientException;
}

更好的做法:当变异性太宽时,用 Strategy 对象替代 hook:

java 复制代码
public final void process(Data data) {
    validate(data);
    try {
        transform(data, transformer);  // Strategy,不是 hook
    } catch (TransformException e) {
        if (retryPolicy.shouldRetry(e)) {  // Strategy,不是 hook
            transform(data, transformer);
        }
    }
    persist(data);
}

当变异点确实宽------重试策略从"从不"到"指数退避始终重试"------Strategy 比 hook 更合适。Hook 适用于变异小、默认覆盖大多数场景的情况。Strategy 适用于变异是根本性的、每个选项是完整策略的情况。

模板方法 vs 回调:什么时候用哪个

继承式模板方法(abstract class + abstract/hook 方法):

  • 变异点稳定------你有 2-3 个已知子类类型
  • 子类层级有意义------每个子类代表真实类别(XML processor, JSON processor, CSV processor)
  • 你要强制模板------模板方法 final,步骤 protected

回调式模板方法(final class + 函数式接口):

  • 变异点每次调用都变------每次调用可能用不同逻辑
  • 不想要永久子类层级------组合优于继承
  • 步骤真正独立------它们之间没有耦合

大多数现代 Java 代码应该优先回调。继承式模板方法是 1995 年的正确选择,当时接口和 lambda 不存在。今天,JdbcTemplate 的回调方式是更好的默认。

模式任何方式都一样:固定骨架,可变步骤。问题在于可变部分是 per-type(继承)还是 per-invocation(回调)。基于变异频率选择,不是基于教程教你的版本。

顺便提一下,我在做的「爪爪代码冒险记」小程序里,模板方法那期画的场景是:卡皮巴拉在填空题试卷上乱写答案,老师把试卷锁死只留了三道必填题。比光看代码讲抽象方法好记一点,搜搜看。

相关推荐
ping某1 小时前
语法树,到底是一棵什么形状的树?
后端
_柳青杨1 小时前
一文吃透 Node.js 事件循环:从原理到 Node 20+ 重大变更
javascript·后端
Alson_Code2 小时前
人机协作项目文档--HITL-AgentScope
后端·aigc·ai编程
IT_陈寒2 小时前
Java 并行流把我坑惨了,这6小时加班值了
前端·人工智能·后端
葫芦和十三3 小时前
图解 MongoDB 03|CRUD 全链路:一条 find 怎么穿过 WiredTiger
后端·mongodb·agent
葫芦和十三11 小时前
图解 MongoDB 04|索引模型:每建一个索引,就是在 B+-tree 森林里多栽一棵
后端·mongodb·agent
用户479492835691512 小时前
claude Fable用不了?把Gpt 5.5pro接到你的claude code里
前端·后端
GetcharZp14 小时前
告别 Nginx 复杂配置!这款带 Web 面板的万能代理神器,让端口转发变得如此简单
后端
IT_陈寒16 小时前
React的useState居然还有这种坑?我差点删库跑路
前端·人工智能·后端