📌 PDF :AI人工智能 --- 大模型微调与部署实战项目
大白话说Java设计模式-28-模板方法模式(业务实战篇):大白商城下单骨架的"算法模板"
📌 一句话本质:模板方法就是"父类定骨架,子类填细节",把不变的流程抽到父类,变的细节让子类实现。
🏷️ 标签:模板方法 / Java 设计模式 / 算法骨架 / 下单流程 / 大白商城 🎯 适合:初中级后端 / 经常被"通用流程"困扰的工程师
目录
- 一、业务场景引入:为什么需要"算法骨架"?
- 二、反面教材:复制粘贴的"灾难现场"
- 三、模式原理:模板方法的"父类定骨架"
- 四、实战代码:大白商城下单流程模板完整实现
- [五、模板方法 vs 策略 / 责任链](#五、模板方法 vs 策略 / 责任链)
- [六、Spring 的 AbstractApplicationContext 怎么用模板方法?](#六、Spring 的 AbstractApplicationContext 怎么用模板方法?)
- [七、模板方法 + 策略怎么组合?](#七、模板方法 + 策略怎么组合?)
- [八、工程决策 Checklist](#八、工程决策 Checklist)
- 九、与其他模式协作
- [十、本篇小结 + 下篇预告](#十、本篇小结 + 下篇预告)
一、业务场景引入:为什么需要"算法骨架"?
大白商城 2024 年重构订单中心,遇到一个老大难:
"普通下单、秒杀下单、拼团下单流程不一样,但核心步骤都一样(创建订单→减库存→支付→通知),每个实现都把流程复制一遍!"
我打开代码一看:
java
/**
* ❌ 大白商城老的下单代码
*/
@Service
public class OrderServiceV0 {
public Order createOrder(Goods goods, User user) {
// 1️⃣ 创建订单
Order order = new Order();
order.setUserId(user.getId());
order.setGoodsId(goods.getId());
// ... 50 行
orderDao.save(order);
// 2️⃣ 减库存
stockService.decrease(goods.getId(), 1);
// 3️⃣ 支付
payService.pay(order);
// 4️⃣ 通知
notifyService.notify(order);
return order;
}
public Order seckillOrder(Goods goods, User user) {
// ❌ 又复制一遍
Order order = new Order();
// ... 50 行
// 2️⃣ 减库存
stockService.decrease(goods.getId(), 1);
// 3️⃣ 支付
payService.pay(order);
// 4️⃣ 通知
notifyService.notify(order);
return order;
}
public Order grouponOrder(Goods goods, User user) {
// ❌ 又复制一遍
// ...
}
}
这种代码的痛点:
| 序号 | 问题 | 后果 |
|---|---|---|
| ① | 核心流程重复 3 遍 | 改流程要改 3 处 |
| ② | 新人接手难 | 不知道哪个是核心 |
| ③ | 业务流程改一处忘一处 | Bug 频出 |
老板要的"通用流程只写一次",怎么搞?
答案就是------模板方法模式。
1.1 大白话讲透模板方法
继续打比方:
场景:3 种下单方式(普通/秒杀/拼团),但核心流程都一样(创建订单→减库存→支付→通知)。
- 错误做法:每个下单方式都把流程复制一遍
- 正确做法 :
- 父类定骨架 :
AbstractOrderProcessor.processOrder()把流程固定- 子类填细节 :
NormalOrderProcessor/SeckillOrderProcessor/GrouponOrderProcessor只实现不同部分- 业务方 只调
processOrder(),不关心是哪种下单
模板方法 = 父类定算法骨架,子类实现具体步骤。
1.2 模板方法的 3 个真实场景
大白商城里,模板方法用在:
| 场景 | "父类骨架" | "子类差异" |
|---|---|---|
| 下单流程 | 4 步固定 | 普通/秒杀/拼团 |
| 数据导入 | 读取→转换→入库 | Excel/CSV/API |
| 报表生成 | 查询→聚合→导出 | 日报/月报/年报 |
| 支付回调 | 验签→处理→响应 | 支付宝/微信/银联 |
| 缓存加载 | 检查→加载→回填 | 多级缓存 |
任何"流程固定,细节有变"的场景,都用模板方法。
二、反面教材:复制粘贴的"灾难现场"
我们看 4 个反面教材,看它们是怎么一步步崩的。
2.1 反面教材 v1:完全复制粘贴
java
/**
* ❌ 反面教材 v1:完全复制粘贴
*/
public class OrderServiceV1 {
public Order normalOrder() {
// 流程 4 步
}
public Order seckillOrder() {
// 复制 4 步
}
public Order grouponOrder() {
// 复制 4 步
}
}
翻车现场:
| 序号 | 问题 | 后果 |
|---|---|---|
| ① | 核心流程重复 3 遍 | 改一处要改 3 处 |
| ② | 新人改漏 | Bug 频出 |
2.2 反面教材 v2:抽公共方法但仍重复调用
java
/**
* ❌ 反面教材 v2:抽公共方法但仍重复调用
*/
public class OrderServiceV2 {
public Order normalOrder() {
createOrder(); // 子方法
decreaseStock();
pay();
notify();
return order;
}
public Order seckillOrder() {
createOrder();
decreaseStock();
pay();
notify();
}
}
翻车现场:
| 序号 | 问题 | 后果 |
|---|---|---|
| ① | 流程顺序无法控制 | 子类可以乱调 |
| ② | 子方法容易漏调 | 流程不完整 |
2.3 反面教材 v3:流程控制分散
java
/**
* ❌ 反面教材 v3:流程控制分散
*/
public class OrderServiceV3 {
public Order order(OrderContext ctx) {
if (ctx.isNormal()) {
// 50 行 normal
} else if (ctx.isSeckill()) {
// 50 行 seckill
}
// 流程控制全在业务方
}
}
翻车现场:
| 序号 | 问题 | 后果 |
|---|---|---|
| ① | 业务方要知道所有细节 | 复杂 |
| ② | 加新流程要改业务方 | 改老代码 |
2.4 反面教材 v4:完全没抽象
java
/**
* ❌ 反面教材 v4:完全没抽象
*/
public class OrderServiceV4 {
public void process() {
// 1000 行所有逻辑混在一起
}
}
翻车现场:
| 序号 | 问题 | 后果 |
|---|---|---|
| ① | 无法复用 | 改一处牵动全身 |
| ② | 无法测试 | 单元测试跑不动 |
2.5 4 个反面教材的共同病根
| 痛点 | 反模式方案能不能解决? |
|---|---|
| 通用流程只写一次 | ❌ 全部重复 |
| 流程顺序控制 | ❌ 全部混乱 |
| 子类专注细节 | ❌ 全部复杂 |
| 业务方只调一处 | ❌ 全部要懂 |
必须上模板方法模式。
三、模式原理:模板方法的"父类定骨架"
3.1 模板方法的 2 个核心概念
模板方法(Template Method):
- 定义在父类的final 方法
- 控制算法骨架(流程)
- 不允许子类覆盖
钩子方法(Hook Method):
- 定义在父类的普通方法(可被子类覆盖)
- 流程中的可变步骤
- 由子类选择性覆盖
大白商城例子:
| 方法 | 类型 | 是否允许覆盖 |
|---|---|---|
processOrder() |
模板方法(final) | ❌ 不允许 |
createOrder() |
钩子方法 | ✅ 子类可覆盖 |
decreaseStock() |
钩子方法 | ✅ 子类可覆盖 |
pay() |
钩子方法 | ✅ 子类可覆盖 |
notify() |
钩子方法 | ✅ 子类可覆盖 |
beforeProcess() |
钩子方法 | ✅ 子类可覆盖(可选) |
afterProcess() |
钩子方法 | ✅ 子类可覆盖(可选) |
3.2 模板方法的 4 个核心角色
| 角色 | 职责 | 例子 |
|---|---|---|
| 抽象模板(Abstract Template) | 定义模板方法 + 钩子方法 | AbstractOrderProcessor |
| 具体模板 A(Concrete Template A) | 实现钩子方法 A | NormalOrderProcessor |
| 具体模板 B(Concrete Template B) | 实现钩子方法 B | SeckillOrderProcessor |
| 客户端(Client) | 调用模板方法 | OrderService |
关键点 :客户端只调 processOrder(),不知道内部细节。
3.3 一张图看懂模板方法
AbstractOrderProcessor(抽象模板)
├── final processOrder() ← 模板方法,固定流程
│ ├── createOrder() ← 钩子方法
│ ├── decreaseStock() ← 钩子方法
│ ├── pay() ← 钩子方法
│ └── notify() ← 钩子方法
│
├── NormalOrderProcessor ← 普通下单
├── SeckillOrderProcessor ← 秒杀下单
└── GrouponOrderProcessor ← 拼团下单
3.4 模板方法的"灵魂三问"
Q1:模板方法 vs 策略,区别是什么?
答:
- 模板方法 :继承 + 父类定骨架(流程固定,细节变)
- 策略 :组合 + 算法族独立(算法独立,可切换)
- 模板方法用继承 ,策略用接口
- 模板方法"流程被控制",策略"完全自由"
Q2:模板方法 vs 责任链,区别是什么?
答:
- 模板方法 :固定流程(4 步按顺序)
- 责任链 :链式处理(每一步独立,可中断)
- 模板方法"流程被父类控制",责任链"每一步决定是否继续"
- 模板方法用在"流程固定",责任链用在"流程可中断"
Q3:模板方法的钩子方法怎么设计?
答:
- 必选钩子(abstract):子类必须实现
- 可选钩子(空实现):子类可选择性覆盖
- 空实现的好处:子类不用被迫实现所有方法
3.5 模板方法的 3 种实现方式
| 方式 | 特点 | 适用 |
|---|---|---|
| abstract 钩子 | 子类必须实现 | 关键步骤 |
| 空实现钩子 | 子类可选择性覆盖 | 可选步骤 |
| protected + final 模板 | 严格控制流程 | 严格流程 |
大白商城主推 abstract + 空实现组合。
四、实战代码:大白商城下单流程模板完整实现
下面是大白商城生产环境在用的模板实现,全套代码可直接复制到 IDEA 跑。
4.1 项目环境与依赖
pom.xml:
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.dabai.mall</groupId>
<artifactId>mall-design-pattern-28</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>mall-design-pattern-28</name>
<description>大白商城 - 设计模式 28 模板方法</description>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
4.2 抽象模板:AbstractOrderProcessor
java
package com.dabai.mall.order.template;
import com.dabai.mall.order.Order;
import com.dabai.mall.order.OrderContext;
import com.dabai.mall.order.OrderResult;
import lombok.extern.slf4j.Slf4j;
/**
* ✅ 抽象模板:下单流程
* <p>
* - 模板方法 processOrder() 固定流程(final)
* - 钩子方法 createOrder / decreaseStock / pay / notify 由子类实现
*
* @author 大白商城技术团队
*/
@Slf4j
public abstract class AbstractOrderProcessor {
/**
* ✅ 模板方法:固定流程(final,不允许子类覆盖)
*/
public final OrderResult processOrder(OrderContext context) {
log.info("【下单流程】开始, type={}", getOrderType());
OrderResult result = new OrderResult();
try {
// 1️⃣ 钩子:下单前置
beforeProcess(context);
// 2️⃣ 钩子:创建订单
Order order = createOrder(context);
result.setOrder(order);
// 3️⃣ 钩子:扣减库存
decreaseStock(order, context);
// 4️⃣ 钩子:支付
pay(order, context);
// 5️⃣ 钩子:通知
notify(order, context);
// 6️⃣ 钩子:下单后置
afterProcess(context, result);
result.setSuccess(true);
} catch (Exception e) {
log.error("【下单流程】失败", e);
result.setSuccess(false);
result.setErrorMessage(e.getMessage());
}
log.info("【下单流程】结束, success={}", result.isSuccess());
return result;
}
// ===== 必选钩子:abstract,子类必须实现 =====
/**
* ✅ 钩子:创建订单
*/
protected abstract Order createOrder(OrderContext context);
/**
* ✅ 钩子:扣减库存
*/
protected abstract void decreaseStock(Order order, OrderContext context);
/**
* ✅ 钩子:支付
*/
protected abstract void pay(Order order, OrderContext context);
/**
* ✅ 钩子:通知
*/
protected abstract void notify(Order order, OrderContext context);
// ===== 可选钩子:空实现,子类可选择性覆盖 =====
/**
* 钩子:下单前置
*/
protected void beforeProcess(OrderContext context) {
log.debug("【下单】前置检查");
}
/**
* 钩子:下单后置
*/
protected void afterProcess(OrderContext context, OrderResult result) {
log.debug("【下单】后置处理");
}
/**
* 钩子:获取下单类型
*/
protected abstract String getOrderType();
}
配套 DTO:
java
package com.dabai.mall.order;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
/**
* 下单上下文
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderContext {
private String userId;
private String goodsId;
private Integer quantity;
private BigDecimal amount;
private String channel; // 支付渠道
private String address;
}
/**
* 订单
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
class Order {
private String orderId;
private String userId;
private String goodsId;
private Integer quantity;
private String status; // 待支付/已支付
}
/**
* 下单结果
*/
@Data
class OrderResult {
private boolean success;
private Order order;
private String errorMessage;
}
4.3 具体模板:3 种下单
NormalOrderProcessor:
java
package com.dabai.mall.order.template;
import com.dabai.mall.order.Order;
import com.dabai.mall.order.OrderContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* ✅ 具体模板:普通下单
*/
@Slf4j
@Component
public class NormalOrderProcessor extends AbstractOrderProcessor {
@Override
protected Order createOrder(OrderContext context) {
log.info("【普通下单】创建订单, userId={}, goodsId={}",
context.getUserId(), context.getGoodsId());
return Order.builder()
.orderId("NORMAL_" + System.currentTimeMillis())
.userId(context.getUserId())
.goodsId(context.getGoodsId())
.quantity(context.getQuantity())
.status("PENDING_PAY")
.build();
}
@Override
protected void decreaseStock(Order order, OrderContext context) {
log.info("【普通下单】扣减库存, goodsId={}, quantity={}",
order.getGoodsId(), order.getQuantity());
// 普通下单:直接扣减
}
@Override
protected void pay(Order order, OrderContext context) {
log.info("【普通下单】支付, orderId={}, amount={}",
order.getOrderId(), context.getAmount());
}
@Override
protected void notify(Order order, OrderContext context) {
log.info("【普通下单】通知, orderId={}", order.getOrderId());
}
@Override
protected String getOrderType() {
return "NORMAL";
}
}
SeckillOrderProcessor:
java
package com.dabai.mall.order.template;
import com.dabai.mall.order.Order;
import com.dabai.mall.order.OrderContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* ✅ 具体模板:秒杀下单
*/
@Slf4j
@Component
public class SeckillOrderProcessor extends AbstractOrderProcessor {
@Override
protected Order createOrder(OrderContext context) {
log.info("【秒杀下单】创建订单, userId={}, goodsId={}",
context.getUserId(), context.getGoodsId());
return Order.builder()
.orderId("SECKILL_" + System.currentTimeMillis())
.userId(context.getUserId())
.goodsId(context.getGoodsId())
.quantity(context.getQuantity())
.status("PENDING_PAY")
.build();
}
@Override
protected void decreaseStock(Order order, OrderContext context) {
log.info("【秒杀下单】Redis 预扣库存, goodsId={}, quantity={}",
order.getGoodsId(), order.getQuantity());
// ✅ 秒杀特色:Redis 预扣 + 异步同步 MySQL
}
@Override
protected void pay(Order order, OrderContext context) {
log.info("【秒杀下单】极速支付, orderId={}", order.getOrderId());
}
@Override
protected void notify(Order order, OrderContext context) {
log.info("【秒杀下单】秒杀成功通知, orderId={}", order.getOrderId());
}
@Override
protected void afterProcess(OrderContext context, com.dabai.mall.order.OrderResult result) {
// ✅ 秒杀特色:记录秒杀统计
log.info("【秒杀下单】统计: goodsId={}, success={}",
context.getGoodsId(), result.isSuccess());
}
@Override
protected String getOrderType() {
return "SECKILL";
}
}
GrouponOrderProcessor:
java
package com.dabai.mall.order.template;
import com.dabai.mall.order.Order;
import com.dabai.mall.order.OrderContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* ✅ 具体模板:拼团下单
*/
@Slf4j
@Component
public class GrouponOrderProcessor extends AbstractOrderProcessor {
@Override
protected Order createOrder(OrderContext context) {
log.info("【拼团下单】创建拼团订单, userId={}, goodsId={}",
context.getUserId(), context.getGoodsId());
return Order.builder()
.orderId("GROUPON_" + System.currentTimeMillis())
.userId(context.getUserId())
.goodsId(context.getGoodsId())
.quantity(context.getQuantity())
.status("PENDING_GROUP")
.build();
}
@Override
protected void decreaseStock(Order order, OrderContext context) {
log.info("【拼团下单】预占库存, goodsId={}, quantity={}",
order.getGoodsId(), order.getQuantity());
// ✅ 拼团特色:库存预占,成团后扣减
}
@Override
protected void pay(Order order, OrderContext context) {
log.info("【拼团下单】暂不支付,等待成团, orderId={}", order.getOrderId());
// ✅ 拼团特色:成团后才扣款
}
@Override
protected void notify(Order order, OrderContext context) {
log.info("【拼团下单】通知开团, orderId={}", order.getOrderId());
}
@Override
protected String getOrderType() {
return "GROUPON";
}
}
4.4 业务调用方
java
package com.dabai.mall.order.service;
import com.dabai.mall.order.OrderContext;
import com.dabai.mall.order.OrderResult;
import com.dabai.mall.order.template.AbstractOrderProcessor;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* ✅ 订单服务:模板方法的调用方
* <p>
* 业务方只调 processOrder(),不关心内部流程
*
* @author 大白商城技术团队
*/
@Slf4j
@Service
public class OrderService {
/**
* 类型 → 处理器
*/
private final Map<String, AbstractOrderProcessor> processorMap = new HashMap<>();
@Autowired
private List<AbstractOrderProcessor> processors;
@PostConstruct
public void init() {
for (AbstractOrderProcessor processor : processors) {
// 通过 getOrderType() 注册
register(processor);
}
}
private void register(AbstractOrderProcessor processor) {
// 简化:实际可用 Bean 名 / 自定义注解
// 这里用反射拿 getOrderType() 的值
log.info("【OrderService】注册处理器: {}",
processor.getClass().getSimpleName());
}
/**
* ✅ 业务入口:业务方只调这里
*/
public OrderResult createOrder(String type, OrderContext context) {
AbstractOrderProcessor processor = findProcessor(type);
// ✅ 调模板方法(不关心是哪种下单)
return processor.processOrder(context);
}
private AbstractOrderProcessor findProcessor(String type) {
for (AbstractOrderProcessor p : processors) {
// 实际可用 getOrderType() 匹配
return p;
}
throw new IllegalArgumentException("未知下单类型: " + type);
}
}
关键点:
| 关键点 | 解释 |
|---|---|
业务方只调 processOrder() |
不知道 4 步流程 |
| 加新下单? | 只加 Processor 类 |
| 改流程? | 只改 AbstractOrderProcessor |
4.5 单元测试
java
package com.dabai.mall.order.template;
import com.dabai.mall.order.OrderContext;
import com.dabai.mall.order.OrderResult;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.*;
/**
* 模板方法模式完整单元测试
*/
class AbstractOrderProcessorTest {
@Test
void testNormalOrder() {
AbstractOrderProcessor processor = new NormalOrderProcessor();
OrderContext context = OrderContext.builder()
.userId("USER_001")
.goodsId("GOODS_001")
.quantity(1)
.amount(new BigDecimal("100"))
.channel("alipay")
.build();
OrderResult result = processor.processOrder(context);
assertTrue(result.isSuccess());
assertNotNull(result.getOrder());
assertTrue(result.getOrder().getOrderId().startsWith("NORMAL_"));
}
@Test
void testSeckillOrder() {
AbstractOrderProcessor processor = new SeckillOrderProcessor();
OrderContext context = OrderContext.builder()
.userId("USER_002")
.goodsId("GOODS_002")
.quantity(1)
.amount(new BigDecimal("50"))
.build();
OrderResult result = processor.processOrder(context);
assertTrue(result.isSuccess());
assertTrue(result.getOrder().getOrderId().startsWith("SECKILL_"));
}
@Test
void testGrouponOrder() {
AbstractOrderProcessor processor = new GrouponOrderProcessor();
OrderContext context = OrderContext.builder()
.userId("USER_003")
.goodsId("GOODS_003")
.quantity(1)
.amount(new BigDecimal("80"))
.build();
OrderResult result = processor.processOrder(context);
assertTrue(result.isSuccess());
assertTrue(result.getOrder().getOrderId().startsWith("GROUPON_"));
}
@Test
void testTemplateMethodFinal() {
// ✅ 模板方法 processOrder 是 final
// 子类无法覆盖流程
AbstractOrderProcessor processor = new NormalOrderProcessor();
assertEquals("NORMAL", processor.getOrderType());
}
@Test
void testHookOverride() {
// ✅ SeckillOrderProcessor 覆盖了 afterProcess
AbstractOrderProcessor seckill = new SeckillOrderProcessor();
OrderContext context = OrderContext.builder()
.userId("USER_004")
.goodsId("GOODS_004")
.quantity(1)
.build();
OrderResult result = seckill.processOrder(context);
// 验证 afterProcess 跑了(log 里能看到)
assertTrue(result.isSuccess());
}
}
五、模板方法 vs 策略 / 责任链
5.1 一张表看清区别
| 模式 | 控制方式 | 实现方式 | 流程 |
|---|---|---|---|
| 模板方法 | 父类控制 | 继承 | 固定 |
| 策略 | 客户端控制 | 组合 | 自由 |
| 责任链 | 每步控制 | 链式 | 链式 |
5.2 一个具体例子区分 3 个模式
大白商城:
| 模式 | 例子 |
|---|---|
| 模板方法 | 下单流程(4 步固定) |
| 策略 | 支付方式(4 种独立) |
| 责任链 | 风控流程(每步独立) |
5.3 决策树
有通用流程?
├── 是
│ ├── 流程固定?
│ │ ├── 是 → ✅ 模板方法
│ │ └── 否(可中断)→ ✅ 责任链
└── 否
├── 多种算法可切换?
│ ├── 是 → ✅ 策略
│ └── 否 → 普通类
5.4 模板方法 vs 策略:易混淆
| 维度 | 模板方法 | 策略 |
|---|---|---|
| 控制方 | 父类 | 客户端 |
| 实现 | 继承 | 接口 |
| 流程 | 固定 | 自由 |
| 改动点 | 子类填细节 | 客户端选策略 |
大白商城选型:
| 场景 | 选哪个 | 原因 |
|---|---|---|
| 下单流程 | 模板方法 | 流程固定 |
| 支付方式 | 策略 | 4 种独立 |
| 风控规则 | 责任链 | 可中断 |
| 数据导入 | 模板方法 | 流程固定 |
六、Spring 的 AbstractApplicationContext 怎么用模板方法?
6.1 AbstractApplicationContext.refresh():经典模板方法
Spring 容器启动用模板方法模式:
java
/**
* ✅ Spring 模板方法:容器刷新
*/
public abstract class AbstractApplicationContext extends ... {
/**
* ✅ 模板方法:固定 12 步
*/
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// 1️⃣ 钩子:准备刷新
prepareRefresh();
// 2️⃣ 钩子:获取 BeanFactory
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// 3️⃣ 钩子:准备 BeanFactory
prepareBeanFactory(beanFactory);
// 4️⃣ 钩子:后置处理 BeanFactory
postProcessBeanFactory(beanFactory);
// 5️⃣ 钩子:调用 BeanFactoryPostProcessor
invokeBeanFactoryPostProcessors(beanFactory);
// 6️⃣ 钩子:注册 BeanPostProcessor
registerBeanPostProcessors(beanFactory);
// 7️⃣ 钩子:初始化消息源
initMessageSource();
// 8️⃣ 钩子:初始化事件广播
initApplicationEventMulticaster();
// 9️⃣ 钩子:子类扩展
onRefresh();
// 🔟 钩子:注册监听器
registerListeners();
// 1️⃣1️⃣ 钩子:实例化所有非懒加载单例
finishBeanFactoryInitialization(beanFactory);
// 1️⃣2️⃣ 钩子:发布刷新事件
finishRefresh();
}
}
}
关键点:
| 关键点 | 解释 |
|---|---|
refresh() |
模板方法(public,未标 final 但实际不允许覆盖) |
prepareRefresh() |
钩子方法 |
onRefresh() |
钩子方法(子类扩展点) |
| 12 步固定流程 | 模板 |
6.2 子类扩展:onRefresh()
java
/**
* ✅ Spring 子类:GenericWebApplicationContext 扩展
*/
public class GenericWebApplicationContext extends GenericApplicationContext
implements WebApplicationContext {
@Override
protected void onRefresh() {
// ✅ 钩子覆盖:web 容器特殊处理
super.onRefresh();
// ... web 特有初始化
}
}
关键点:
| 关键点 | 解释 |
|---|---|
父类 AbstractApplicationContext |
定骨架 |
子类 GenericWebApplicationContext |
覆盖 onRefresh() |
| 加 web 特性 | 只覆盖钩子 |
6.3 大白商城的"抄作业"
java
/**
* 借鉴 Spring refresh:通用业务流程
*/
public abstract class AbstractBusinessProcess {
/**
* ✅ 模板方法
*/
public final void process(BusinessContext ctx) {
check(ctx);
doBusiness(ctx);
afterBusiness(ctx);
}
protected abstract void check(BusinessContext ctx);
protected abstract void doBusiness(BusinessContext ctx);
protected void afterBusiness(BusinessContext ctx) {
// 钩子:子类可覆盖
}
}
七、模板方法 + 策略怎么组合?
7.1 组合 1:模板方法内用策略
java
/**
* ✅ 模板方法内用策略:支付步骤用策略
*/
public abstract class AbstractOrderProcessor {
@Autowired
private PaymentContext paymentContext; // 策略上下文
public final OrderResult processOrder(OrderContext context) {
// ...
// 4️⃣ 支付:用策略
paymentContext.pay(context.getChannel(), ...);
// ...
}
}
7.2 组合 2:策略 + 模板方法 = 策略骨架
java
/**
* ✅ 策略抽象类(带模板)
*/
public abstract class AbstractPaymentStrategy implements PaymentStrategy {
public final PaymentResult doPay(PaymentRequest request) {
// ✅ 模板方法
validate(request);
doPayInternal(request);
recordLog(request);
return result;
}
protected abstract void doPayInternal(PaymentRequest request);
protected void validate(PaymentRequest request) {
// 公共校验
}
protected void recordLog(PaymentRequest request) {
// 公共日志
}
}
@Component
public class AlipayStrategy extends AbstractPaymentStrategy {
@Override
protected void doPayInternal(PaymentRequest request) {
// 支付宝特有支付
}
}
7.3 组合 3:钩子方法 + 工厂方法
java
/**
* ✅ 钩子 + 工厂
*/
public abstract class AbstractOrderProcessor {
/**
* 钩子:获取支付策略(子类可覆盖)
*/
protected PaymentStrategy getPaymentStrategy(String channel) {
return PaymentStrategyFactory.getStrategy(channel);
}
}
八、工程决策 Checklist
8.1 ✅ 这 5 种情况,强烈建议用模板方法
| 序号 | 场景 | 原因 |
|---|---|---|
| ① | 流程固定 | 下单/数据导入/报表 |
| ② | 多个变体 | 普通/秒杀/拼团 |
| ③ | 流程被严格控制 | 不允许子类乱改 |
| ④ | 复用公共逻辑 | 父类抽公共 |
| ⑤ | 子类只填差异 | 关注点分离 |
8.2 ❌ 这 5 种情况,绝对不要用模板方法
| 序号 | 场景 | 原因 |
|---|---|---|
| ① | 流程不固定 | 用策略 / 责任链 |
| ② | 只有一个变体 | 没意义 |
| ③ | 算法之间独立 | 用策略 |
| ④ | 业务简单 | 过度设计 |
| ⑤ | 需要运行时切换 | 用策略 |
8.3 ⚠️ 模板方法的 6 大常见坑
| 序号 | 坑 | 表现 | 解决方案 |
|---|---|---|---|
| ① | 模板方法被覆盖 | 父类失去控制 | 标 final |
| ② | 钩子方法太多 | 子类被迫实现 | abstract + 空实现 |
| ③ | 继承层次深 | 难调试 | 控制继承层数 |
| ④ | 父类臃肿 | 难维护 | 父类只管骨架 |
| ⑤ | 子类破坏流程 | 跳过关键步骤 | 模板 final |
| ⑥ | 钩子语义不清 | 命名混乱 | 命名规范 |
8.4 面试官视角:模板方法高频追问
Q1:模板方法 vs 策略,区别是什么?
答:模板方法用继承 (父类定骨架),策略用组合 (算法族独立)。模板方法"流程被控制",策略"完全自由"。
Q2:钩子方法 vs 抽象方法,区别是什么?
答:抽象方法子类必须实现 ,钩子方法子类可选择性覆盖 。钩子给子类扩展点,但不强求。
Q3:Spring refresh() 是模板方法吗?
答:是的 。
AbstractApplicationContext.refresh()定 12 步骨架,onRefresh()等钩子让子类扩展。WebApplicationContext 覆盖 onRefresh() 加入 web 特性。
九、与其他模式协作
9.1 模板方法 + 策略 = 流程固定 + 算法可切换
9.2 模板方法 + 工厂方法 = 钩子内创建对象
java
protected Order createOrder(OrderContext context) {
Order order = OrderFactory.createOrder(context);
return order;
}
9.3 模板方法 + 装饰器 = 钩子被装饰
java
public abstract class AbstractOrderProcessor {
protected abstract void notify(Order order, OrderContext context);
}
// 装饰:加日志
public class LoggedNotifyOrderProcessor extends AbstractOrderProcessor {
// ...
@Override
protected void notify(Order order, OrderContext context) {
log.info("通知: orderId={}", order.getOrderId());
// ... 调原 notify
}
}
9.4 大白商城模式协作全景图
┌──────────────┐
│ 模板方法 │ ← 本篇
└──────┬───────┘
│
┌───────────┬───────┼───────┬───────────┐
│ │ │ │ │
┌───▼───┐ ┌────▼───┐ ┌▼────┐ ┌▼─────┐ ┌───▼────┐
│策略 │ │工厂方法│ │装饰器│ │单例 │ │ 责任链 │
│(步骤) │ │(创建) │ │(增强)│ │(骨架)│ │ (变体) │
└───────┘ └────────┘ └──────┘ └──────┘ └───────┘
26 篇 04 篇 16 篇 02 篇 46 篇
十、本篇小结 + 下篇预告
10.1 本篇小结(5 个核心要点)
- 本质 :模板方法 = 父类定骨架 + 子类填细节 ,加新变体只加 1 个类。
- 场景 :下单流程 / 数据导入 / 报表生成,流程固定,细节有变。
- 对比:模板方法 vs 策略(继承 vs 组合)/ 模板方法 vs 责任链(固定 vs 链式)。
- Spring refresh() :经典模板方法,12 步骨架 + 钩子扩展。
- 避坑:模板方法被覆盖 / 钩子过多 / 继承层次深 / 父类臃肿。
10.2 一句话总结
模板方法不是"换种方式继承",是"用父类控制流程" 。大白商城普通/秒杀/拼团 3 种下单,核心 4 步只写一次 ,加新下单只加 1 个 Processor。改流程?只改父类。
10.3 知识脑图
模板方法模式
├── 2 个核心概念
│ ├── 模板方法(final)
│ └── 钩子方法(abstract + 空实现)
├── 4 大角色
│ ├── 抽象模板(AbstractOrderProcessor)
│ ├── 具体模板 A(NormalOrderProcessor)
│ ├── 具体模板 B(SeckillOrderProcessor)
│ └── 客户端(OrderService)
├── 3 个具体模板
│ ├── NormalOrderProcessor(普通下单)
│ ├── SeckillOrderProcessor(秒杀下单)
│ └── GrouponOrderProcessor(拼团下单)
├── 模式对比
│ ├── vs 策略(继承 vs 组合)
│ ├── vs 责任链(固定 vs 链式)
│ └── vs 状态(流程 vs 状态机)
├── Spring refresh()
│ ├── 12 步骨架
│ ├── onRefresh() 钩子
│ └── WebApplicationContext 扩展
└── 模式协作
├── + 策略(流程内用策略)
├── + 工厂(钩子创建对象)
└── + 装饰器(钩子被装饰)
10.4 下篇预告
第 29 篇【模板方法模式 - 源码剖析篇】:JDK / Spring 中的模板实现
下一篇我们会深入源码,回答三个问题:
- JDK 的
AbstractList/AbstractMap怎么用模板方法? - Spring 的
AbstractApplicationContext.refresh()怎么用模板? JdbcTemplate/RestTemplate怎么用模板简化?
并附完整的源码解读 + 流程图 + 大白商城的"抄作业"实践。
觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯