模板方法设计模式 | JavaSE
模板方法设计模式
一、学习目标
学完本章,你应该能够:
- 解释模板方法设计模式(Template Method Pattern)解决什么问题。
- 理解“固定流程”与“可变步骤”的区别。
- 能够从多个高度重复的方法中识别公共流程。
- 能够使用抽象父类封装公共算法骨架。
- 能够把不同子类的变化步骤抽取为抽象方法。
- 能够让模板方法统一调用抽象步骤。
- 理解为什么子类只需要关注自己的差异部分。
- 理解为什么模板方法经常使用
final修饰。 - 理解模板方法与普通代码复用之间的区别。
- 理解模板方法如何结合抽象类、继承、重写和多态。
- 能够独立完成
DataImporter模板案例。 - 能够判断什么时候适合使用模板方法,什么时候只是普通方法抽取。
二、核心知识
2.1 从重复代码开始
假设现在需要完成两种数据导入:
CSV 数据导入
Excel 数据导入
CSV:
public void importCsv() {
System.out.println("读取文件");
System.out.println(
"校验文件格式"
);
System.out.println(
"转换 CSV 数据"
);
System.out.println(
"保存到数据库"
);
}
Excel:
public void importExcel() {
System.out.println("读取文件");
System.out.println(
"校验文件格式"
);
System.out.println(
"转换 Excel 数据"
);
System.out.println(
"保存到数据库"
);
}
对比:
读取文件 相同
校验文件格式 相同
转换 CSV / Excel 数据 不同
保存到数据库 相同
真正重复的是:
整个流程结构
+
大部分固定步骤
只有:
数据转换方式
发生变化。
这种情况下,如果继续复制粘贴整个方法:
CSV 一份
Excel 一份
JSON 一份
XML 一份
...
代码重复会越来越严重。
2.2 最简单的普通方法抽取
第一反应可能是:
public void read() {
System.out.println("读取文件");
}
public void validate() {
System.out.println("校验数据");
}
public void save() {
System.out.println("保存数据");
}
这样确实减少了一部分重复。
但是每个子类仍然可能自己写:
read();
validate();
convert();
save();
问题仍然存在:
每个子类都要重复写流程
而且可能有人写成:
validate();
save();
convert();
read();
如果业务要求流程必须固定:
读取
↓
校验
↓
转换
↓
保存
仅仅抽取几个公共方法还不够。
我们需要进一步:
把整个流程本身也统一起来。
2.3 什么是模板方法设计模式
模板方法设计模式英文:
Template Method Pattern
当前课程可以这样理解:
在父类中定义完成某项工作的固定流程骨架,把所有实现共有的步骤写在父类中,把部分需要变化的步骤留给子类实现。
基本结构:
抽象父类
│
├── templateMethod()
│ 固定整体流程
│
├── commonStep1()
│ 公共实现
│
├── abstract variableStep()
│ 子类提供实现
│
└── commonStep2()
公共实现
子类:
ChildA
└── variableStep() A方案
ChildB
└── variableStep() B方案
2.4 原课程给出的核心写法
原教学资料把模板方法模式总结成:
1. 定义一个抽象类
2. 定义模板方法
放公共、固定流程
3. 定义抽象方法
把具体不同实现交给子类
4. 建议使用 final 修饰模板方法
这是本章最核心结构。
2.5 第一个模板方法案例
public abstract class DataImporter {
public final void importData() {
read();
validate();
convert();
save();
}
private void read() {
System.out.println(
"读取文件"
);
}
private void validate() {
System.out.println(
"校验文件"
);
}
protected abstract void convert();
private void save() {
System.out.println(
"保存到数据库"
);
}
}
这里:
importData()
就是:
模板方法。
它定义了:
读取
↓
校验
↓
转换
↓
保存
整个算法骨架。
2.6 哪些步骤是固定步骤
在:
importData()
中:
read();
validate();
save();
如果所有导入器都完全一样,就可以直接由父类实现。
例如:
private void read() {
System.out.println(
"读取文件"
);
}
这种步骤:
不希望每个子类重复实现,也不需要每个子类变化。
可以称为:
固定步骤
公共步骤
2.7 哪些步骤是可变步骤
protected abstract void convert();
父类知道:
所有数据导入都必须执行“数据转换”。
但是:
CSV 怎么转换
Excel 怎么转换
JSON 怎么转换
并不相同。
所以:
convert()
只定义行为:
protected abstract void convert();
具体实现交给子类。
这就是:
变化点。
2.8 CSVImporter
public class CsvImporter
extends DataImporter {
@Override
protected void convert() {
System.out.println(
"转换 CSV 数据"
);
}
}
子类只需要关心:
CSV 和别人哪里不一样?
答案:
convert()
因此只实现这一部分。
2.9 ExcelImporter
public class ExcelImporter
extends DataImporter {
@Override
protected void convert() {
System.out.println(
"转换 Excel 数据"
);
}
}
它同样不需要重新写:
读取
校验
保存
也不需要重复定义:
整个 importData 流程
2.10 调用模板方法
测试:
public class Test {
public static void main(String[] args) {
DataImporter csv =
new CsvImporter();
csv.importData();
System.out.println(
"---------------"
);
DataImporter excel =
new ExcelImporter();
excel.importData();
}
}
对于:
csv.importData();
流程:
DataImporter.importData()
↓
read()
↓
validate()
↓
convert()
↓
CsvImporter.convert()
↓
save()
而:
excel.importData();
流程:
DataImporter.importData()
↓
read()
↓
validate()
↓
convert()
↓
ExcelImporter.convert()
↓
save()
整个算法骨架没有变化。
真正变化的只是:
convert()
2.11 为什么模板方法使用 final
原课程特别强调:
建议使用
final修饰模板方法。
例如:
public final void importData() {
read();
validate();
convert();
save();
}
为什么?
因为整个模板方法存在的目的就是:
固定算法骨架。
如果子类可以:
@Override
public void importData() {
save();
convert();
read();
}
那父类规定的:
读取
↓
校验
↓
转换
↓
保存
就被彻底破坏了。
所以在当前课程这种:
流程必须严格固定
的模板方法中:
final
非常合理。
2.12 final 真正保护的是什么
不是为了:
“看到模板方法就机械加 final”
而是因为当前设计明确要求:
子类只能改变指定步骤,不能重新定义整个算法流程。
所以:
final templateMethod()
真正保护的是:
算法骨架。
2.13 模板方法模式中的“变与不变”
这是本章最重要的设计思想。
分析一个业务流程:
哪些永远一样?
↓
放父类固定
哪些一定存在,但实现不同?
↓
定义抽象方法
↓
交给子类
例如:
文章生成流程
初始化 固定
读取数据 固定
生成正文 变化
格式检查 固定
保存结果 固定
就可以考虑:
template()
├── init()
├── read()
├── generate() ← abstract
├── check()
└── save()
2.14 Template Method 和抽象类是什么关系
模板方法模式通常会利用:
抽象类
+
抽象方法
+
继承
+
方法重写
父类:
定义流程骨架
子类:
实现变化步骤
因此上一章:
抽象类与抽象方法
实际上就是本章的重要语言基础。
2.15 Template Method 与多态是什么关系
父类模板方法内部:
convert();
表面写的是:
DataImporter 中定义的抽象行为
实际对象如果是:
CsvImporter
就运行:
CsvImporter.convert()
实际对象如果是:
ExcelImporter
就运行:
ExcelImporter.convert()
所以模板方法内部本身就在利用:
方法重写 + 动态分派。
2.16 Template Method 调用关系
可以画成:
调用者
│
▼
templateMethod()
│
├── commonStep()
│
├── abstractStep()
│ │
│ ├── ChildA 实现
│ └── ChildB 实现
│
└── commonStep()
最有意思的地方是:
父类代码在运行过程中主动调用子类提供的实现。
这与普通理解中的:
子类调用父类方法
方向不同。
2.17 “父类调用子类实现”怎么做到的
代码:
DataImporter importer =
new CsvImporter();
importer.importData();
进入:
DataImporter.importData()
模板中:
convert();
因为当前实际对象:
CsvImporter
所以动态分派最终找到:
CsvImporter.convert()
因此实际调用链:
父类模板方法
↓
调用抽象步骤
↓
运行时选择子类实现
这就是多态真正参与模板方法的地方。
2.18 模板方法解决的不只是“重复几行代码”
原资料把它总结为:
解决方法中的重复代码问题。
这是最直接的效果。
但进一步看,它还解决:
1. 公共代码重复
2. 流程顺序容易被多个子类写乱
3. 公共流程修改时需要修改很多类
4. 子类承担了不必要的流程控制责任
模板方法把:
流程控制权
集中到父类。
子类只负责:
变化部分
三、使用方法
3.1 DataImporter 完整版
public abstract class DataImporter {
public final void importData() {
read();
validate();
convert();
save();
}
private void read() {
System.out.println(
"1. 读取原始数据"
);
}
private void validate() {
System.out.println(
"2. 校验原始数据"
);
}
protected abstract void convert();
private void save() {
System.out.println(
"4. 保存处理结果"
);
}
}
CSV:
public class CsvImporter
extends DataImporter {
@Override
protected void convert() {
System.out.println(
"3. 转换 CSV 数据"
);
}
}
Excel:
public class ExcelImporter
extends DataImporter {
@Override
protected void convert() {
System.out.println(
"3. 转换 Excel 数据"
);
}
}
3.2 为什么 fixed step 可以 private
例如:
private void read()
如果设计明确规定:
子类不需要修改读取规则。
那么甚至可以:
private
完全隐藏给父类自己使用。
这样子类只能看到真正的扩展点:
protected abstract void convert();
这使 API 更清楚:
你应该改哪里?
↓
convert()
你不应该改哪里?
↓
read / validate / save
3.3 protected abstract 作为扩展点
为什么:
protected abstract void convert();
而不是:
public abstract void convert();
如果 convert() 只是:
模板内部调用、供子类实现的内部步骤
那么没必要把它公开给整个系统。
使用:
protected
可以表达:
这是继承体系内部的扩展点。
这正好与前面学习的权限修饰符结合起来。
3.4 学生成绩统计案例
假设不同班级都执行:
读取学生数据
↓
统计平均分
↓
执行自定义统计
↓
打印完成
其中:
自定义统计
不同。
可以:
public abstract class StatisticsTask {
public final void execute() {
readStudents();
calculateAverage();
customStatistics();
finish();
}
private void readStudents() {
System.out.println(
"读取学生数据"
);
}
private void calculateAverage() {
System.out.println(
"计算平均分"
);
}
protected abstract void customStatistics();
private void finish() {
System.out.println(
"统计完成"
);
}
}
四、原理与进阶
4.1 模板方法是一种“控制反转”的雏形
普通调用:
子类
↓
调用父类工具方法
Template Method:
外部
↓
调用父类模板
↓
父类控制整个流程
↓
父类在指定位置调用子类实现
也就是:
总体控制权在父类,子类只提供变化步骤。
这种思想以后学习:
框架
Spring
回调
插件式设计
时会不断遇到。
当前阶段只需要理解这种方向变化,不需要提前学习框架。
4.2 好莱坞原则的直觉
模板方法常被用一个形象思想描述:
不要调用我们,我们会调用你。
这里不是让子类自己控制:
什么时候执行 read
什么时候执行 convert
什么时候执行 save
而是:
父类模板
决定时间和顺序。
子类只提供:
convert 的具体实现
这就是框架式设计的一种非常早期的直觉。
4.3 不是所有重复代码都需要 Template Method
例如:
public int add(
int a,
int b
) {
return a + b;
}
三个类都需要加法。
最自然可能只是:
抽取公共方法
没有必要:
创建抽象类
创建继承体系
创建抽象步骤
模板方法真正适合的是:
存在稳定整体流程
+
多个实现共享流程结构
+
部分步骤存在可预测变化
4.4 为什么不能为了模式而模式
如果只有:
一个实现类
而且流程未来也没有变化需求,却设计:
AbstractProcessor
BaseProcessor
TemplateProcessor
ConcreteProcessor
可能只是增加复杂度。
所以设计模式最重要的不是:
“我用了 Template Method,很高级。”
而是:
当前问题真的具有固定流程与可变步骤吗?
4.5 模板方法与普通父类复用的区别
普通父类复用可能只是:
protected void read() {
}
子类自行:
read();
convert();
save();
而模板方法进一步:
public final void execute() {
read();
convert();
save();
}
区别在于:
普通复用:
只复用单个功能
模板方法:
连整个算法执行骨架也统一
4.6 模板方法不一定只有一个抽象步骤
简单教材结构:
模板方法
+
一个抽象方法
只是最基础版本。
实际可以:
public final void execute() {
read();
validate();
transform();
save();
}
其中:
protected abstract void validate();
protected abstract void transform();
两个步骤都由不同子类实现。
关键不是抽象方法数量,而是:
哪些步骤稳定,哪些步骤变化。
4.7 模板方法本身是不是绝对必须 final
在原课程当前结构中:
建议使用 final。
原因非常明确:
防止子类重写后破坏算法骨架。
因此本教程训练代码都采用:
public final void templateMethod()
但是从更广义的设计模式讨论看,是否强制 final 与具体框架设计有关。
当前课程阶段最合理的掌握方式是:
当整个流程不允许子类改变时,使用 final 明确保护模板方法。
不要机械背成:
世界上所有叫“模板方法”的方法语法上都必须 final。
4.8 可变步骤为什么设计成 abstract
如果写:
protected void convert() {
}
空实现。
子类可能忘记重写。
程序依然编译。
而:
protected abstract void convert();
会直接告诉编译器:
具体非抽象子类必须解决这个步骤。
因此 abstract 不只是减少代码。
它还能:
通过类型系统强制实现者完成必要步骤。
五、实践应用
5.1 数据导入
read
validate
convert
save
非常典型。
固定:
read
validate
save
变化:
convert
适合 Template Method。
5.2 文件处理
打开文件
↓
读取数据
↓
解析数据
↓
输出处理结果
↓
关闭文件
如果:
解析方式
因文件格式不同而变化,也可以考虑模板方法。
不过真实资源管理还涉及 IO 与异常,后面学习后再做完整版本。
5.3 报表生成
统一:
读取数据
↓
计算统计
↓
生成报表主体
↓
保存文件
不同报表:
日报
周报
月报
可能只改变:
统计算法
报表主体
同样适合分析固定与变化。
5.4 自动化测试流程
例如概念上:
准备测试环境
↓
执行测试
↓
收集结果
↓
清理环境
不同测试用例只实现:
执行测试
这也是类似的模板思想。
六、常见问题
6.1 什么是模板方法设计模式?
在父类中定义固定算法流程,把变化步骤交给子类实现的一种设计模式。
6.2 模板方法主要解决什么问题?
当前课程重点:
- 重复流程代码;
- 多个子类重复控制同一流程;
- 固定流程容易被写乱;
- 可变步骤与固定步骤职责混在一起。
6.3 模板方法通常放在哪里?
通常放:
父类 / 抽象父类
由父类控制整体流程。
6.4 什么是固定步骤?
所有实现都遵循相同逻辑的步骤。
可以由父类直接提供实现。
6.5 什么是可变步骤?
流程中必须存在,但不同子类实现方式不同的步骤。
可以设计为:
abstract
方法。
6.6 子类主要负责什么?
主要实现:
差异化步骤。
而不是重复控制整个公共流程。
6.7 模板方法为什么建议 final?
当流程不允许改变时:
final
可以阻止子类重写模板方法,从而保护算法骨架。
6.8 abstract step 为什么不直接写空方法?
因为:
abstract
能够强制具体子类提供实现。
空方法则可能让子类忘记实现却仍然正常编译。
6.9 模板方法一定只有一个抽象方法吗?
不一定。
可以根据变化点存在多个抽象步骤。
6.10 Template Method 只是为了少写代码吗?
不是。
它还把:
流程控制权
集中到父类,并限制子类只改变允许变化的部分。
6.11 模板方法和继承有什么关系?
模板父类通过继承让子类获得固定流程,同时让子类重写变化步骤。
6.12 模板方法和多态有什么关系?
父类模板内部调用抽象步骤时,运行时会根据实际子类对象执行相应重写实现。
6.13 所有代码重复都应该用 Template Method 吗?
不是。
只有存在:
稳定流程骨架
+
可变步骤
+
合理继承关系
时才值得考虑。
6.14 模板方法必须写 abstract class 吗?
本课程采用:
抽象类 + 抽象步骤
这一经典结构。
它非常适合当前案例。
后续学习接口、组合等技术后,还会看到其他组织变化行为的方法。
七、练习与验收
7.1 知识问答
- 什么是模板方法设计模式?
- 它主要解决什么问题?
- 什么叫固定流程?
- 什么叫可变步骤?
- 为什么模板方法通常写在父类?
- 为什么父类适合控制算法骨架?
- 为什么变化步骤适合设计成抽象方法?
- 子类在 Template Method 中主要承担什么职责?
- 为什么建议使用 final 修饰模板方法?
- 如果子类重写模板方法可能产生什么问题?
- Template Method 和抽象类是什么关系?
- Template Method 和方法重写是什么关系?
- Template Method 和多态是什么关系?
- 模板方法内部为什么能够执行子类重写实现?
- 模板方法一定只有一个 abstract step 吗?
- fixed step 可以设计成 private 吗?
- variable step 为什么可能使用 protected?
- 模板方法只是为了减少代码量吗?
- 什么情况下普通方法抽取已经足够?
- 为什么不能为了“使用设计模式”而强行创建继承层次?
7.2 代码阅读
禁止运行:
abstract class Processor {
public final void process() {
step1();
step2();
step3();
}
private void step1() {
System.out.println("step1");
}
protected abstract void step2();
private void step3() {
System.out.println("step3");
}
}
class AProcessor extends Processor {
@Override
protected void step2() {
System.out.println("A step2");
}
}
class BProcessor extends Processor {
@Override
protected void step2() {
System.out.println("B step2");
}
}
回答:
- 哪个方法是模板方法?
- 哪些步骤固定?
- 哪个步骤变化?
- 为什么
step2()是 abstract? - 为什么
process()使用 final? - 创建 AProcessor 后调用
process(),执行哪些方法? - 创建 BProcessor 后有什么不同?
继续分析:
Processor processor =
new AProcessor();
processor.process();
回答:
processor的编译时类型是什么?- 实际对象是什么?
process()定义在哪个类?- 运行到
step2()时为什么执行 AProcessor 的实现? - 这里使用了什么多态机制?
7.3 手写代码
任务一:DataImporter
按照:
read
validate
convert
save
设计:
DataImporter
├── CsvImporter
└── ExcelImporter
要求:
- 整体流程由模板方法统一控制;
- 模板方法使用 final;
read()、validate()、save()使用父类固定实现;convert()为变化步骤;- CsvImporter、ExcelImporter 分别实现不同转换。
任务二:ReportGenerator
流程:
加载数据
↓
统计数据
↓
生成正文
↓
输出报表
设计:
DailyReportGenerator
WeeklyReportGenerator
要求自行判断:
- 哪些步骤固定;
- 哪些步骤变化;
- 哪些方法 private;
- 哪些方法 protected abstract;
- 哪个方法 final。
任务三:文章发布流程
流程:
检查标题
↓
处理正文
↓
生成摘要
↓
发布
不同文章类型:
TutorialArticle
BlogArticle
其中:
处理正文
方式不同。
使用 Template Method 设计。
7.4 Debug
下面代码的问题是什么?
abstract class DataImporter {
public void importData() {
read();
convert();
save();
}
protected abstract void convert();
private void read() {
}
private void save() {
}
}
class CsvImporter extends DataImporter {
@Override
public void importData() {
saveCsvDirectly();
}
@Override
protected void convert() {
}
private void saveCsvDirectly() {
}
}
已知业务要求:
所有导入器必须严格:
read → convert → save
回答:
- 当前模板方法最大设计漏洞是什么?
- CsvImporter 为什么能够绕开固定流程?
- 应该使用什么关键字保护模板方法?
- 修复代码。
下面代码:
abstract class DataImporter {
public final void importData() {
convert();
}
protected void convert() {
}
}
已知:
每一种 importer 都必须提供自己的转换规则。
回答:
- 当前子类是否可以忘记重写 convert?
- 为什么?
- convert 更适合声明成什么?
- 修改后对子类产生什么编译期约束?
7.5 综合训练
设计一个:
DataImporter
模板系统。
固定整体流程:
1. read
2. validate
3. convert
4. save
业务要求:
CSVImporter:
CSV 转换规则
ExcelImporter:
Excel 转换规则
JsonImporter:
JSON 转换规则
要求:
- 设计抽象父类。
- 模板方法命名为:
importData()
importData()必须保护流程不被子类改写。- 公共固定步骤放父类。
- 可变步骤使用 abstract。
- 三个子类只实现变化逻辑。
- 使用:
DataImporter importer
分别接收三个子类。 8. 调用同一个:
importer.importData();
- 画出其中一次完整方法调用链。
最后回答:
Template Method 到底把“控制权”放在父类还是子类?子类真正拥有的变化权又在哪里?
7.6 本章验收
关闭资料,确认自己能够:
- [ ] 解释 Template Method 解决的问题。
- [ ] 从重复代码中识别公共流程。
- [ ] 区分固定步骤与可变步骤。
- [ ] 把固定流程写成模板方法。
- [ ] 使用 abstract 声明变化步骤。
- [ ] 让不同子类只实现变化步骤。
- [ ] 使用 final 保护固定流程。
- [ ] 解释父类模板为什么可以调用子类实现。
- [ ] 把多态、重写、抽象类与模板方法连起来。
- [ ] 从零完成 DataImporter。
- [ ] 判断什么时候没必要使用模板方法。
- [ ] 解释“模式不是为了炫技,而是为了适配真实重复问题”。
如果你只会背:
模板方法
+
抽象方法
+
final
却不能从一段真实重复代码中找出:
哪些东西应该固定?
哪些东西应该变化?
谁负责控制整体流程?
那么 Template Method 还没有真正掌握。