第一章:什么是模板方法模式?
(以做菜比喻,5分钟轻松理解设计模式)
1. 定义:好莱坞原则("别找我们,我们找你")
想象你参加《厨神大赛》,评委说:"别问我们该做什么菜,按我们给的流程做"。
模板方法模式就像这个评委:
- 父类 规定固定流程(洗菜→切菜→烹饪→装盘)
- 子类 只需关注差异步骤(洗菜用冷水还是盐水?切丁还是切片?)
🔥 关键特征:子类不能改变流程,只能"填空"具体步骤
2. 核心思想:骨架与细节分离
场景还原:假设你要实现不同饮料制作流程
java
// 抽象饮料制作模板
abstract class BeverageTemplate {
// 模板方法(不可被重写)
public final void make() {
boilWater();
brew();
pourInCup();
addCondiments();
}
protected abstract void brew(); // 冲泡(咖啡/茶不同)
protected abstract void addCondiments(); // 加调料(糖/柠檬不同)
private void boilWater() { // 共用方法
System.out.println("烧开水");
}
private void pourInCup() { // 共用方法
System.out.println("倒入杯子");
}
}
小白理解技巧:
- 父类的
make()
像填空题的题目 - 子类只需填写答案(实现抽象方法)
3. 适用场景(附真实项目案例)
场景特征:
- ✅ 流程步骤固定(如支付必须:校验→执行→通知)
- ✅ 部分步骤实现方式不同(微信/支付宝支付逻辑差异)
常见应用场景:
场景 | 固定流程 | 可变步骤 |
---|---|---|
支付系统 | 校验→支付→通知 | 微信需获取openid,支付宝要签名 |
报表生成 | 取数据→转换格式→导出 | Excel生成逻辑 vs PDF渲染 |
工单处理 | 创建→分配→处理→关闭 | 不同工单类型处理逻辑不同 |
🎨 模式可视化(结合Mermaid图解析)
图解重点:
- 黄色
templateMethod()
是不可修改的流程控制器 - 蓝色抽象方法必须实现(如考试必答题)
- 绿色钩子方法(
step3()
)可选实现(如考试选答题)
💡 一句话理解模板方法模式
"把固定流程写在父类里,把变化封装到子类中" ------ 就像用PPT模板做汇报,只需修改内容页,不用重做封面和目录!
第二章:模式结构解析
(用"做奶茶"拆解设计模式DNA)
1. UML类图深度解析(动态演示)
逐层解析(配合动画想象):
-
固定流程(制作奶茶方法):
- 不可修改的
final
方法,就像奶茶店的标准化操作手册
javapublic final void 制作奶茶() { 煮水(); 加茶底(); // 抽象方法(必须实现) 加配料(); // 抽象方法(必须实现) if(需要加糖()) { // 钩子方法(可选实现) 加糖(); } 封口(); }
- 不可修改的
-
必须实现的方法(红色感叹号标识):
加茶底()
、加配料()
是抽象方法,子类必须实现- 相当于奶茶师傅说:"茶底和配料必须选,其他我不管"
-
可选实现的钩子方法(绿色对勾标识):
加糖()
是具体方法(默认空实现)- 子类可根据需要决定是否覆盖
java// 默认实现(空方法) protected void 加糖() {} // 芝士奶盖茶覆盖实现 protected void 加糖() { System.out.println("加入半糖糖浆"); }
2. 关键角色实战手册(附代码对照表)
角色 | 作用 | 对应奶茶案例 | 对应支付案例 |
---|---|---|---|
抽象模板类 | 定义骨架流程+抽象方法 | 奶茶制作模板 |
PaymentTemplate |
具体实现类 | 实现具体步骤 | 珍珠奶茶 /芝士奶盖茶 |
WechatPayment |
模板方法 | 封装流程的final方法 | 制作奶茶() |
processPayment() |
抽象方法 | 子类必须实现的步骤 | 加茶底() /加配料() |
validateParameters() |
钩子方法 | 子类可选覆盖的方法 | 加糖() |
preProcess() |
🛠️ 架构师诊断:如何避免常见设计错误?
错误案例 :在支付流程子类中重写processPayment()
java
// 危险!破坏流程完整性
public class HackedPayment extends PaymentTemplate {
@Override
public void processPayment() { // ❌ 重写final方法会编译报错
executePayment();
sendNotification();
}
}
正确姿势:
- 用
final
关键字保护模板方法 - 流程变更应通过增加钩子方法实现
- 极端情况考虑用策略模式替代
💡 结构设计心法:三要三不要
✅ 该做 | ❌ 不该做 |
---|---|
将稳定流程声明为final | 允许子类修改流程顺序 |
用protected修饰具体步骤方法 | 暴露不需要公开的方法为public |
通过钩子方法提供扩展点 | 强迫子类实现不相关的方法 |
练手小测验:以下哪些适合用模板方法模式?
- 不同数据库的连接流程(MySQL需要SSL,Oracle需要服务名)
- 用户权限校验(不同角色校验规则不同)
- 数据导出功能(Excel/CSV导出步骤相同)
答案:1和3(固定流程+部分步骤变化),2适合用策略模式
第三章:实战案例 - 电商支付系统(深度剖析版)
(附赠可直接粘贴的生产级代码)
🛠️ 代码增强点说明
- 增加异常处理机制
- 添加日志追踪
- 引入重试机制钩子
- 增加模板方法返回值
升级版代码实现(带详细注释)
java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 抽象支付模板(生产环境建议用接口+抽象类组合)
*/
public abstract class PaymentTemplate {
protected final Logger logger = LoggerFactory.getLogger(getClass());
// 模板方法(final确保支付流程原子性)
public final boolean processPayment() {
try {
logger.info("开始支付流程");
validateParameters();
preProcess();
boolean paymentResult = executePayment();
if (paymentResult) {
sendNotification();
}
return paymentResult;
} catch (PaymentException e) {
logger.error("支付流程异常", e);
retryIfNeeded(e); // 调用重试钩子
return false;
}
}
// 必须实现的校验方法
protected abstract void validateParameters() throws PaymentException;
// 预处理钩子(空实现表示可选步骤)
protected void preProcess() throws PaymentException {}
// 必须实现的支付执行
protected abstract boolean executePayment() throws PaymentException;
// 重试机制钩子(默认不重试)
protected void retryIfNeeded(PaymentException e) throws PaymentException {
logger.info("默认不进行重试");
}
// 固定通知方式(不允许修改)
private void sendNotification() {
logger.info("发送短信+站内信通知");
// 实际项目可接入消息队列
}
}
// 微信支付实现(包含重试机制)
public class WechatPayment extends PaymentTemplate {
@Override
protected void validateParameters() throws PaymentException {
logger.debug("校验OpenID格式");
if (Math.random() < 0.3) { // 模拟30%校验失败
throw new PaymentException("OpenID校验失败");
}
}
@Override
protected void preProcess() {
logger.info("获取微信用户授权信息");
}
@Override
protected boolean executePayment() {
logger.info("调用微信支付统一下单API");
return Math.random() > 0.2; // 模拟80%成功
}
@Override
protected void retryIfNeeded(PaymentException e) {
logger.warn("微信支付失败,10秒后重试");
try {
Thread.sleep(10000);
this.processPayment();
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
// 支付宝支付实现(包含签名生成)
public class AlipayPayment extends PaymentTemplate {
@Override
protected void validateParameters() throws PaymentException {
logger.debug("验证支付宝账户状态");
}
@Override
protected boolean executePayment() {
logger.info("生成RSA2签名");
logger.info("调用支付宝当面付接口");
return true;
}
}
// 自定义支付异常
public class PaymentException extends Exception {
public PaymentException(String message) {
super(message);
}
}
🎯 关键实现技巧解析
- 异常处理模板
java
try {
// 标准流程
} catch (PaymentException e) {
retryIfNeeded(e); // 通过钩子实现差异化重试
}
- 微信支付:失败后自动重试
- 支付宝:使用默认不重试策略
- 日志分级策略
java
logger.debug("校验参数明细"); // 开发阶段可见
logger.info("核心步骤记录"); // 生产环境可见
logger.error("异常关键信息"); // 告警系统对接
- 模板方法返回值设计
java
public final boolean processPayment() {
// 返回支付结果
}
- 统一返回布尔值,便于后续对账处理
- 重试机制扩展点
java
protected void retryIfNeeded(PaymentException e)
- 默认空实现
- 微信支付覆盖实现指数退避重试
💻 客户端调用示例
java
public class PaymentClient {
public static void main(String[] args) {
// 模拟微信支付(带重试)
PaymentTemplate wechatPay = new WechatPayment();
boolean result = wechatPay.processPayment();
System.out.println("微信支付结果:" + result);
// 模拟支付宝支付
PaymentTemplate alipay = new AlipayPayment();
System.out.println("支付宝支付结果:" + alipay.processPayment());
}
}
运行结果示例:
yaml
INFO WechatPayment: 开始支付流程
DEBUG WechatPayment: 校验OpenID格式
ERROR WechatPayment: 支付流程异常 PaymentException: OpenID校验失败
WARN WechatPayment: 微信支付失败,10秒后重试
INFO WechatPayment: 开始支付流程(重试)
DEBUG WechatPayment: 校验OpenID格式
INFO WechatPayment: 获取微信用户授权信息
INFO WechatPayment: 调用微信支付统一下单API
INFO WechatPayment: 发送短信+站内信通知
微信支付结果:true
INFO AlipayPayment: 开始支付流程
DEBUG AlipayPayment: 验证支付宝账户状态
INFO AlipayPayment: 生成RSA2签名
INFO AlipayPayment: 调用支付宝当面付接口
INFO AlipayPayment: 发送短信+站内信通知
支付宝支付结果:true
🔧 生产环境适配建议
- 配置化改造
properties
# application.properties
wechat.retry.interval=10000
wechat.max.retries=3
- Spring集成示例
java
@Service
public class PaymentService {
@Autowired
private WechatPayment wechatPayment;
@Transactional
public boolean handlePayment(String type) {
PaymentTemplate template = PaymentFactory.create(type);
return template.processPayment();
}
}
- 监控埋点
java
public abstract class PaymentTemplate {
protected void recordMetric(String metricName) {
// 接入Micrometer等监控组件
}
}
❓ 新手常见问题解答
Q:为什么sendNotification要设为private?
A:保证所有支付方式使用统一的通知方式,避免子类擅自修改通知逻辑导致漏通知
Q:如何处理不同支付方式的参数传递?
A:推荐使用Builder模式构造支付上下文对象:
java
public abstract class PaymentTemplate {
protected PaymentContext context;
public PaymentTemplate(PaymentContext context) {
this.context = context;
}
}
第四章:模式优缺点(真实项目血泪教训版)
(附规避缺点的最佳实践方案)
✅ 优势全景解析(附代码量对比)
1. 代码复用率提升80%的奥秘
案例对比:支付系统接入新渠道(银联支付)
java
// 传统写法:每个支付类重复流程代码
class UnionPay {
public void pay() {
validate(); // 重复
checkRisk(); // 重复
execute(); // 差异
notify(); // 重复
}
}
// 模板方法模式写法
class UnionPayment extends PaymentTemplate {
protected void executePayment() { /* 仅实现差异部分 */ }
}
数据统计:
- 传统方式:新增1个支付类需200行代码
- 模板模式:新增类仅需50行(复用父类150行)
2. 流程管控的工业级实践
优势体现:
- 新增支付方式无法跳过风控检查
- 统一在父类修改通知逻辑(如增加微信模板消息)
3. 开闭原则的完美诠释
java
// 扩展开放:新增刷脸支付
class FacePayment extends PaymentTemplate {
// 仅实现抽象方法,不修改已有代码
}
// 修改关闭:支付流程优化
abstract class PaymentTemplate {
// 在父类统一增加缓存逻辑
public final void processPayment() {
checkCache(); // 新增的通用步骤
validate();
// ...原有流程
}
}
❌ 劣势破解指南
1. 父类变更的雪崩效应
灾难现场:在抽象类新增抽象方法
java
abstract class PaymentTemplate {
// 新增抽象方法
protected abstract void riskCheck();
}
// 所有子类爆红:必须实现riskCheck()
class WechatPayment extends PaymentTemplate { /* 编译错误 */ }
解决方案:
代码改良:
java
// 默认提供空实现而非抽象方法
protected void riskCheck() {} // 钩子方法
2. 类膨胀的救赎之道
问题场景:当存在多种组合时
破解方案:组合模式+策略模式
java
class PaymentTemplate {
private RiskStrategy riskStrategy; // 将风险校验策略化
public void setRiskStrategy(RiskStrategy strategy) {
this.riskStrategy = strategy;
}
}
interface RiskStrategy {
void check();
}
改造效果:
- 支付类型类从10个减少到2个
- 风险策略类新增3个(但可复用)
🛡️ 生产环境避险清单
风险场景 | 应对方案 | 代码示例 |
---|---|---|
需要新增流程步骤 | 在父类模板方法中添加钩子方法 | protected void newStep() {} |
不同实现类参数差异大 | 使用Context对象封装参数 | processPayment(PaymentContext ctx) |
部分步骤需要动态替换 | 结合策略模式 | setExecuteStrategy(ExecuteStrategy strategy) |
需要兼容历史代码 | 使用适配器模式包装旧系统 | class LegacyAdapter extends PaymentTemplate |
💎 架构师经验之谈
"模板方法模式是把双刃剑,使用时牢记两个原则:
- 流程稳定性:确认核心流程半年内不会大改
- 差异明确性 :变化点不超过总步骤的40%
当变化点超过50%时,请改用策略模式+责任链模式组合"
🔍 灵魂拷问:你的场景真的适合吗?
通过决策树检验适用性:
第五章:项目中的最佳实践
(Spring源码级解析 + 可插拔架构设计)
1. Spring框架应用场景(源码级剖析)
🔍 场景一:JdbcTemplate的execute()方法
java
public class JdbcTemplate {
// 模板方法(控制连接生命周期)
public <T> T execute(ConnectionCallback<T> action) {
Connection con = DataSourceUtils.getConnection(obtainDataSource());
try {
return action.doInConnection(con); // 抽象方法的具体实现
}
finally {
DataSourceUtils.releaseConnection(con, getDataSource());
}
}
}
// 使用示例(匿名内部类实现回调)
jdbcTemplate.execute(new ConnectionCallback<Void>() {
@Override
public Void doInConnection(Connection con) {
// 开发者只需关注SQL逻辑
try (Statement stmt = con.createStatement()) {
stmt.execute("UPDATE users SET status=1");
}
return null;
}
});
模式映射:
设计精髓:
- 模板方法:
execute()
控制连接获取/释放 - 抽象方法:
doInConnection()
由用户实现具体SQL操作
🔍 场景二:TransactionTemplate事务管理
java
public class TransactionTemplate {
// 模板方法(事务边界控制)
public <T> T execute(TransactionCallback<T> action) {
TransactionStatus status = transactionManager.getTransaction(this);
try {
T result = action.doInTransaction(status); // 业务逻辑入口
transactionManager.commit(status);
return result;
} catch (RuntimeException ex) {
transactionManager.rollback(status);
throw ex;
}
}
}
// 业务使用示例
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
userDao.update(user);
orderDao.create(order);
}
});
架构启示:
- 父类(TransactionTemplate)控制事务提交/回滚
- 子类(匿名回调)专注业务逻辑
2. 扩展技巧(生产级代码示例)
技巧一:工厂方法 + 模板方法 黄金组合
java
// 支付工厂类
public class PaymentFactory {
public static PaymentTemplate createPayment(String type) {
switch (type.toLowerCase()) {
case "wechat":
return new WechatPayment();
case "alipay":
return new AlipayPayment();
default:
throw new IllegalArgumentException("不支持的支付类型");
}
}
}
// 客户端调用
public class PaymentService {
public boolean pay(String paymentType) {
PaymentTemplate payment = PaymentFactory.createPayment(paymentType);
return payment.processPayment();
}
}
优势:
- 将对象创建与使用分离
- 符合单一职责原则
技巧二:钩子方法的妙用(灰度发布场景)
java
public abstract class PaymentTemplate {
// 主模板方法
public final void processPayment() {
init();
validate();
if (needRiskCheck()) { // 钩子方法
riskCheck();
}
execute();
notify();
}
// 钩子方法(默认需要风控)
protected boolean needRiskCheck() {
return true;
}
}
// 特定支付方式跳过风控
public class InternalPayment extends PaymentTemplate {
@Override
protected boolean needRiskCheck() {
return false; // 内部支付不风控
}
}
应用场景:
- 新功能灰度发布
- 特定渠道特殊处理
技巧三:模板方法模式+策略模式混合使用
java
public abstract class ReportGenerator {
// 模板方法
public final File generate() {
Data data = fetchData();
processedData = process(data);
return export(processedData);
}
// 策略接口
protected abstract DataProcessor getProcessor();
private Data process(Data data) {
return getProcessor().process(data);
}
}
// 具体报表实现
public class SalesReport extends ReportGenerator {
@Override
protected DataProcessor getProcessor() {
return new SalesDataProcessor(); // 策略实现
}
}
优势对比:
维度 | 纯模板方法 | 模板+策略 |
---|---|---|
类数量 | 每个报表一个类 | N个报表 + M个处理器 |
扩展性 | 修改步骤需改父类 | 新增策略类即可 |
适合场景 | 步骤固定且少变化 | 处理逻辑变化频繁 |
🚀 高并发场景优化方案
问题 :模板方法中的同步控制
解决方案:模板方法与ThreadLocal结合
java
public abstract class PaymentTemplate {
private ThreadLocal<PaymentContext> context = new ThreadLocal<>();
public final boolean processPayment() {
try {
context.set(new PaymentContext());
validate(context.get());
execute(context.get());
return true;
} finally {
context.remove(); // 防止内存泄漏
}
}
}
// 子类通过context获取参数
class WechatPayment extends PaymentTemplate {
protected void execute(PaymentContext ctx) {
String openId = ctx.get("openId");
// 业务逻辑
}
}
优势:
- 线程安全的参数传递
- 避免方法参数膨胀
🔮 未来演进方向
- 模板方法微服务化
- 与工作流引擎集成
java
// 使用Activiti实现动态流程
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
RuntimeService runtimeService = engine.getRuntimeService();
runtimeService.startProcessInstanceByKey("paymentProcess");
- AOP增强模板方法
java
@Aspect
public class PaymentMonitor {
@Around("execution(* PaymentTemplate.processPayment(..))")
public Object monitor(ProceedingJoinPoint pjp) {
long start = System.currentTimeMillis();
try {
return pjp.proceed();
} finally {
log.info("支付耗时:{}ms", System.currentTimeMillis()-start);
}
}
}