设计模式:模板方法模式

欢迎你来读这篇博客,这篇博客主要是关于模板方法模式
其中包括模板方法模式的核心思想、适用场景、钩子方法、优缺点、与策略模式/责任链模式/工厂方法模式的区别,以及 Java 后端开发中数据导入流程的完整案例。

序言

在 Java 后端开发中,我们经常会遇到这种情况:

多个业务流程整体步骤差不多,但其中某几个步骤的具体实现不一样。

比如数据导入。

无论是导入用户数据、订单数据,还是商品数据,整体流程可能都差不多:

1
2
3
4
5
6
7
8
检查文件
读取文件
解析数据
校验数据
转换对象
保存数据
记录导入日志
返回导入结果

但不同导入任务的细节不同:

  • 用户导入解析的是用户字段;
  • 订单导入解析的是订单字段;
  • 商品导入解析的是商品字段;
  • 每种数据校验规则不同;
  • 每种数据保存逻辑不同。

如果每个导入类都完整写一遍流程,代码会大量重复。

如果把所有导入逻辑都塞进一个类,再用 if else 判断导入类型,代码也会越来越乱。

模板方法模式就是为了解决这类问题:

在父类中定义算法骨架,把某些具体步骤延迟到子类实现。

简单说:

模板方法模式就是父类定流程,子类填细节。

它特别适合那些“流程固定,但步骤可变”的场景。

这有点像做饭:

1
准备食材 -> 清洗 -> 烹饪 -> 装盘

这个流程很稳定。

但是做红烧肉和做番茄炒蛋,具体食材和烹饪方式不一样。

模板方法模式就是把“做饭流程”固定下来,让具体菜品自己决定某些步骤怎么做。

当然,别把流程定得太死。模板方法用不好,就像领导给了你一个“创新模板”:每一步都写死,还要求你自由发挥。

正文

chapter 1:什么是模板方法模式

模板方法模式,英文是 Template Method Pattern,属于行为型设计模式。

它的定义是:

在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。

这个定义里有几个关键词:

  1. 算法骨架
  2. 步骤延迟到子类
  3. 不改变流程结构
  4. 子类重写部分步骤

在 Java 中,模板方法模式通常通过抽象类实现。

父类中定义一个模板方法:

1
2
3
4
5
public final void execute() {
step1();
step2();
step3();
}

某些步骤由父类实现:

1
2
3
protected void step1() {
// 通用逻辑
}

某些步骤由子类实现:

1
protected abstract void step2();

模板方法通常使用 final 修饰,防止子类修改流程顺序。

1
2
public final void execute() {
}

这就是模板方法模式的核心。

chapter 2:模板方法模式解决什么问题

模板方法模式主要解决的是:

多个类有相同的流程骨架,但部分步骤实现不同。

例如:

1. 数据导入流程

1
上传文件 -> 解析文件 -> 校验数据 -> 保存数据 -> 返回结果

不同导入类型解析和保存逻辑不同。

2. 支付回调处理流程

1
验签 -> 解析参数 -> 查询订单 -> 更新状态 -> 记录日志 -> 返回响应

支付宝、微信、Stripe 的验签和参数不同,但流程相似。

3. 文件导出流程

1
查询数据 -> 转换格式 -> 生成文件 -> 上传文件 -> 返回下载地址

Excel、CSV、PDF 的生成方式不同。

4. 任务执行流程

1
初始化 -> 执行任务 -> 处理异常 -> 清理资源 -> 记录日志

不同任务执行逻辑不同。

5. 消息消费流程

1
反序列化 -> 幂等校验 -> 业务处理 -> ACK -> 异常处理

不同消息业务处理不同。

这些场景的共同点是:

流程稳定,步骤变化。

这就是模板方法模式的主场。

chapter 3:不用模板方法模式会怎样

假设我们有用户导入和订单导入。

用户导入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class UserImportService {

public void importData(String filePath) {
System.out.println("检查文件:" + filePath);

System.out.println("读取用户文件");

System.out.println("解析用户数据");

System.out.println("校验用户数据");

System.out.println("保存用户数据");

System.out.println("记录导入日志");
}
}

订单导入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class OrderImportService {

public void importData(String filePath) {
System.out.println("检查文件:" + filePath);

System.out.println("读取订单文件");

System.out.println("解析订单数据");

System.out.println("校验订单数据");

System.out.println("保存订单数据");

System.out.println("记录导入日志");
}
}

你会发现很多流程是重复的:

1
2
3
4
5
6
检查文件
读取文件
解析数据
校验数据
保存数据
记录日志

只是某些具体步骤不同。

如果继续新增商品导入、库存导入、供应商导入,就会复制更多代码。

一旦流程要增加一步,比如“导入前记录开始时间”,所有导入类都要改。

模板方法模式可以把通用流程抽到父类中:

1
AbstractImportTemplate

具体导入类只实现变化步骤。

chapter 4:模板方法模式的核心结构

模板方法模式通常有两个角色:

  1. AbstractClass 抽象类:定义模板方法和通用步骤。
  2. ConcreteClass 具体子类:实现抽象步骤。

结构如下:

1
2
3
4
5
6
7
8
9
AbstractClass
│ templateMethod()
│ step1()
│ step2()
│ step3()


ConcreteClassA
ConcreteClassB

父类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public abstract class AbstractTemplate {

public final void templateMethod() {
step1();
step2();
step3();
}

protected void step1() {
System.out.println("通用步骤 1");
}

protected abstract void step2();

protected void step3() {
System.out.println("通用步骤 3");
}
}

子类:

1
2
3
4
5
6
7
public class ConcreteTemplate extends AbstractTemplate {

@Override
protected void step2() {
System.out.println("子类实现步骤 2");
}
}

客户端:

1
2
3
AbstractTemplate template = new ConcreteTemplate();

template.templateMethod();

这里的关键点是:

客户端调用的是父类模板方法,流程由父类控制,细节由子类扩展。

chapter 5:模板方法为什么通常用 final

模板方法通常建议用 final 修饰:

1
2
public final void execute() {
}

原因是:

模板方法定义的是流程骨架,不希望子类随意改流程。

如果子类可以重写模板方法,就可能破坏父类定义的流程。

例如:

1
2
3
4
5
@Override
public void execute() {
saveData();
validateData();
}

原本应该先校验再保存,结果子类改成先保存再校验。

这就很危险。

所以模板方法一般是:

1
2
3
4
5
6
7
8
public final void importData(String filePath) {
checkFile(filePath);
readFile(filePath);
parseData();
validateData();
saveData();
recordLog();
}

子类只能重写父类允许变化的步骤。

这是一种“受控扩展”。

父类像导演,子类像演员。

演员可以演不同角色,但不能擅自把第三幕演到第一幕前面。

chapter 6:什么是钩子方法

模板方法模式里还有一个常见概念:钩子方法,Hook Method。

钩子方法是父类提供的可选扩展点。

它通常有默认实现,子类可以选择重写,也可以不重写。

例如:

1
2
3
protected boolean needValidate() {
return true;
}

模板方法中使用:

1
2
3
if (needValidate()) {
validateData();
}

子类可以重写:

1
2
3
4
@Override
protected boolean needValidate() {
return false;
}

钩子方法的作用是:

在不改变模板流程的前提下,允许子类对某些流程开关或扩展点进行控制。

常见钩子方法包括:

  • 是否执行某个步骤;
  • 某个步骤执行前后的扩展;
  • 默认空实现;
  • 允许子类补充行为。

例如:

1
2
protected void beforeImport() {
}
1
2
protected void afterImport() {
}

父类默认不做事。

子类需要时再重写。

chapter 7:案例背景:数据导入模板

下面用一个 Java 后端常见案例来讲模板方法模式:数据导入流程。

假设系统需要支持多种数据导入:

  • 用户导入;
  • 订单导入;
  • 商品导入。

它们的整体流程一致:

1
2
3
4
5
6
7
8
1. 检查文件
2. 读取文件
3. 解析数据
4. 校验数据
5. 转换数据
6. 保存数据
7. 导入后处理
8. 返回结果

不同点在于:

  • 解析逻辑不同;
  • 校验规则不同;
  • 转换对象不同;
  • 保存方式不同;
  • 后处理不同。

我们使用模板方法模式来实现。

chapter 8:定义导入结果 ImportResult

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ImportResult {

private final int totalCount;

private final int successCount;

private final int failureCount;

private final String message;

public ImportResult(int totalCount, int successCount, int failureCount, String message) {
this.totalCount = totalCount;
this.successCount = successCount;
this.failureCount = failureCount;
this.message = message;
}

public int getTotalCount() {
return totalCount;
}

public int getSuccessCount() {
return successCount;
}

public int getFailureCount() {
return failureCount;
}

public String getMessage() {
return message;
}
}

chapter 9:定义导入上下文 ImportContext

导入过程中需要保存一些上下文数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.util.HashMap;
import java.util.Map;

public class ImportContext {

private final String filePath;

private String fileContent;

private final Map<String, Object> attributes = new HashMap<>();

public ImportContext(String filePath) {
this.filePath = filePath;
}

public String getFilePath() {
return filePath;
}

public String getFileContent() {
return fileContent;
}

public void setFileContent(String fileContent) {
this.fileContent = fileContent;
}

public void put(String key, Object value) {
attributes.put(key, value);
}

@SuppressWarnings("unchecked")
public <T> T get(String key) {
return (T) attributes.get(key);
}
}

上下文用于在模板流程的多个步骤之间传递数据。

例如:

  • 文件内容;
  • 解析后的行数据;
  • 转换后的业务对象;
  • 错误信息;
  • 导入统计。

chapter 10:定义抽象导入模板 AbstractDataImportTemplate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import java.util.List;

public abstract class AbstractDataImportTemplate<T> {

public final ImportResult importData(String filePath) {
long start = System.currentTimeMillis();

ImportContext context = new ImportContext(filePath);

try {
beforeImport(context);

checkFile(context);

readFile(context);

List<String> rows = parseRows(context);

if (needValidate()) {
validateRows(rows, context);
}

List<T> dataList = convertRows(rows, context);

int successCount = saveData(dataList, context);

afterImport(context);

return new ImportResult(
rows.size(),
successCount,
rows.size() - successCount,
"导入成功"
);
} catch (Exception e) {
onError(context, e);

return new ImportResult(
0,
0,
0,
"导入失败:" + e.getMessage()
);
} finally {
long cost = System.currentTimeMillis() - start;

System.out.println("导入流程结束,filePath = " + filePath + ",cost = " + cost + " ms");
}
}

protected void beforeImport(ImportContext context) {
System.out.println("导入开始,filePath = " + context.getFilePath());
}

protected void checkFile(ImportContext context) {
String filePath = context.getFilePath();

if (filePath == null || filePath.isBlank()) {
throw new IllegalArgumentException("filePath can not be blank");
}

if (!filePath.endsWith(".csv")) {
throw new IllegalArgumentException("only csv file is supported");
}

System.out.println("文件检查通过:" + filePath);
}

protected void readFile(ImportContext context) {
String mockContent = mockReadFile(context.getFilePath());

context.setFileContent(mockContent);

System.out.println("读取文件完成");
}

protected abstract List<String> parseRows(ImportContext context);

protected boolean needValidate() {
return true;
}

protected abstract void validateRows(List<String> rows, ImportContext context);

protected abstract List<T> convertRows(List<String> rows, ImportContext context);

protected abstract int saveData(List<T> dataList, ImportContext context);

protected void afterImport(ImportContext context) {
System.out.println("导入后处理完成");
}

protected void onError(ImportContext context, Exception e) {
System.out.println("导入异常,filePath = " + context.getFilePath()
+ ",error = " + e.getMessage());
}

private String mockReadFile(String filePath) {
if (filePath.contains("user")) {
return "1,Mario,mario@example.com\n2,Luigi,luigi@example.com";
}

if (filePath.contains("order")) {
return "1001,1,99.00\n1002,2,199.00";
}

return "";
}
}

这个抽象类就是模板方法模式的核心。

它定义了固定流程:

1
public final ImportResult importData(String filePath)

其中:

  • checkFile() 是通用步骤;
  • readFile() 是通用步骤;
  • parseRows() 是抽象步骤;
  • validateRows() 是抽象步骤;
  • convertRows() 是抽象步骤;
  • saveData() 是抽象步骤;
  • beforeImport() 是钩子方法;
  • afterImport() 是钩子方法;
  • needValidate() 是钩子方法;
  • onError() 是异常钩子方法。

chapter 11:定义用户导入对象 UserImportDTO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class UserImportDTO {

private final Long userId;

private final String username;

private final String email;

public UserImportDTO(Long userId, String username, String email) {
this.userId = userId;
this.username = username;
this.email = email;
}

public Long getUserId() {
return userId;
}

public String getUsername() {
return username;
}

public String getEmail() {
return email;
}
}

chapter 12:实现用户导入模板 UserImportTemplate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class UserImportTemplate extends AbstractDataImportTemplate<UserImportDTO> {

@Override
protected List<String> parseRows(ImportContext context) {
String content = context.getFileContent();

List<String> rows = Arrays.asList(content.split("\n"));

System.out.println("解析用户数据行数:" + rows.size());

return rows;
}

@Override
protected void validateRows(List<String> rows, ImportContext context) {
for (String row : rows) {
String[] columns = row.split(",");

if (columns.length != 3) {
throw new IllegalArgumentException("用户数据列数不正确:" + row);
}

if (!columns[2].contains("@")) {
throw new IllegalArgumentException("邮箱格式不正确:" + row);
}
}

System.out.println("用户数据校验通过");
}

@Override
protected List<UserImportDTO> convertRows(List<String> rows, ImportContext context) {
List<UserImportDTO> users = new ArrayList<>();

for (String row : rows) {
String[] columns = row.split(",");

users.add(new UserImportDTO(
Long.valueOf(columns[0]),
columns[1],
columns[2]
));
}

System.out.println("用户数据转换完成");

return users;
}

@Override
protected int saveData(List<UserImportDTO> dataList, ImportContext context) {
for (UserImportDTO user : dataList) {
System.out.println("保存用户,userId = " + user.getUserId()
+ ",username = " + user.getUsername()
+ ",email = " + user.getEmail());
}

return dataList.size();
}

@Override
protected void afterImport(ImportContext context) {
System.out.println("用户导入完成,发送用户导入完成通知");
}
}

用户导入只实现自己关心的步骤。

文件检查、读取、异常处理、耗时统计由父类统一处理。

chapter 13:定义订单导入对象 OrderImportDTO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.math.BigDecimal;

public class OrderImportDTO {

private final Long orderId;

private final Long userId;

private final BigDecimal amount;

public OrderImportDTO(Long orderId, Long userId, BigDecimal amount) {
this.orderId = orderId;
this.userId = userId;
this.amount = amount;
}

public Long getOrderId() {
return orderId;
}

public Long getUserId() {
return userId;
}

public BigDecimal getAmount() {
return amount;
}
}

chapter 14:实现订单导入模板 OrderImportTemplate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class OrderImportTemplate extends AbstractDataImportTemplate<OrderImportDTO> {

@Override
protected List<String> parseRows(ImportContext context) {
String content = context.getFileContent();

List<String> rows = Arrays.asList(content.split("\n"));

System.out.println("解析订单数据行数:" + rows.size());

return rows;
}

@Override
protected void validateRows(List<String> rows, ImportContext context) {
for (String row : rows) {
String[] columns = row.split(",");

if (columns.length != 3) {
throw new IllegalArgumentException("订单数据列数不正确:" + row);
}

BigDecimal amount = new BigDecimal(columns[2]);

if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("订单金额必须大于 0:" + row);
}
}

System.out.println("订单数据校验通过");
}

@Override
protected List<OrderImportDTO> convertRows(List<String> rows, ImportContext context) {
List<OrderImportDTO> orders = new ArrayList<>();

for (String row : rows) {
String[] columns = row.split(",");

orders.add(new OrderImportDTO(
Long.valueOf(columns[0]),
Long.valueOf(columns[1]),
new BigDecimal(columns[2])
));
}

System.out.println("订单数据转换完成");

return orders;
}

@Override
protected int saveData(List<OrderImportDTO> dataList, ImportContext context) {
for (OrderImportDTO order : dataList) {
System.out.println("保存订单,orderId = " + order.getOrderId()
+ ",userId = " + order.getUserId()
+ ",amount = " + order.getAmount());
}

return dataList.size();
}

@Override
protected void afterImport(ImportContext context) {
System.out.println("订单导入完成,刷新订单统计缓存");
}
}

订单导入和用户导入流程一样,但细节不同。

这就是模板方法模式非常适合的场景。

chapter 15:客户端使用模板方法模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class TemplateMethodDemo {

public static void main(String[] args) {
AbstractDataImportTemplate<UserImportDTO> userImportTemplate = new UserImportTemplate();

ImportResult userResult = userImportTemplate.importData("user-import.csv");

printResult(userResult);

System.out.println("----------");

AbstractDataImportTemplate<OrderImportDTO> orderImportTemplate = new OrderImportTemplate();

ImportResult orderResult = orderImportTemplate.importData("order-import.csv");

printResult(orderResult);
}

private static void printResult(ImportResult result) {
System.out.println("总数:" + result.getTotalCount());
System.out.println("成功:" + result.getSuccessCount());
System.out.println("失败:" + result.getFailureCount());
System.out.println("消息:" + result.getMessage());
}
}

输出类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
导入开始,filePath = user-import.csv
文件检查通过:user-import.csv
读取文件完成
解析用户数据行数:2
用户数据校验通过
用户数据转换完成
保存用户,userId = 1,username = Mario,email = mario@example.com
保存用户,userId = 2,username = Luigi,email = luigi@example.com
用户导入完成,发送用户导入完成通知
导入流程结束,filePath = user-import.csv,cost = 5 ms
总数:2
成功:2
失败:0
消息:导入成功

客户端只调用:

1
importData(filePath)

但实际执行流程由父类统一控制,细节由子类完成。

chapter 16:模板方法模式中的钩子方法实战

假设某些导入不需要校验。

可以重写:

1
2
3
4
@Override
protected boolean needValidate() {
return false;
}

例如:

1
2
3
4
5
6
7
8
9
10
11
12
public class FastUserImportTemplate extends UserImportTemplate {

@Override
protected boolean needValidate() {
return false;
}

@Override
protected void afterImport(ImportContext context) {
System.out.println("快速用户导入完成,不发送通知");
}
}

这里 needValidate() 就是典型钩子方法。

它让子类可以控制父类流程中的某些分支。

注意:

钩子方法不是让子类推翻整个流程,而是提供有限扩展点。

这就是模板方法模式的精髓:控制变化范围。

chapter 17:模板方法模式中的抽象步骤和具体步骤

模板方法中的步骤一般有三类。

1. 抽象步骤

父类只定义方法,子类必须实现。

1
protected abstract List<T> convertRows(List<String> rows, ImportContext context);

适合每个子类都不同的逻辑。

2. 具体步骤

父类提供默认实现,子类通常不改。

1
2
protected void checkFile(ImportContext context) {
}

适合通用逻辑。

3. 钩子步骤

父类提供默认空实现或默认返回值,子类可选重写。

1
2
protected void beforeImport(ImportContext context) {
}
1
2
3
protected boolean needValidate() {
return true;
}

这三类步骤组合起来,就能形成一个稳定但可扩展的流程。

chapter 18:模板方法模式和继承

模板方法模式基于继承。

这是它的优点,也是它的缺点。

优点是:

  • 父类可以复用通用逻辑;
  • 子类可以重写变化步骤;
  • 流程控制很清晰。

缺点是:

  • 继承层级可能变深;
  • 子类和父类耦合较强;
  • 父类修改可能影响所有子类;
  • Java 单继承限制扩展;
  • 子类容易依赖父类内部细节。

所以使用模板方法模式时,要注意父类设计。

父类不要太复杂。

模板方法不要太多。

否则抽象类会变成一个“业务祖宗类”,所有子类都要看它脸色。

chapter 19:Spring Boot 中落地模板方法模式

在 Spring Boot 中,可以把模板类做成抽象类,把具体实现做成 Bean。

抽象模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public abstract class AbstractDataImportTemplate<T> {

public final ImportResult importData(String filePath) {
// 固定流程
return doImport(filePath);
}

private ImportResult doImport(String filePath) {
// 省略具体流程
return null;
}

protected abstract List<String> parseRows(ImportContext context);

protected abstract void validateRows(List<String> rows, ImportContext context);

protected abstract List<T> convertRows(List<String> rows, ImportContext context);

protected abstract int saveData(List<T> dataList, ImportContext context);
}

具体导入类:

1
2
3
4
5
import org.springframework.stereotype.Component;

@Component
public class UserImportTemplate extends AbstractDataImportTemplate<UserImportDTO> {
}

再定义类型接口:

1
2
3
4
5
6
public interface ImportTemplate {

String type();

ImportResult importData(String filePath);
}

也可以让抽象类实现这个接口:

1
2
3
4
5
6
7
public abstract class AbstractDataImportTemplate<T> implements ImportTemplate {

@Override
public final ImportResult importData(String filePath) {
// 模板方法
}
}

具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.stereotype.Component;

@Component
public class UserImportTemplate extends AbstractDataImportTemplate<UserImportDTO> {

@Override
public String type() {
return "USER";
}

// 实现抽象步骤
}

然后可以用路由器选择不同导入模板。

chapter 20:Spring Boot 模板路由器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Component
public class ImportTemplateRouter {

private final Map<String, ImportTemplate> templateMap;

public ImportTemplateRouter(List<ImportTemplate> templates) {
this.templateMap = templates.stream()
.collect(Collectors.toUnmodifiableMap(ImportTemplate::type, template -> template));
}

public ImportTemplate route(String type) {
ImportTemplate template = templateMap.get(type);

if (template == null) {
throw new IllegalArgumentException("Unsupported import type: " + type);
}

return template;
}
}

应用服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import org.springframework.stereotype.Service;

@Service
public class DataImportService {

private final ImportTemplateRouter importTemplateRouter;

public DataImportService(ImportTemplateRouter importTemplateRouter) {
this.importTemplateRouter = importTemplateRouter;
}

public ImportResult importData(String type, String filePath) {
ImportTemplate template = importTemplateRouter.route(type);

return template.importData(filePath);
}
}

Controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/imports")
public class DataImportController {

private final DataImportService dataImportService;

public DataImportController(DataImportService dataImportService) {
this.dataImportService = dataImportService;
}

@PostMapping
public ImportResult importData(@RequestParam String type,
@RequestParam String filePath) {
return dataImportService.importData(type, filePath);
}
}

这就把模板方法模式和策略/工厂思想结合起来了:

  • 模板方法负责固定流程;
  • Spring Bean 负责具体模板实现;
  • 路由器负责选择具体模板。

真实项目里经常是多种设计模式组合,而不是一个模式单打独斗。

chapter 21:JDK 和框架中的模板方法思想

模板方法模式在 Java 和 Spring 中非常常见。

1. Spring JdbcTemplate

JdbcTemplate 就有很强的模板方法思想。

它封装了 JDBC 的通用流程:

1
2
3
4
5
6
获取连接
创建 Statement
执行 SQL
处理 ResultSet
关闭资源
异常转换

开发者只需要关注变化部分:

1
jdbcTemplate.query(sql, rowMapper);

RowMapper 负责把一行数据转成对象。

通用流程由 JdbcTemplate 控制。

2. Spring TransactionTemplate

TransactionTemplate 封装事务流程:

1
2
3
4
5
开启事务
执行业务逻辑
提交事务
异常回滚
释放资源

调用方只提供业务逻辑。

1
2
3
4
transactionTemplate.execute(status -> {
// 业务代码
return result;
});

3. HttpServlet

Servlet 中的 service() 方法会根据 HTTP 方法分发:

1
2
3
4
doGet()
doPost()
doPut()
doDelete()

子类重写不同方法处理不同请求。

这也有模板方法思想。

4. AbstractApplicationContext refresh

Spring 容器启动过程里很多抽象方法和钩子方法,也体现了模板方法思想。

框架定义流程,子类扩展步骤。

chapter 22:模板方法模式和策略模式的区别

模板方法模式和策略模式都能复用算法,但方式不同。

对比项 模板方法模式 策略模式
复用方式 继承 组合
核心目的 固定流程骨架,子类实现步骤 封装可替换算法
流程是否固定 固定 不一定
扩展方式 子类重写步骤 替换策略对象
灵活性 较低,但流程稳定 较高
示例 数据导入流程 折扣计算策略

一句话区分:

模板方法模式适合“流程固定,步骤有变化”。

策略模式适合“算法整体可替换”。

例如:

1
导入流程:检查文件 -> 解析 -> 校验 -> 保存

流程固定,适合模板方法。

1
VIP 折扣 / 新人折扣 / 满减折扣

算法可替换,适合策略模式。

chapter 23:模板方法模式和责任链模式的区别

对比项 模板方法模式 责任链模式
流程结构 父类固定流程 多个处理器组成链
扩展方式 子类重写步骤 新增 Handler
顺序控制 父类控制 链路配置控制
是否动态调整 不方便 较方便
示例 文件导入模板 下单校验链

模板方法的流程通常固定在代码中。

责任链的处理器可以更灵活地增删和排序。

如果流程稳定,用模板方法。

如果步骤经常增删和调整顺序,用责任链。

chapter 24:模板方法模式和工厂方法模式的区别

模板方法模式和工厂方法模式经常组合使用。

工厂方法模式负责创建对象。

模板方法模式负责固定流程。

例如:

1
2
3
4
5
6
7
8
9
10
public abstract class AbstractProcessor {

public final void process() {
Parser parser = createParser();

parser.parse();
}

protected abstract Parser createParser();
}

这里:

  • process() 是模板方法;
  • createParser() 是工厂方法。

区别如下:

对比项 模板方法模式 工厂方法模式
关注点 算法流程 对象创建
核心方法 templateMethod factoryMethod
子类职责 实现流程步骤 创建具体产品
示例 导入流程 创建不同解析器

两者可以组合,但目的不同。

chapter 25:模板方法模式和建造者模式的区别

对比项 模板方法模式 建造者模式
核心目的 固定流程骨架 构建复杂对象
关注点 行为流程 对象创建过程
常见实现 抽象类 + 子类 Builder 类
示例 导入流程 构建 HTTP 请求对象

模板方法解决“怎么执行流程”。

建造者解决“怎么构建对象”。

chapter 26:模板方法模式和回调的关系

很多现代 Java 代码不一定用继承实现模板方法,而是用回调。

例如:

1
2
3
4
5
public void execute(TaskCallback callback) {
before();
callback.doTask();
after();
}

这也是模板方法思想。

Spring 的很多 Template 类就是“模板 + 回调”的组合。

例如:

1
jdbcTemplate.query(sql, rowMapper);

流程由 JdbcTemplate 固定。

变化部分由 RowMapper 回调提供。

这种方式比继承更灵活。

所以现代工程中,模板方法思想不一定非要写抽象父类。

它也可以通过:

  • 回调;
  • Lambda;
  • 函数式接口;
  • 组合对象;
  • Spring Template 类。

来实现。

chapter 27:模板方法模式的优点

1. 复用通用流程

公共流程写在父类中,避免重复代码。

2. 固定算法骨架

父类控制流程顺序,子类只负责变化部分。

3. 符合开闭原则的一部分

新增具体实现时,可以新增子类,不修改父类流程。

4. 便于统一处理横切逻辑

比如日志、异常、耗时统计、资源释放。

5. 子类扩展点清晰

抽象方法和钩子方法明确告诉子类哪里可以扩展。

6. 适合框架设计

框架定义流程,用户扩展部分步骤。

chapter 28:模板方法模式的缺点

1. 基于继承,耦合较强

子类依赖父类流程。

父类修改可能影响所有子类。

2. 类数量增加

每种具体流程都要一个子类。

3. 流程不够灵活

流程顺序写死在父类中,不适合经常变化的流程。

4. 子类可能被迫实现不需要的方法

如果抽象步骤设计不好,子类会写很多空实现。

5. 父类容易变复杂

抽象模板如果塞太多逻辑,会成为难维护的大类。

chapter 29:适用场景

模板方法模式适合以下场景。

1. 多个类有相同流程

例如导入、导出、回调处理、任务执行。

2. 流程固定,但部分步骤变化

这是最典型的场景。

3. 希望统一控制流程顺序

例如必须先校验,再处理,再保存。

4. 需要统一处理日志、异常、资源释放

这些都可以放在父类模板方法中。

5. 框架扩展点设计

框架定义流程,业务方实现某些方法。

chapter 30:不适合使用的场景

以下场景不建议使用模板方法模式。

1. 流程经常变化

如果步骤顺序经常调整,责任链或流程编排更合适。

2. 各实现之间差异很大

如果只是表面流程相似,实际差异巨大,强行模板会很别扭。

3. 只想替换一个算法

策略模式可能更合适。

4. 不想使用继承

如果团队更倾向组合,可以使用策略、回调或函数式接口。

5. 父类会越来越胖

如果模板父类已经很大,就要谨慎继续加逻辑。

chapter 31:真实项目中的实践建议

1. 模板方法用 final 修饰

避免子类破坏流程骨架。

1
2
public final ImportResult importData(String filePath) {
}

2. 抽象步骤不要太多

抽象方法太多,子类实现成本会很高。

3. 钩子方法要有明确语义

例如:

1
2
3
4
beforeImport()
afterImport()
needValidate()
onError()

不要定义太多含义模糊的钩子。

4. 父类控制流程,子类只处理变化点

不要让子类知道太多父类内部细节。

5. 通用异常处理可以放父类

例如导入失败统一记录日志。

6. 资源释放放 finally

例如文件流、数据库连接、临时文件清理。

7. 如果流程步骤需要动态增删,考虑责任链

不要硬把动态流程塞进模板方法。

8. 如果只是算法替换,考虑策略模式

模板方法不是万能替代 if else。

9. Spring 项目中可以结合 Bean 路由

多种模板实现可以注册成 Bean,然后通过 type 路由。

10. 不要让模板父类变成祖传基类

如果所有业务都继承一个超级模板类,后期维护会非常痛苦。

抽象类不是垃圾场,别什么公共代码都往里面倒。

chapter 32:完整案例代码汇总

导入结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ImportResult {

private final int totalCount;

private final int successCount;

private final int failureCount;

private final String message;

public ImportResult(int totalCount, int successCount, int failureCount, String message) {
this.totalCount = totalCount;
this.successCount = successCount;
this.failureCount = failureCount;
this.message = message;
}

public int getTotalCount() {
return totalCount;
}

public int getSuccessCount() {
return successCount;
}

public int getFailureCount() {
return failureCount;
}

public String getMessage() {
return message;
}
}

导入上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.util.HashMap;
import java.util.Map;

public class ImportContext {

private final String filePath;

private String fileContent;

private final Map<String, Object> attributes = new HashMap<>();

public ImportContext(String filePath) {
this.filePath = filePath;
}

public String getFilePath() {
return filePath;
}

public String getFileContent() {
return fileContent;
}

public void setFileContent(String fileContent) {
this.fileContent = fileContent;
}

public void put(String key, Object value) {
attributes.put(key, value);
}

@SuppressWarnings("unchecked")
public <T> T get(String key) {
return (T) attributes.get(key);
}
}

抽象导入模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import java.util.List;

public abstract class AbstractDataImportTemplate<T> {

public final ImportResult importData(String filePath) {
long start = System.currentTimeMillis();

ImportContext context = new ImportContext(filePath);

try {
beforeImport(context);

checkFile(context);

readFile(context);

List<String> rows = parseRows(context);

if (needValidate()) {
validateRows(rows, context);
}

List<T> dataList = convertRows(rows, context);

int successCount = saveData(dataList, context);

afterImport(context);

return new ImportResult(
rows.size(),
successCount,
rows.size() - successCount,
"导入成功"
);
} catch (Exception e) {
onError(context, e);

return new ImportResult(
0,
0,
0,
"导入失败:" + e.getMessage()
);
} finally {
long cost = System.currentTimeMillis() - start;

System.out.println("导入流程结束,filePath = " + filePath + ",cost = " + cost + " ms");
}
}

protected void beforeImport(ImportContext context) {
System.out.println("导入开始,filePath = " + context.getFilePath());
}

protected void checkFile(ImportContext context) {
String filePath = context.getFilePath();

if (filePath == null || filePath.isBlank()) {
throw new IllegalArgumentException("filePath can not be blank");
}

if (!filePath.endsWith(".csv")) {
throw new IllegalArgumentException("only csv file is supported");
}

System.out.println("文件检查通过:" + filePath);
}

protected void readFile(ImportContext context) {
String mockContent = mockReadFile(context.getFilePath());

context.setFileContent(mockContent);

System.out.println("读取文件完成");
}

protected abstract List<String> parseRows(ImportContext context);

protected boolean needValidate() {
return true;
}

protected abstract void validateRows(List<String> rows, ImportContext context);

protected abstract List<T> convertRows(List<String> rows, ImportContext context);

protected abstract int saveData(List<T> dataList, ImportContext context);

protected void afterImport(ImportContext context) {
System.out.println("导入后处理完成");
}

protected void onError(ImportContext context, Exception e) {
System.out.println("导入异常,filePath = " + context.getFilePath()
+ ",error = " + e.getMessage());
}

private String mockReadFile(String filePath) {
if (filePath.contains("user")) {
return "1,Mario,mario@example.com\n2,Luigi,luigi@example.com";
}

if (filePath.contains("order")) {
return "1001,1,99.00\n1002,2,199.00";
}

return "";
}
}

用户导入 DTO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class UserImportDTO {

private final Long userId;

private final String username;

private final String email;

public UserImportDTO(Long userId, String username, String email) {
this.userId = userId;
this.username = username;
this.email = email;
}

public Long getUserId() {
return userId;
}

public String getUsername() {
return username;
}

public String getEmail() {
return email;
}
}

用户导入模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class UserImportTemplate extends AbstractDataImportTemplate<UserImportDTO> {

@Override
protected List<String> parseRows(ImportContext context) {
String content = context.getFileContent();

List<String> rows = Arrays.asList(content.split("\n"));

System.out.println("解析用户数据行数:" + rows.size());

return rows;
}

@Override
protected void validateRows(List<String> rows, ImportContext context) {
for (String row : rows) {
String[] columns = row.split(",");

if (columns.length != 3) {
throw new IllegalArgumentException("用户数据列数不正确:" + row);
}

if (!columns[2].contains("@")) {
throw new IllegalArgumentException("邮箱格式不正确:" + row);
}
}

System.out.println("用户数据校验通过");
}

@Override
protected List<UserImportDTO> convertRows(List<String> rows, ImportContext context) {
List<UserImportDTO> users = new ArrayList<>();

for (String row : rows) {
String[] columns = row.split(",");

users.add(new UserImportDTO(
Long.valueOf(columns[0]),
columns[1],
columns[2]
));
}

System.out.println("用户数据转换完成");

return users;
}

@Override
protected int saveData(List<UserImportDTO> dataList, ImportContext context) {
for (UserImportDTO user : dataList) {
System.out.println("保存用户,userId = " + user.getUserId()
+ ",username = " + user.getUsername()
+ ",email = " + user.getEmail());
}

return dataList.size();
}

@Override
protected void afterImport(ImportContext context) {
System.out.println("用户导入完成,发送用户导入完成通知");
}
}

订单导入 DTO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.math.BigDecimal;

public class OrderImportDTO {

private final Long orderId;

private final Long userId;

private final BigDecimal amount;

public OrderImportDTO(Long orderId, Long userId, BigDecimal amount) {
this.orderId = orderId;
this.userId = userId;
this.amount = amount;
}

public Long getOrderId() {
return orderId;
}

public Long getUserId() {
return userId;
}

public BigDecimal getAmount() {
return amount;
}
}

订单导入模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class OrderImportTemplate extends AbstractDataImportTemplate<OrderImportDTO> {

@Override
protected List<String> parseRows(ImportContext context) {
String content = context.getFileContent();

List<String> rows = Arrays.asList(content.split("\n"));

System.out.println("解析订单数据行数:" + rows.size());

return rows;
}

@Override
protected void validateRows(List<String> rows, ImportContext context) {
for (String row : rows) {
String[] columns = row.split(",");

if (columns.length != 3) {
throw new IllegalArgumentException("订单数据列数不正确:" + row);
}

BigDecimal amount = new BigDecimal(columns[2]);

if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("订单金额必须大于 0:" + row);
}
}

System.out.println("订单数据校验通过");
}

@Override
protected List<OrderImportDTO> convertRows(List<String> rows, ImportContext context) {
List<OrderImportDTO> orders = new ArrayList<>();

for (String row : rows) {
String[] columns = row.split(",");

orders.add(new OrderImportDTO(
Long.valueOf(columns[0]),
Long.valueOf(columns[1]),
new BigDecimal(columns[2])
));
}

System.out.println("订单数据转换完成");

return orders;
}

@Override
protected int saveData(List<OrderImportDTO> dataList, ImportContext context) {
for (OrderImportDTO order : dataList) {
System.out.println("保存订单,orderId = " + order.getOrderId()
+ ",userId = " + order.getUserId()
+ ",amount = " + order.getAmount());
}

return dataList.size();
}

@Override
protected void afterImport(ImportContext context) {
System.out.println("订单导入完成,刷新订单统计缓存");
}
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class TemplateMethodDemo {

public static void main(String[] args) {
AbstractDataImportTemplate<UserImportDTO> userImportTemplate = new UserImportTemplate();

ImportResult userResult = userImportTemplate.importData("user-import.csv");

printResult(userResult);

System.out.println("----------");

AbstractDataImportTemplate<OrderImportDTO> orderImportTemplate = new OrderImportTemplate();

ImportResult orderResult = orderImportTemplate.importData("order-import.csv");

printResult(orderResult);
}

private static void printResult(ImportResult result) {
System.out.println("总数:" + result.getTotalCount());
System.out.println("成功:" + result.getSuccessCount());
System.out.println("失败:" + result.getFailureCount());
System.out.println("消息:" + result.getMessage());
}
}

chapter 33:一句话总结

模板方法模式的本质是:

在父类中固定算法流程,把变化的步骤交给子类实现,让子类在不改变流程结构的前提下扩展具体行为。

它特别适合:

  • 数据导入;
  • 文件导出;
  • 支付回调处理;
  • 消息消费流程;
  • 任务执行框架;
  • 资源处理流程;
  • 框架扩展点设计。

模板方法模式最重要的不是“写一个抽象类”,而是判断:

这个流程是否稳定,变化点是否清晰。

如果流程稳定,步骤变化,模板方法很合适。

如果流程经常变,步骤要动态组合,责任链可能更合适。

如果只是算法替换,策略模式可能更合适。

好的模板方法像一份清晰 SOP:流程固定,岗位职责明确。

坏的模板方法像祖传规章制度:每一步都写死,谁改谁背锅。

所以使用模板方法模式时,要记住一句话:

父类定骨架,子类填细节;流程要稳定,扩展点要克制。

参考资料

  • Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software.
  • Robert C. Martin. Agile Software Development, Principles, Patterns, and Practices.
  • Joshua Bloch. Effective Java.
  • Martin Fowler. Patterns of Enterprise Application Architecture.
  • Spring Framework Documentation: JdbcTemplate.
  • Spring Framework Documentation: TransactionTemplate.
  • Refactoring Guru: Template Method Pattern.
  • SourceMaking: Template Method Design Pattern.

启示录

富贵岂由人,时会高志须酬。

能成功于千载者,必以近察远。


设计模式:模板方法模式
https://allendericdalexander.github.io/2026/04/01/java/design/23template-method-pattern-blog/
作者
AtLuoFu
发布于
2026年4月1日
许可协议