模板方法模式:把算法骨架焊死,把细节留给孩子
说实话,模板方法模式是我在重构老系统时用得最多的模式之一。它不花哨,但特别实在。你想想看,当你在一个团队里维护一套代码,发现好几个类都在做类似的事情——步骤差不多,就是中间某几步不一样。这时候,模板方法就该登场了。
它的核心思想很简单:在父类里定义好算法的骨架,把某些步骤的实现延迟到子类中。说白了,就是「流程我定好了,具体怎么做你们看着办」。
一句话总结:模板方法模式让你在不改变算法结构的前提下,允许子类重新定义算法中的某些特定步骤。
为什么需要它?
我在几年前接手过一个支付系统。当时系统支持微信支付、支付宝、银联三种渠道。每个渠道的支付流程其实差不多:
- 校验参数
- 构建请求数据
- 调用第三方接口
- 处理响应
- 更新订单状态
但每个渠道的签名算法、请求格式、回调处理都不一样。一开始,团队里每个人各写各的,代码里充斥着大量重复的「校验-构建-调用-处理」逻辑。后来我实在看不下去了,用模板方法模式重构了一遍。嗯,效果立竿见影。
我的习惯:只要发现两个以上的类在做「步骤相同、细节不同」的事情,我就会考虑模板方法模式。它比复制粘贴强一百倍。
模式结构
模板方法模式涉及两个角色:
- 抽象类(AbstractClass):定义算法的骨架,包含一个模板方法和若干个基本方法。
- 具体类(ConcreteClass):实现抽象类中定义的基本方法,完成算法中可变的部分。
这里有个关键点:模板方法一般用 final 修饰,防止子类重写整个流程。基本方法则分为三类:
| 类型 | 说明 | 示例 |
|---|---|---|
| 抽象方法 | 必须由子类实现 | signData() |
| 具体方法 | 父类已实现,子类可选覆盖 | validateParams() |
| 钩子方法 | 默认空实现,子类按需覆盖 | beforeRequest() |
代码示例:支付流程
咱们直接看代码。这是我当年重构支付系统时的简化版本:
// 抽象类:定义支付流程骨架
public abstract class PaymentProcessor {
// 模板方法,用 final 防止子类篡改流程
public final void processPayment(Order order) {
validateOrder(order);
String requestData = buildRequest(order);
String response = callThirdParty(requestData);
handleResponse(response);
updateOrderStatus(order);
}
// 具体方法:参数校验,所有渠道都一样
private void validateOrder(Order order) {
if (order == null || order.getAmount() <= 0) {
throw new IllegalArgumentException("订单无效");
}
System.out.println("订单校验通过");
}
// 抽象方法:构建请求数据,各渠道不同
protected abstract String buildRequest(Order order);
// 抽象方法:调用第三方接口,各渠道不同
protected abstract String callThirdParty(String requestData);
// 钩子方法:处理响应前的预处理,子类可选实现
protected void beforeHandleResponse(String response) {
// 默认什么都不做
}
// 具体方法:处理响应,大部分渠道逻辑相同
private void handleResponse(String response) {
beforeHandleResponse(response); // 调用钩子
if (response.contains("success")) {
System.out.println("支付成功");
} else {
System.out.println("支付失败");
}
}
// 具体方法:更新订单状态
private void updateOrderStatus(Order order) {
order.setStatus("PAID");
System.out.println("订单状态已更新");
}
}
// 具体类:微信支付
public class WechatPayment extends PaymentProcessor {
@Override
protected String buildRequest(Order order) {
// 微信特有的 XML 格式
return "<xml><appid>wx123</appid><total_fee>"
+ order.getAmount() + "</total_fee></xml>";
}
@Override
protected String callThirdParty(String requestData) {
System.out.println("调用微信支付接口");
return "success";
}
@Override
protected void beforeHandleResponse(String response) {
System.out.println("微信支付:验证签名");
}
}
// 具体类:支付宝支付
public class AlipayPayment extends PaymentProcessor {
@Override
protected String buildRequest(Order order) {
// 支付宝特有的 JSON 格式
return "{\"app_id\":\"alipay456\",\"total_amount\":\""
+ order.getAmount() + "\"}";
}
@Override
protected String callThirdParty(String requestData) {
System.out.println("调用支付宝接口");
return "success";
}
}
注意:我曾经见过有人把模板方法写成非 final 的,结果子类直接重写了整个流程,把骨架改得面目全非。这完全违背了模板方法模式的初衷。记住:模板方法必须是 final 的,除非你有非常特殊的理由。
钩子方法:给子类留个「后门」
你可能会问:如果子类想在某个步骤前后加点自己的逻辑,但又不想破坏骨架,怎么办?
答案就是钩子方法。它是个空方法,子类可以按需覆盖。比如上面的 beforeHandleResponse(),微信支付用它来验证签名,支付宝不需要就直接跳过。这种设计非常灵活,我特别喜欢。
我个人习惯在模板方法里放两三个钩子:一个在开始前,一个在关键步骤后,一个在结束前。这样既保证了流程的稳定性,又给了子类足够的扩展空间。
SVG 流程图:模板方法模式的核心逻辑
什么时候用?什么时候别用?
我总结了几条经验,供你参考:
- 适合用:多个类有相同的算法步骤,只是某些步骤的实现不同。比如数据导入导出、批处理任务、测试用例执行流程。
- 不适合用:算法步骤经常变化,或者子类需要大幅修改流程。这时候用模板方法反而会绑死你。
- 避坑:不要为了用模式而用模式。我曾经见过有人把只有两个步骤的简单逻辑也套上模板方法,结果代码反而更复杂了。
我的建议:模板方法模式最适合「80%相同,20%不同」的场景。如果相同部分少于50%,不如考虑策略模式或者组合模式。
与策略模式的区别
很多人会把模板方法和策略模式搞混。我简单说一下:
- 模板方法:在父类控制流程,子类实现细节。是「继承」的体现。
- 策略模式:把算法封装成独立的策略类,通过组合来替换整个算法。是「组合」的体现。
说白了,模板方法是在「流程不变、细节可变」的场景下用的。策略模式是在「整个算法都可以替换」的场景下用的。两者各有千秋,选对场景才是关键。
最后说两句
模板方法模式是我个人非常喜欢的一个模式。它不复杂,但很实用。尤其是在做框架设计或者重构遗留系统时,它能帮你把重复的流程收拢到一处,让代码变得清晰可控。
嗯,如果你在项目中遇到「流程固定、实现多变」的场景,不妨试试模板方法模式。它不会让你失望的。
公众号:蓝海资料掘金营,微信deep3321