模板方法用组合还是继承?多平台电子面单的抉择
摘要:模板方法模式通常用抽象类定义算法骨架,但在多平台电子面单架构中,我们却选择了"组合"方式------
WaybillFetchTemplate通过注入策略对象来固定流程,而非让子类继承。本文从真实代码出发,对比两种实现方式,拆解为什么在这个场景下"组合优于继承",并总结出模板方法选型的决策框架。
📖 系列导航
- 系列开篇:从"能跑就行"到"整洁架构"
- 上一篇:解析器职责分离改造
- 本文:模板方法的组合与继承抉择
- 后续:京东、拼多多等平台专项篇
文章目录
- 模板方法用组合还是继承?多平台电子面单的抉择
-
- 一、一个经典问题:模板方法怎么写?
- 二、我们的选择:组合式模板
- 三、为什么不用继承?四个核心理由
- [四、对比表格:继承 vs 组合实现模板方法](#四、对比表格:继承 vs 组合实现模板方法)
- 五、什么时候该用继承?
- 六、延伸:顺丰子母件------模板层分支的威力
- 七、总结:模板方法选型决策框架
- 八、系列导航与参考
- [延伸阅读:Java 23种设计模式实战系列](#延伸阅读:Java 23种设计模式实战系列)
- 九、一起交流,共同进步
一、一个经典问题:模板方法怎么写?
模板方法模式(Template Method Pattern)是 GoF 23 种设计模式中最简单也最常用的行为型模式之一。它的核心思想是:在父类中定义算法骨架,将某些步骤延迟到子类实现。
教科书上的标准实现通常是这样的:
java
// 抽象父类:定义算法骨架
public abstract class AbstractWaybillFetcher {
// 模板方法:固定流程
public final void fetch() {
validate(); // 1. 校验
Object request = buildRequest(); // 2. 构建请求
String response = callApi(request); // 3. 调用API
parseResponse(response); // 4. 解析响应
save(); // 5. 持久化
}
private void validate() { /* 通用校验 */ }
protected abstract Object buildRequest(); // 子类实现
protected abstract void parseResponse(String response);
protected abstract void save();
protected String callApi(Object request) { /* 通用调用 */ return ""; }
}
// 子类:奇门实现
public class QiMenWaybillFetcher extends AbstractWaybillFetcher {
@Override
protected Object buildRequest() { /* 构建奇门SDK请求 */ }
@Override
protected void parseResponse(String response) { /* 解析奇门响应 */ }
@Override
protected void save() { /* 保存运单 */ }
}
这种写法用了二十多年,看起来毫无问题。但在我们的多平台电子面单架构中,却主动放弃了这个"标准答案",选择了一个看似"离经叛道"的做法:用组合代替继承。
🏭 设计模式视角 :模板方法模式是 GoF 23 种行为型模式之一,理解它的两种实现方式(继承 vs 组合)对于判断"何时用继承、何时用组合"至关重要。在《Java 23种设计模式:从踩坑到精通》系列的 第22篇(策略模式) 和 第23篇(模板方法模式) 中,我深度对比了两种模式的适用场景与选型边界,欢迎延伸阅读。
二、我们的选择:组合式模板
在我们的架构中,模板类 WaybillFetchTemplate 是一个普通的、非抽象的类,它不要求任何子类继承它:
java
public class WaybillFetchTemplate {
private final RequestStrategy requestStrategy;
private final ParseStrategy parseStrategy;
private final ExceptionStrategy exceptionStrategy;
private final ApiInvoker apiInvoker;
private final WaybillPersistence persistence;
// 通过构造函数注入三个策略对象
public WaybillFetchTemplate(RequestStrategy req, ParseStrategy parse,
ExceptionStrategy ex, ApiInvoker invoker, WaybillPersistence persist) {
this.requestStrategy = req;
this.parseStrategy = parse;
this.exceptionStrategy = ex;
this.apiInvoker = invoker;
this.persistence = persist;
}
public boolean execute(WaybillContext ctx) {
// 1. 构建请求(策略)
Object request = requestStrategy.buildRequest(ctx);
// 2. 调用API(ApiInvoker)
String response = apiInvoker.invoke(ctx, request, traceId);
// 3. 业务异常判断(策略)
if (!exceptionStrategy.isBusinessSuccess(response)) {
markException(ctx.getTicket(), exceptionStrategy.extractErrorMsg(response));
return false;
}
// 4. 解析响应(策略)
List<Detail> details = parseStrategy.parseResponse(ctx, response);
if (details.isEmpty()) {
markException(ctx.getTicket(), "未获取到运单号");
return false;
}
// 5. 持久化
persistence.saveAndBind(ctx.getTicket(), details, ctx.isFirst());
return true;
}
}
使用时,通过工厂注入不同平台的策略实例:
java
// 奇门
RequestStrategy req = new QiMenRequestStrategy();
ParseStrategy parse = new QiMenParseStrategy();
ExceptionStrategy ex = new QiMenExceptionStrategy();
WaybillFetchTemplate template = new WaybillFetchTemplate(req, parse, ex, invoker, persistence);
template.execute(ctx);
// 抖音
req = new DouYinRequestStrategy();
parse = new DouYinParseStrategy();
ex = new DouYinExceptionStrategy();
template = new WaybillFetchTemplate(req, parse, ex, invoker, persistence);
template.execute(ctx);
可以看到,WaybillFetchTemplate 没有任何 abstract 方法,也没有任何子类。它的"可变部分"全部通过构造函数注入的策略对象来实现。
三、为什么不用继承?四个核心理由
理由一:策略已经用接口独立了,继承会造成冗余
在我们的架构中,平台差异已经被三个策略接口完美隔离:
java
public interface RequestStrategy { Object buildRequest(WaybillContext ctx); }
public interface ParseStrategy { List<Detail> parseResponse(WaybillContext ctx, String response); }
public interface ExceptionStrategy { boolean isBusinessSuccess(String response); }
如果再用一个抽象的 AbstractWaybillFetchTemplate 让各平台子类去实现,等于把同样的差异逻辑在两个地方重复定义。子类既要实现策略接口,又要覆写模板的抽象方法------这是典型的"过度抽象"。
理由二:避免类爆炸
假设我们用继承方式实现,每接入一个新平台就要写一个子类:
AbstractWaybillFetchTemplate
├── QiMenWaybillFetchTemplate
├── DouYinWaybillFetchTemplate
├── DouYinDaiFaWaybillFetchTemplate
├── JDWaybillFetchTemplate
└── PDDWaybillFetchTemplate
十几个平台就是十几个子类,而它们之间的唯一区别只是策略不同。用组合,只需一个 WaybillFetchTemplate,通过注入不同策略实例即可覆盖所有平台。
理由三:运行时灵活性
组合允许在运行时动态替换策略。比如,通过工厂根据平台编码返回不同的策略组合:
java
RequestStrategy req = strategyFactory.getRequestStrategy(platformCode, original);
ParseStrategy parse = strategyFactory.getParseStrategy(platformCode, original);
ExceptionStrategy ex = strategyFactory.getExceptionStrategy(platformCode, original);
WaybillFetchTemplate template = new WaybillFetchTemplate(req, parse, ex, ...);
继承的类关系在编译期就固定了,无法做到这种运行时动态组装。
理由四:更好的可测试性
组合式模板可以单独测试,注入 Mock 的策略对象即可,无需启动 Spring 容器或继承任何基类:
java
@Test
public void testTemplate() {
RequestStrategy mockReq = ctx -> "mock request";
ParseStrategy mockParse = (ctx, resp) -> Collections.singletonList(mockDetail);
ExceptionStrategy mockEx = resp -> true;
WaybillFetchTemplate template = new WaybillFetchTemplate(mockReq, mockParse, mockEx, mockInvoker, mockPersistence);
boolean result = template.execute(ctx);
assertTrue(result);
}
如果用继承,测试时需要创建匿名子类或启动整个依赖链,成本高得多。
四、对比表格:继承 vs 组合实现模板方法
| 维度 | 继承方式 | 组合方式 |
|---|---|---|
| 代码量 | 每个平台一个子类 | 一个模板类 + 策略接口 |
| 扩展性 | 新增平台需新增子类 | 新增平台只需新增策略实现 |
| 灵活性 | 编译期绑定 | 运行时动态替换 |
| 测试性 | 需创建子类实例 | 直接 Mock 策略注入 |
| 类数量 | N 个平台 → N 个子类 | 1 个模板类 |
| 复用性 | 子类间无法复用策略 | 策略可跨平台组合 |
| 适合场景 | 步骤固定、差异集中、子类数量少 | 步骤固定、差异分散、实现类数量多 |
五、什么时候该用继承?
组合不是银弹。在以下场景中,继承方式仍然更合适:
- 需要共享实例状态 :比如父类持有
commonDao、logger等字段,子类直接使用。这正是我们架构中DefaultBaseManager用抽象类的原因。 - 子类数量极少:只有两三个子类时,继承的简洁性优于组合的灵活性。
- 步骤之间有强关联:模板的某些步骤需要访问父类的内部状态,不适合拆分成独立接口。
在我们的电子面单架构中,策略已经通过接口独立,且平台数量可能增长到十几个,组合的优势远大于继承。
六、延伸:顺丰子母件------模板层分支的威力
组合式模板还有一个额外优势:在模板层增加流程分支非常自然。
顺丰超过 10 件需要走子母件模式(分批调用 API),我们的处理方式是直接在 WaybillFetchTemplate 中增加一个 executeSFMoreTen 方法:
java
public boolean execute(WaybillContext ctx) {
if (isSFMoreTen(ctx)) {
return executeSFMoreTen(ctx, traceId); // 子母件分支
}
return executeNormal(ctx, traceId); // 普通分支
}
如果用继承,子母件逻辑要么重复在每个子类中实现,要么在父类中增加复杂的分支判断,很容易失控。组合方式让模板层成为唯一的流程控制点,分支逻辑清晰可维护。
七、总结:模板方法选型决策框架
结合我们的实战经验,可以提炼出以下决策框架:
- 先看差异是否已经抽象成接口:如果平台差异已经通过策略接口独立,优先用组合。
- 看平台数量:超过 5 个平台时,组合避免类爆炸的优势明显。
- 看是否需要运行时灵活替换:需要则用组合,不需要则继承也可。
- 看是否需要共享实例状态:如果需要且差异未接口化,继承更合适。
面试绝杀一句话:模板方法模式的核心是"固定流程骨架,差异化具体步骤"。这个骨架既可以用继承实现,也可以用组合实现。在我们的多平台电子面单架构中,因为平台差异已经通过三层策略接口独立,组合方式更灵活、更简洁。
八、系列导航与参考
本篇文章是「电商多平台电子面单对接实战」的第八篇(设计模式篇),聚焦模板方法模式在真实项目中的选型实践。
系列文章目录:
- 开篇:从"能跑就行"到"整洁架构"
- 第一篇:奇门对接顺丰电子面单
- 第二篇:抖音代发电子面单对接
- 第三篇:抖音普通订单电子面单对接
- 第四篇:多平台统一架构设计
- 第五篇:策略工厂复合Key路由改造
- 第六篇:快递公司前置校验改造
- 第七篇:解析器职责分离改造
- 第八篇:模板方法的组合与继承抉择(本文)
- 第九篇:API调用调度层Handler分组设计
- 第十篇:奇门 trade_order_list 排查实录
- 第十一篇:数据库查询优化让多包裹取号快一倍
- 第十二篇:两次架构升级完整复盘
- 第十三篇:常量与配置集中管控改造
- 后续:京东、拼多多等平台专项篇
延伸阅读:Java 23种设计模式实战系列
本文中模板方法选型的核心------组合优于继承,以及背后涉及的策略模式、模板方法模式等设计理念,在《Java 23种设计模式:从踩坑到精通》系列中有更体系化的讲解。如果你对以下问题感兴趣,推荐延伸阅读:
- 策略模式 vs 模板方法模式:两者如何选择?各适用于什么场景?
- 组合优于继承:六大设计原则中的这一条,在真实项目中如何落地?
- 工厂模式 + 策略模式:如何配合实现复合Key路由?
📖 《Java 23 种设计模式:从踩坑到精通》
- 系列开篇:从踩坑到精通 ------ 总览与导航
- 策略模式 ------ 算法族的封装与切换
- 模板方法模式 ------ 定义算法骨架,交给子类填充细节
💡 学习建议 :电子面单系列侧重业务落地 ,设计模式系列侧重理论体系。两者搭配阅读,既能应对面试,又能反哺项目,形成"理论→实战"的闭环。
九、一起交流,共同进步
技术之路,一个人走得快,一群人走得远。
如果您在项目中也有过"继承还是组合"的纠结,希望本文的实战经验能给您带来启发。
- 📌 关注我 :点击上方"关注",第一时间获取系列更新推送。
- 💬 留言讨论:您在实际项目中选择过组合式模板方法吗?遇到过什么坑?欢迎在评论区分享。
- 🔗 分享转发 :如果本文对您有帮助,请 点赞 、收藏 、分享,让更多同行看到。
标签 :#Java #设计模式 #模板方法模式 #组合优于继承 #电子面单 #架构设计 #重构