模板方法模式(Template Method Pattern)实战指南
一、模板方法模式概述
定义: 在父类中定义一个算法的骨架,将某些步骤的具体实现延迟到子类中。模板方法使得子类可以在不改变算法整体结构的前提下,重新定义算法的某些步骤。
解决什么问题: 多个类有相同的处理流程,但某些步骤的实现不同。如果每个类都独立写一套流程,会导致大量重复代码。
核心思想: 封装不变,扩展可变。 把不变的流程固定在父类,把可变的步骤交给子类实现。
二、模式结构
2.1 角色说明
| 角色 | 职责 | 说明 |
|---|---|---|
| AbstractClass(抽象类) | 定义算法骨架 | 包含一个模板方法和若干基本方法 |
| ConcreteClass(具体子类) | 实现抽象方法和钩子方法 | 是算法顶级逻辑的组成步骤 |
2.2 基本方法的三种类型
| 类型 | 定义 | 特点 |
|---|---|---|
| 抽象方法(Abstract Method) | 由抽象类声明,由具体子类实现 | 子类必须实现,决定算法的关键步骤 |
| 具体方法(Concrete Method) | 由抽象类声明并实现 | 子类可直接继承,也可覆盖 |
| 钩子方法(Hook Method) | 在抽象类中已实现 | 一般为 isXxx() 返回 boolean,用于判断逻辑;或空方法供子类选择性重写 |
2.3 结构伪代码
java
// 1. 抽象类 ------ 定义算法骨架
public abstract class AbstractClass {
// 【模板方法】定义算法骨架,按顺序调用基本方法,通常声明为 final
public final void templateMethod() {
step1(); // 【具体方法】父类已实现,子类继承即可
if (isNeedStep2()) { // 【钩子方法】用于判断,子类可选择性重写
step2(); // 【抽象方法】由子类实现
}
step3(); // 【抽象方法】由子类实现
hook(); // 【钩子方法】空方法,子类可选择性重写
}
// 【具体方法】父类已实现
private void step1() {
System.out.println("固定步骤1:初始化");
}
// 【抽象方法】子类必须实现
protected abstract void step2();
// 【抽象方法】子类必须实现
protected abstract void step3();
// 【钩子方法】默认返回 true,子类可重写改变流程
protected boolean isNeedStep2() {
return true;
}
// 【钩子方法】空方法,子类可选择性重写
protected void hook() {
}
}
// 2. 具体子类 A
public class ConcreteClassA extends AbstractClass {
@Override
protected void step2() {
System.out.println("子类A:实现步骤2");
}
@Override
protected void step3() {
System.out.println("子类A:实现步骤3");
}
// isNeedStep2() 和 hook() 继承父类默认实现
}
// 3. 具体子类 B
public class ConcreteClassB extends AbstractClass {
@Override
protected void step2() {
System.out.println("子类B:实现步骤2");
}
@Override
protected void step3() {
System.out.println("子类B:实现步骤3");
}
@Override
protected boolean isNeedStep2() {
return false; // 重写钩子方法,跳过步骤2
}
@Override
protected void hook() {
System.out.println("子类B:重写了钩子方法");
}
}
// 4. 客户端调用
AbstractClass objA = new ConcreteClassA();
objA.templateMethod(); // 输出:固定步骤1 → 子类A步骤2 → 子类A步骤3
AbstractClass objB = new ConcreteClassB();
objB.templateMethod(); // 输出:固定步骤1 → 子类B步骤3 → 子类B钩子
三、优缺点
优点
- 消除重复代码: 不变的流程固定在父类,子类只需关注差异化的步骤。
- 符合开闭原则: 新增子类无需修改父类算法骨架,只需实现或重写基本方法。
- 流程可控: 父类通过模板方法统一控制算法的执行顺序和条件。
- 钩子方法提供灵活性: 子类可通过重写钩子方法影响算法流程,而无需修改算法结构。
缺点
- 类数量增多: 每增加一种变体就需要新增一个子类。
- 继承关系强耦合: 子类和父类之间通过继承紧密绑定,父类的修改可能影响所有子类。
- 灵活性受限: 算法骨架固定在父类中,运行时无法动态切换流程。
四、适用场景
- 流程固定,步骤可变: 多个类有相同的整体流程,但某些步骤的实现不同。
- 代码重复: 多个类中存在重复的流程控制代码,只是具体处理逻辑不同。
- 需要约束子类: 希望子类实现某些步骤,但不允许改变算法的整体执行顺序。
- 典型场景: 数据导入导出、支付流程、测试框架(JUnit 的
setUp()→runTest()→tearDown())、Servlet 的doGet()/doPost()等。
五、实战分析
5.1 JdbcTemplate(Spring 经典案例)
问题背景
JDBC 原生代码中,每次执行 SQL 都需要重复编写:获取连接 → 创建 Statement → 设置参数 → 执行 SQL → 处理 ResultSet → 关闭资源。这些流程是固定的,但「获取连接的方式」和「对查询结果的处理」是可变的。Spring 的 JdbcTemplate 用模板方法模式解决了这个问题。
模板方法(算法骨架)
Spring 源码中 JdbcTemplate 的核心模板方法 execute():
java
// 【抽象类】JdbcTemplate ------ 定义算法骨架
public class JdbcTemplate {
// 【模板方法】定义了 JDBC 操作的完整流程,按固定顺序调用各步骤
public <T> T execute(StatementCallback<T> action) {
// 【具体方法】步骤1:获取数据库连接(父类已实现)
Connection con = DataSourceUtils.getConnection(obtainDataSource());
Statement stmt = null;
try {
// 【具体方法】步骤2:创建 Statement(父类已实现)
stmt = con.createStatement();
// 【具体方法】步骤3:设置查询超时(父类已实现)
applyStatementSettings(stmt);
// 【抽象方法】步骤4:执行 SQL ------ 由子类/回调实现
T result = action.doInStatement(stmt);
// 【具体方法】步骤5:处理结果(父类已实现)
handleResults(stmt);
return result;
} catch (SQLException ex) {
// 【具体方法】步骤6:异常处理(父类已实现)
throw translateException("StatementCallback", sql, ex);
} finally {
// 【具体方法】步骤7:关闭资源(父类已实现)
JdbcUtils.closeStatement(stmt);
DataSourceUtils.releaseConnection(con, getDataSource());
}
}
}
角色对应:
JdbcTemplate= 抽象类(虽然不是 abstract 修饰,但承担抽象类角色)execute()= 模板方法(定义算法骨架,按顺序调用各步骤)con.createStatement()/applyStatementSettings()/handleResults()= 具体方法(父类已实现)action.doInStatement(stmt)= 抽象方法(由调用方通过回调实现)translateException()= 具体方法(父类已实现的异常处理)
子类如何使用(回调方式)
JdbcTemplate 没有通过继承来扩展,而是通过回调接口让调用方定义差异化的步骤:
java
// 查询操作 ------ 调用方实现【抽象方法】doInStatement
List<User> users = jdbcTemplate.execute((Statement stmt) -> {
ResultSet rs = stmt.executeQuery("SELECT * FROM user");
List<User> list = new ArrayList<>();
while (rs.next()) {
list.add(mapRowToUser(rs)); // 调用方定义如何处理每行数据
}
return list;
});
// 更新操作 ------ 另一个调用方实现【抽象方法】doInStatement
int rows = jdbcTemplate.execute((Statement stmt) -> {
return stmt.executeUpdate("UPDATE user SET name = 'test' WHERE id = 1");
});
整体流程
调用方调用 jdbcTemplate.execute(回调)
↓
【模板方法】execute() 开始
↓
【具体方法】获取数据库连接
↓
【具体方法】创建 Statement
↓
【抽象方法】回调.doInStatement(stmt) ← 调用方实现差异化逻辑
↓
【具体方法】处理结果 / 异常处理 / 关闭资源
↓
返回结果
5.2 支付流程(业务实战案例)
问题背景
电商/金融系统中,支付流程通常包含:创建支付订单 → 调用支付渠道 → 处理回调 → 更新订单状态 → 发送通知。这些步骤的顺序是固定的,但「调用支付渠道」和「处理回调」的具体逻辑因渠道不同而不同(微信支付、支付宝支付、银联支付等)。
模板方法(算法骨架)
java
// 【抽象类】AbstractPayService ------ 定义支付流程骨架
public abstract class AbstractPayService {
/**
* 【模板方法】支付流程骨架,声明为 final 防止子类修改流程顺序
* 固定步骤:验证 → 创建订单 → 调用支付 → 处理回调 → 更新状态 → 通知
*/
public final PayResult pay(PayRequest request) {
// 【具体方法】步骤1:参数校验(父类已实现,子类继承即可)
validateRequest(request);
// 【抽象方法】步骤2:创建支付订单 ------ 子类必须实现
PayOrder order = createPayOrder(request);
// 【抽象方法】步骤3:调用支付渠道 ------ 子类必须实现(核心差异点)
PayResponse response = doPay(order);
// 【钩子方法】步骤4:是否需要处理回调 ------ 子类可选择性重写
if (needHandleCallback()) {
handleCallback(response);
}
// 【具体方法】步骤5:更新订单状态(父类已实现)
updateOrderStatus(order, response);
// 【钩子方法】步骤6:发送通知 ------ 子类可选择性重写
afterPayNotify(order, response);
// 【具体方法】步骤7:返回结果(父类已实现)
return buildResult(order, response);
}
// ==================== 基本方法 ====================
// 【具体方法】参数校验 ------ 父类已实现
private void validateRequest(PayRequest request) {
if (request == null || request.getAmount() == null) {
throw new ServiceException("支付参数不能为空");
}
if (request.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new ServiceException("支付金额必须大于0");
}
}
// 【抽象方法】创建支付订单 ------ 子类必须实现
protected abstract PayOrder createPayOrder(PayRequest request);
// 【抽象方法】调用支付渠道 ------ 子类必须实现(核心差异点)
protected abstract PayResponse doPay(PayOrder order);
// 【具体方法】处理回调 ------ 父类已实现
protected void handleCallback(PayResponse response) {
log.info("支付回调处理完成,交易号:{}", response.getTransactionNo());
}
// 【具体方法】更新订单状态 ------ 父类已实现
private void updateOrderStatus(PayOrder order, PayResponse response) {
if (response.isSuccess()) {
order.setStatus(PayStatus.PAID);
} else {
order.setStatus(PayStatus.FAILED);
}
orderService.updateById(order);
}
// 【钩子方法】是否需要处理回调 ------ 默认返回 true,子类可重写
protected boolean needHandleCallback() {
return true;
}
// 【钩子方法】支付后通知 ------ 默认空实现,子类可选择性重写
protected void afterPayNotify(PayOrder order, PayResponse response) {
}
// 【具体方法】构建返回结果 ------ 父类已实现
private PayResult buildResult(PayOrder order, PayResponse response) {
PayResult result = new PayResult();
result.setOrderId(order.getId());
result.setSuccess(response.isSuccess());
result.setMessage(response.getMessage());
return result;
}
}
角色对应:
AbstractPayService= 抽象类(定义支付流程骨架)pay()= 模板方法 (final修饰,按固定顺序调用各步骤)validateRequest()/updateOrderStatus()/buildResult()= 具体方法(父类已实现)createPayOrder()/doPay()= 抽象方法(子类必须实现)needHandleCallback()= 钩子方法(返回 boolean,用于条件判断)afterPayNotify()= 钩子方法(空方法,子类可选择性重写)
具体子类实现
java
// 【具体子类】微信支付
@Component("wechatPay")
public class WechatPayService extends AbstractPayService {
@Override
protected PayOrder createPayOrder(PayRequest request) {
PayOrder order = new PayOrder();
order.setOrderNo(generateOrderNo("WX"));
order.setAmount(request.getAmount());
order.setPayType("WECHAT");
return order;
}
@Override
protected PayResponse doPay(PayOrder order) {
// 调用微信支付 API
WechatPayRequest wxRequest = new WechatPayRequest();
wxRequest.setTotalFee(order.getAmount().multiply(BigDecimal.valueOf(100)).intValue());
wxRequest.setOutTradeNo(order.getOrderNo());
WechatPayResponse wxResponse = wechatPayClient.unifiedOrder(wxRequest);
// 转换为通用响应
PayResponse response = new PayResponse();
response.setSuccess("SUCCESS".equals(wxResponse.getReturnCode()));
response.setTransactionNo(wxResponse.getTransactionId());
return response;
}
// 微信支付不需要处理回调通知,重写【钩子方法】返回 false
@Override
protected boolean needHandleCallback() {
return false;
}
}
// 【具体子类】支付宝支付
@Component("alipay")
public class AlipayService extends AbstractPayService {
@Override
protected PayOrder createPayOrder(PayRequest request) {
PayOrder order = new PayOrder();
order.setOrderNo(generateOrderNo("ALI"));
order.setAmount(request.getAmount());
order.setPayType("ALIPAY");
return order;
}
@Override
protected PayResponse doPay(PayOrder order) {
// 调用支付宝 API
AlipayTradePayRequest aliRequest = new AlipayTradePayRequest();
aliRequest.setTotalAmount(order.getAmount().toString());
aliRequest.setOutTradeNo(order.getOrderNo());
AlipayTradePayResponse aliResponse = alipayClient.execute(aliRequest);
// 转换为通用响应
PayResponse response = new PayResponse();
response.setSuccess("10000".equals(aliResponse.getCode()));
response.setTransactionNo(aliResponse.getTradeNo());
return response;
}
// 支付宝支付需要发送通知,重写【钩子方法】
@Override
protected void afterPayNotify(PayOrder order, PayResponse response) {
if (response.isSuccess()) {
// 发送支付成功短信/微信通知
notificationService.sendPaySuccess(order);
}
}
}
调用入口
java
@Service
public class PayService {
@Resource
private Map<String, AbstractPayService> payServiceMap; // Spring 自动注入所有子类
public PayResult pay(String payType, PayRequest request) {
AbstractPayService payService = payServiceMap.get(payType);
if (payService == null) {
throw new ServiceException("不支持的支付方式: " + payType);
}
return payService.pay(request); // 调用模板方法
}
}
// 业务调用
payService.pay("wechatPay", request); // 走微信支付流程
payService.pay("alipay", request); // 走支付宝流程
整体流程
业务方调用 payService.pay("alipay", request)
↓
获取对应的子类 AlipayService
↓
调用模板方法 pay(request)
↓
【具体方法】validateRequest() → 参数校验
↓
【抽象方法】createPayOrder() → 子类实现:创建支付宝订单
↓
【抽象方法】doPay() → 子类实现:调用支付宝 API
↓
【钩子方法】needHandleCallback() → 子类可重写:是否处理回调
↓
【具体方法】updateOrderStatus() → 更新订单状态
↓
【钩子方法】afterPayNotify() → 子类可重写:发送通知
↓
【具体方法】buildResult() → 构建返回结果
↓
返回 PayResult
两者对比总结
| 对比项 | JdbcTemplate | 支付流程 |
|---|---|---|
| 抽象类 | JdbcTemplate |
AbstractPayService |
| 模板方法 | execute() |
pay() |
| 具体方法 | 获取连接、创建 Statement、关闭资源 | 参数校验、更新状态、构建结果 |
| 抽象方法 | action.doInStatement()(回调) |
createPayOrder()、doPay() |
| 钩子方法 | 无 | needHandleCallback()、afterPayNotify() |
| 扩展方式 | 回调接口(无需继承) | 继承抽象类(子类重写) |
| 典型场景 | 框架层代码,追求低耦合 | 业务层代码,追求易理解 |
六、模板方法 vs 策略模式
| 对比项 | 模板方法模式 | 策略模式 |
|---|---|---|
| 核心思想 | 封装不变的流程,扩展可变的步骤 | 封装可互换的算法 |
| 关系 | 父类与子类之间是继承关系 | 策略与上下文之间是组合关系 |
| 算法数量 | 一个算法骨架,多个子类变体 | 多个独立的算法 |
| 切换时机 | 编译时确定(子类类型) | 运行时动态切换(通过引擎/工厂) |
| 扩展方式 | 新增子类 | 新增策略实现类 |
| 流程控制 | 父类控制整体流程 | 上下文/引擎控制策略选择 |
| 适用场景 | 流程固定,步骤可变 | 多个算法可互换 |
如何选择:
- 如果多个类有相同的处理流程 ,只是某些步骤不同 → 模板方法
- 如果同一个操作有多种完全不同的实现 ,需要运行时切换 → 策略模式
七、面试高频考点
Q1:模板方法模式中模板方法为什么要声明为 final?
答: 防止子类修改算法的执行顺序。模板方法的核心价值是固定流程,如果子类重写了模板方法,就破坏了算法的稳定性,违背了模板方法模式的初衷。
Q2:模板方法的三种基本方法有什么区别?
答:
- 抽象方法: 父类只声明,子类必须实现。决定算法的关键差异化步骤。
- 具体方法: 父类已实现,子类继承即可。适用于所有子类都相同的步骤。
- 钩子方法: 父类已实现(通常为空或返回默认值),子类可选择性重写。用于条件控制(如
isXxx()返回 boolean)或可选的扩展点。
Q3:JdbcTemplate 为什么用回调而不是继承?
答: 回调方式比继承更灵活:
- 避免类爆炸: 继承方式需要为每种 SQL 操作创建子类;回调方式只需传入不同的回调对象。
- 低耦合: 调用方不需要继承框架类,只需实现回调接口。
- 可组合: 同一个
JdbcTemplate实例可以配合不同的回调使用。
Q4:模板方法和策略模式有什么区别?怎么选择?
答:
- 模板方法关注的是「流程相同,步骤不同」,通过继承来扩展。
- 策略模式关注的是「算法可互换」,通过组合来切换。
- 如果有固定的执行流程,只是某些步骤需要自定义 → 用模板方法。
- 如果有多种完全不同的实现,需要在运行时切换 → 用策略模式。
Q5:钩子方法有什么作用?
答: 钩子方法为子类提供了不影响算法结构的扩展能力:
- 条件控制: 返回 boolean 控制某步骤是否执行,如
needHandleCallback()。 - 空操作: 默认不做任何事情,子类需要时再重写,如
afterPayNotify()。 - 降低耦合: 子类不需要的步骤不用强制实现。
Q6:模板方法模式的缺点是什么?怎么缓解?
答:
- 缺点 1:类数量增多。 缓解方式:合理使用回调接口代替继承(如 JdbcTemplate)。
- 缺点 2:继承强耦合。 缓解方式:父类尽量只包含稳定的公共逻辑,减少对子类的影响。
- 缺点 3:流程不灵活。 缓解方式:如果需要运行时切换流程,改用策略模式。
八、总结
模板方法模式的本质是封装不变的流程,扩展可变的步骤。
JdbcTemplate 和支付流程两个案例,都遵循了同一个模式:
抽象类定义模板方法(final)
↓
具体方法:父类实现,子类继承
↓
抽象方法:子类必须实现
↓
钩子方法:子类可选择性重写
掌握了这个套路,你就能在自己的项目中灵活运用模板方法模式------当发现多个类有重复的流程代码时,把不变的流程提取到父类,把可变的步骤留给子类。