本文是「设计模式实战解读」系列第三篇。系列文章统一按照 定义 → 痛点场景 → 模式结构 → 核心实现 → 真实应用 → 常见变种 → 优缺点 → 避坑指南 → FAQ 的结构展开,每篇聚焦一个模式讲透。
一句话定义
模板方法模式(Template Method):在父类中定义一个算法的骨架(步骤顺序),把某些步骤的具体实现延迟到子类中。子类可以重写步骤细节,但不能改变整体流程。
归属:行为型模式。
一、没有模板方法时的痛点
假设你在做一个数据同步模块,需要支持多种数据源的全量同步:
java
// MySQL → 目标系统
public class MySQLSyncJob {
public void execute() {
log("开始同步");
Connection conn = DriverManager.getConnection("jdbc:mysql://..."); // 建立连接
ResultSet rs = conn.createStatement().executeQuery("SELECT * FROM orders"); // 拉取数据
List<Map> data = convertToList(rs); // 数据转换
targetApi.batchPush(data); // 推送到目标
conn.close(); // 关闭连接
log("同步完成");
}
}
// PostgreSQL → 目标系统
public class PostgresSyncJob {
public void execute() {
log("开始同步");
Connection conn = DriverManager.getConnection("jdbc:postgresql://..."); // 建立连接
ResultSet rs = conn.createStatement().executeQuery("SELECT * FROM orders"); // 拉取数据
List<Map> data = convertToList(rs); // 数据转换
targetApi.batchPush(data); // 推送到目标
conn.close(); // 关闭连接
log("同步完成");
}
}
// MongoDB → 目标系统
public class MongoSyncJob {
public void execute() {
log("开始同步");
MongoClient client = MongoClients.create("mongodb://..."); // 建立连接
FindIterable<Document> docs = client.getDatabase("db").getCollection("orders").find(); // 拉取
List<Map> data = convertDocs(docs); // 数据转换(不同!)
targetApi.batchPush(data); // 推送到目标
client.close(); // 关闭连接
log("同步完成");
}
}
三段代码的骨架完全相同(日志 → 连接 → 拉取 → 转换 → 推送 → 关闭 → 日志),只是具体步骤的实现不同。这就是典型的代码重复------流程是通用的,差异只在某几步。
问题:
- 骨架被重复了 N 遍,修改一处(比如加个耗时统计)要改 N 个文件
- 流程一致性无法保证------有人忘了关连接,有人忘了打日志
- 难以做统一的异常处理、监控埋点、重试逻辑
二、模式结构
┌───────────────────────────────────┐
│ AbstractSyncJob (抽象父类) │
├───────────────────────────────────┤
│ + execute() ← 模板方法 │ 流程骨架(final 不可重写)
│ # connect() ← 抽象步骤 │ 子类必须实现
│ # fetchData() ← 抽象步骤 │ 子类必须实现
│ # convertData() ← 抽象步骤 │ 子类必须实现
│ # pushData() ← 具体步骤 │ 父类默认实现(子类可选重写)
│ # close() ← 抽象步骤 │ 子类必须实现
│ # beforeExecute() ← 钩子方法 │ 默认空实现(子类可选重写)
│ # afterExecute() ← 钩子方法 │ 默认空实现(子类可选重写)
└───────────────────┬───────────────┘
│
┌───────────┼───────────┐
↓ ↓ ↓
MySQLSyncJob PostgresSyncJob MongoSyncJob
三种方法类型:
- 模板方法(Template Method) :定义流程骨架,通常标记
final防止子类重写 - 抽象步骤(Abstract Step):子类必须实现的变化点
- 钩子方法(Hook):有默认空实现,子类按需重写
三、核心实现
3.1 基础版
java
public abstract class AbstractSyncJob {
// 模板方法:定义流程骨架(不允许子类修改流程顺序)
public final void execute() {
long start = System.currentTimeMillis();
log("同步任务开始");
beforeExecute(); // 钩子:执行前
Object connection = connect(); // 步骤1:建立连接
List<Map<String, Object>> rawData = fetchData(connection); // 步骤2:拉取数据
List<Map<String, Object>> converted = convertData(rawData); // 步骤3:转换数据
pushData(converted); // 步骤4:推送数据
close(connection); // 步骤5:关闭连接
afterExecute(); // 钩子:执行后
long cost = System.currentTimeMillis() - start;
log("同步任务完成,耗时: " + cost + "ms");
}
// 抽象步骤(子类必须实现)
protected abstract Object connect();
protected abstract List<Map<String, Object>> fetchData(Object connection);
protected abstract List<Map<String, Object>> convertData(List<Map<String, Object>> rawData);
protected abstract void close(Object connection);
// 具体步骤(有默认实现,子类可以重写)
protected void pushData(List<Map<String, Object>> data) {
targetApi.batchPush(data);
}
// 钩子方法(默认空实现)
protected void beforeExecute() {}
protected void afterExecute() {}
private void log(String msg) {
System.out.println("[" + getClass().getSimpleName() + "] " + msg);
}
}
3.2 子类实现
java
public class MySQLSyncJob extends AbstractSyncJob {
private final String jdbcUrl;
public MySQLSyncJob(String jdbcUrl) {
this.jdbcUrl = jdbcUrl;
}
@Override
protected Object connect() {
return DriverManager.getConnection(jdbcUrl);
}
@Override
protected List<Map<String, Object>> fetchData(Object connection) {
Connection conn = (Connection) connection;
// 执行 SQL,转成 List<Map>
return JdbcUtils.queryForList(conn, "SELECT * FROM orders");
}
@Override
protected List<Map<String, Object>> convertData(List<Map<String, Object>> rawData) {
// MySQL 的字段名转换逻辑
return rawData.stream()
.map(row -> convertFieldNames(row, "mysql"))
.collect(Collectors.toList());
}
@Override
protected void close(Object connection) {
((Connection) connection).close();
}
}
public class MongoSyncJob extends AbstractSyncJob {
private final String mongoUri;
@Override
protected Object connect() {
return MongoClients.create(mongoUri);
}
@Override
protected List<Map<String, Object>> fetchData(Object connection) {
MongoClient client = (MongoClient) connection;
// MongoDB 查询逻辑
return MongoUtils.findAll(client, "db", "orders");
}
@Override
protected List<Map<String, Object>> convertData(List<Map<String, Object>> rawData) {
// MongoDB 的 _id 转换、嵌套文档展平等
return rawData.stream()
.map(MongoFieldConverter::flatten)
.collect(Collectors.toList());
}
@Override
protected void close(Object connection) {
((MongoClient) connection).close();
}
// 重写钩子:MongoDB 同步前先检查集合是否存在
@Override
protected void beforeExecute() {
checkCollectionExists();
}
}
3.3 关键设计点
execute()标final:防止子类"不小心"改了流程顺序- 抽象步骤用
protected abstract:强制子类实现 - 钩子用
protected(非 abstract):有默认空实现,子类选择性重写 - 模板方法内统一做耗时统计、异常处理------修改一处,所有子类生效
四、真实应用场景
4.1 框架级应用
| 框架 | 模板方法在哪 | 骨架步骤 | 子类扩展点 |
|---|---|---|---|
| Spring | AbstractApplicationContext.refresh() | Bean 生命周期加载流程 | onRefresh() 等钩子 |
| Servlet | HttpServlet.service() | 解析请求类型→分发 | doGet()/doPost() |
| JUnit | TestCase.runBare() | setUp→test→tearDown | setUp()/tearDown() |
| MyBatis | BaseExecutor.query() | 缓存检查→查询→结果处理 | doQuery() |
| Spring MVC | DispatcherServlet.doDispatch() | 请求映射→处理→视图渲染 | HandlerAdapter |
| RocketMQ | DefaultMQPushConsumer | 拉取→过滤→消费→ACK | consumeMessage() |
4.2 业务场景
| 业务 | 骨架(不变的流程) | 变化的步骤 |
|---|---|---|
| 数据同步 | 连接→拉取→转换→推送→关闭 | 连接方式、数据转换逻辑 |
| 导出报告 | 查数据→组装→渲染→写文件 | 渲染引擎(PDF/Excel/HTML) |
| 审批流 | 提交→校验→通知→归档 | 校验规则、通知渠道 |
| 支付 | 参数组装→签名→请求→验签→返回 | 签名算法、请求格式 |
| 连接器执行 | 鉴权→构建请求→发送→解析响应→记录日志 | 鉴权方式、请求构造、响应解析 |
| 消息消费 | 接收→反序列化→校验→处理→ACK | 处理逻辑 |
4.3 连接器 Handler 的模板方法
在 iPaaS 引擎中,每个连接器的执行流程是模板化的:
AbstractConnectorHandler.execute() ← 模板方法
├── 1. resolveAuth() ← 解析鉴权信息(OAuth/APIKey/Basic)
├── 2. buildRequest() ← 构建 HTTP/RPC 请求(子类实现)
├── 3. executeRequest() ← 发送请求(通用 HTTP 客户端)
├── 4. parseResponse() ← 解析响应(子类实现)
├── 5. handleError() ← 错误处理(有默认实现,子类可重写)
└── 6. recordLog() ← 记录执行日志(通用)
新增一个连接器(比如对接飞书),只需要实现 buildRequest() 和 parseResponse(),其他步骤由父类统一保证。这让连接器开发从"写全链路"变成"只写差异"。
五、常见变种
5.1 模板方法 + 策略模式
当变化的步骤可以独立抽象成策略时,可以用组合代替继承:
java
public class SyncJob {
private final DataFetcher fetcher; // 策略1:拉取
private final DataConverter converter; // 策略2:转换
private final DataPusher pusher; // 策略3:推送
// 模板方法(流程固定,步骤委托给策略)
public void execute() {
Object data = fetcher.fetch();
Object converted = converter.convert(data);
pusher.push(converted);
}
}
这种方式比纯继承更灵活------可以自由组合 fetcher/converter/pusher,不需要为每种组合创建子类。
5.2 带回调的模板方法
Java 8+ 可以用 Lambda 代替继承:
java
public class SyncTemplate {
public void execute(
Supplier<Object> connect,
Function<Object, List<Map<String, Object>>> fetch,
UnaryOperator<List<Map<String, Object>>> convert,
Consumer<Object> close
) {
Object conn = connect.get();
try {
List<Map<String, Object>> data = fetch.apply(conn);
List<Map<String, Object>> converted = convert.apply(data);
pushData(converted);
} finally {
close.accept(conn);
}
}
}
// 使用
syncTemplate.execute(
() -> DriverManager.getConnection(url),
conn -> JdbcUtils.queryForList((Connection) conn, sql),
data -> data.stream().map(this::convertFields).collect(toList()),
conn -> ((Connection) conn).close()
);
优势:不需要定义子类,一次性使用更轻量。
5.3 多级模板方法
父类定义大骨架,中间层抽象类定义子骨架:
AbstractHandler
└── AbstractHttpHandler (骨架: buildUrl → addHeaders → sendRequest → parseBody)
├── RestApiHandler
└── GraphQLHandler
└── AbstractMqHandler (骨架: connectBroker → subscribe → consume → ack)
├── RocketMqHandler
└── KafkaHandler
六、优缺点
| 优点 | 缺点 |
|---|---|
| 复用骨架代码,消除重复 | 子类受父类约束,灵活性有限 |
| 流程一致性有保障 | 继承层次深时可读性下降 |
| 统一做横切逻辑(日志/监控/异常) | 模板方法越多,父类越臃肿 |
| 新增变种只需写差异部分 | 不了解父类全貌时容易误用 |
| 子类不能破坏整体流程 | 对组合友好度不如策略模式 |
七、避坑指南
坑 1:模板方法没标 final
不加 final,子类可能"重写"模板方法本身,破坏流程------导致某些步骤被跳过。务必给模板方法加 final。
坑 2:步骤太多导致"超级父类"
当骨架步骤超过 7 个时,父类变得很难理解。解法:把步骤分组,用多级模板(中间层抽象类)或组合模式分解。
坑 3:抽象步骤和钩子的边界不清
- 抽象步骤(abstract):子类必须实现的核心差异
- 钩子(非 abstract + 空实现):子类可选增强的扩展点
如果搞反了------把钩子做成 abstract,子类被迫实现一堆空方法;把核心步骤做成钩子,子类忘了重写导致默认行为不对。
坑 4:子类之间有交叉逻辑
MySQLSyncJob 和 PostgresSyncJob 的 connect() 很像(都是 JDBC),但和 MongoSyncJob 完全不同。这时应该加一个中间层 AbstractJdbcSyncJob,把 JDBC 共性抽上去:
AbstractSyncJob
├── AbstractJdbcSyncJob (共享 JDBC connect/close)
│ ├── MySQLSyncJob
│ └── PostgresSyncJob
└── MongoSyncJob
坑 5:模板方法内异常处理不统一
父类的 execute() 应该统一处理异常(try-catch + 清理资源),不要让子类各自处理。否则某些子类忘了 close 连接,就会泄漏。
八、常见问题(FAQ)
Q:模板方法和策略模式什么时候该用哪个?
A:如果变化的只是某一步的算法选择 ,用策略模式(组合);如果有多个步骤需要变化且这些步骤之间有顺序依赖,用模板方法(继承)。实际项目中经常结合使用------模板方法控制流程骨架,每一步内部可以委托给策略。
Q:模板方法的子类可以改变步骤顺序吗?
A:不应该改变。模板方法的价值就在于"流程不变,细节可变"。如果流程本身也需要变化,应该用策略模式或责任链模式。
Q:如何避免继承层次太深?
A:① 控制在 2-3 层以内;② 用 Lambda 回调代替子类(Java 8+ 场景);③ 用组合(策略)代替继承。如果发现继承超过 3 层,通常意味着需要重构。
Q:Spring 里大量使用模板方法吗?
A:是的。Spring 的核心设计哲学就是"框架控制流程,开发者填充细节"。JdbcTemplate.execute()、AbstractApplicationContext.refresh()、RestTemplate.doExecute() 都是模板方法。用 Spring 的人每天都在用模板方法,只是很多人没意识到。
Q:什么时候应该把步骤做成钩子而不是抽象方法?
A:当这个步骤"大部分子类不需要重写"时设为钩子(有默认空实现)。当这个步骤"每个子类都不一样、必须提供实现"时设为抽象方法。判断标准是"默认不做任何事"是否合理。
九、小结
模板方法模式的核心价值:把不变的流程锁死在父类,把变化的细节交给子类。
三个实践要点:
- 模板方法加 final------锁死流程顺序,防止子类越界
- 区分抽象步骤和钩子------必须实现的用 abstract,可选的给空实现
- 当继承变复杂时切换到组合------用 Lambda/策略代替子类
下一篇我们聊观察者模式------当一个对象状态变化需要通知多个关注方时,如何做到松耦合的事件分发。
标签:#设计模式 #模板方法 #TemplateMethod #行为型模式 #Java #继承 #钩子方法 #Spring #框架设计 #代码复用 #面向对象 #软件工程