大多数 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);
}
差异:
process()是final------没有子类能改顺序- 错误处理在模板里------
handleTransformFailure是有默认的 hook,不是 abstract notifyListeners是 no-op hook------子类可以加行为,但可选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(回调)。基于变异频率选择,不是基于教程教你的版本。
顺便提一下,我在做的「爪爪代码冒险记」小程序里,模板方法那期画的场景是:卡皮巴拉在填空题试卷上乱写答案,老师把试卷锁死只留了三道必填题。比光看代码讲抽象方法好记一点,搜搜看。