24|模板方法:固定执行顺序,开放必要步骤
配套代码:GitHub 仓库 · 本篇完整源码 · 行为测试。使用 JDK 25 与 Gradle,包名为
com.hanserwei.patterns.templatemethod。
系列导航:Java 25 设计模式学习指南
文章导出总要先校验和清理标题,再输出前缀、正文和后缀。Markdown 与带框文本的差异只在某些步骤。如果每个导出器自由实现 export,某个子类可能忘记校验,或把后缀拼在错误位置。
模板方法让父类固定整体算法,子类只提供有限步骤。本例保持继承关系简单,使“不能改什么”和“允许扩展什么”都能从 API 直接看出来。
从问题提炼设计意图
模板方法在父类定义算法骨架,把部分步骤延迟给子类实现或覆写,在保持流程结构的同时复用公共行为。
对象职责与协作关系
| 示例角色 | 职责 |
|---|---|
ArticleExporter |
抽象模板,定义 final export 算法 |
formatBody |
必须由子类实现的抽象步骤 |
header、footer |
带默认实现的可选钩子 |
MarkdownExporter、FramedExporter |
提供不同步骤行为的具体子类 |
classDiagram
ArticleExporter <|-- MarkdownExporter
ArticleExporter <|-- FramedExporter
Demo ..> ArticleExporter : export
代码思路:变化应该落在哪个对象上
export 使用 final 防止子类替换流程:先拒绝空白标题,再 strip,最后按 header + formatBody + footer 顺序形成结果。模板入口拥有公共不变量,而非要求每个子类自觉复制校验。
formatBody 是抽象步骤,任何具体子类都必须给出实现。header 与 footer 默认返回空字符串,是可选钩子;MarkdownExporter 只需要实现正文,FramedExporter 则覆写三个步骤。
调用发生在父类方法内部,具体步骤依然会动态分派给实际子类。这种“父类流程调用子类扩展点”的控制关系,使框架能够控制时机,让扩展者只负责局部行为。
关键实现与独立运行
配套仓库中的包名是 com.hanserwei.patterns.templatemethod,源码目录为 src/main/java/com/hanserwei/patterns/templatemethod/。仓库地址统一见系列导航。以下展示关键文件的完整内容;其余角色和测试在同一仓库中,每个顶级类型各占一个文件。
ArticleExporter.java:
package com.hanserwei.patterns.templatemethod;
/** 固定校验、转换和包装顺序的导出模板. */
public abstract class ArticleExporter {
/** 固定算法骨架,子类不能跳过标题校验或改变执行顺序. */
public final String export(String title) {
if (title == null || title.isBlank()) {
throw new IllegalArgumentException("Title must not be blank");
}
return header() + formatBody(title.strip()) + footer();
}
/** 返回文档前缀;默认不添加前缀. */
protected String header() {
return "";
}
/** 由子类实现正文转换,接收已去除首尾空白的标题. */
protected abstract String formatBody(String title);
/** 返回文档后缀;默认不添加后缀. */
protected String footer() {
return "";
}
}
FramedExporter.java:
package com.hanserwei.patterns.templatemethod;
/** 覆盖三个步骤但不改变模板顺序的带边框导出器. */
public final class FramedExporter extends ArticleExporter {
/** 添加左边框. */
@Override
protected String header() {
return "[";
}
/** 保留校验和清理后的标题. */
@Override
protected String formatBody(String title) {
return title;
}
/** 添加右边框. */
@Override
protected String footer() {
return "]";
}
}
Demo.java 展示调用方如何装配这些对象:
package com.hanserwei.patterns.templatemethod;
/** 演示本章对象的装配方式和可观察结果. */
public final class Demo {
/** 禁止实例化演示入口. */
private Demo() {}
/** 运行独立示例;args 为未使用的命令行参数. */
public static void main(String[] args) {
ArticleExporter exporter = new FramedExporter();
System.out.println(exporter.export(" Java 25 "));
}
}
在配套代码仓库根目录运行;Windows 使用 gradlew.bat 替换 ./gradlew:
./gradlew runTemplatemethod
./gradlew test --tests 'com.hanserwei.patterns.templatemethod.PatternTest'
示例的业务输出如下,省略 Gradle 自身的任务提示:
[Java 25]
用测试确认模式的行为
两个子类都复用首尾空白清理;FramedExporter 的输出顺序为左框、正文、右框;两个子类都不能接受空白标题。
对应测试位于 src/test/java/com/hanserwei/patterns/templatemethod/PatternTest.java。建议先运行现有测试,再改动一个协作环节,观察哪个断言能够发现问题。
常见用法
- 批处理、导入导出、测试生命周期等流程顺序稳定。
- 框架定义何时执行动作,扩展者提供少量具体步骤。
- 多个实现共享校验、准备和收尾,且步骤之间有稳定约束。
适用边界与容易踩的坑
不要在父类构造器里调用可覆写步骤,此时子类字段可能尚未初始化。把模板入口作为构造完成后显式调用的方法,更容易保证对象有效。
钩子越多,子类越难理解允许的组合。如果几乎每一步都要覆写,甚至需要改变执行顺序,说明父类骨架可能并不稳定,应考虑组合多个策略或独立流程对象。
涉及资源时,收尾不能只靠字符串式“先后拼接”。真实文件导出要用 try-with-resources 或 finally,明确失败路径上哪些步骤仍会执行。本例只处理字符串,不声称展示了资源管理模板。
与相近模式比较
工厂方法可以成为模板方法中的一个步骤,专门创建产品;策略则用组合替换算法,减少继承耦合。选择时先问流程顺序是否稳定,以及变化是一个步骤还是整体算法。
动手练习
增加导出审计钩子,明确成功与失败时是否调用。再实现一个 RecordingExporter 测试替身记录步骤调用顺序,验证异常发生后不会错误执行成功审计。
系列导航:Java 25 设计模式学习指南