你写的 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(回调)。基于变异频率选择,不是基于教程教你的版本。

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

相关推荐
垚垚学技术_聚焦云原生42 分钟前
K8s Pod 完整生命周期详解
后端
JavaGuide1 小时前
Github 史诗级故障,与此同时,Cursor 版「GitHub」正式上线!
前端·后端
不一样的少年_1 小时前
修了 Bug、做了重构,为什么老板还是觉得你没产出?
前端·后端·程序员
水深火乐1 小时前
安全地生成验证码、密码和 Token
后端
小强19881 小时前
SQL Server 慢查询怎么定位?一套从“卡”到“快”的完整排查流程
后端
水深火乐1 小时前
interface和any
后端
步行cgn1 小时前
MyBatis Error evaluating expression ‘ids‘. Return value (3) was not iterable 错误详
java·后端
神奇小汤圆1 小时前
Spring Boot + LangChain4j实现RAG——从零搭建企业级知识库问答系统
后端
H_Peak2 小时前
Python运算符与流程控制:if条件判断
后端
H_Peak2 小时前
Python字符串操作:从基础到正则入门
后端