模板方法用组合还是继承?多平台电子面单的抉择

模板方法用组合还是继承?多平台电子面单的抉择

摘要:模板方法模式通常用抽象类定义算法骨架,但在多平台电子面单架构中,我们却选择了“组合”方式——WaybillFetchTemplate通过注入策略对象来固定流程,而非让子类继承。本文从真实代码出发,对比两种实现方式,拆解为什么在这个场景下“组合优于继承”,并总结出模板方法选型的决策框架。

📖系列导航

  • 系列开篇:从“能跑就行”到“整洁架构”
  • 上一篇:解析器职责分离改造
  • 本文:模板方法的组合与继承抉择
  • 后续:京东、拼多多等平台专项篇

文章目录

  • 模板方法用组合还是继承?多平台电子面单的抉择
    • 一、一个经典问题:模板方法怎么写?
    • 二、我们的选择:组合式模板
    • 三、为什么不用继承?四个核心理由
      • 理由一:策略已经用接口独立了,继承会造成冗余
      • 理由二:避免类爆炸
      • 理由三:运行时灵活性
      • 理由四:更好的可测试性
    • 四、对比表格:继承 vs 组合实现模板方法
    • 五、什么时候该用继承?
    • 六、延伸:顺丰子母件——模板层分支的威力
    • 七、总结:模板方法选型决策框架
    • 八、系列导航与参考
    • 延伸阅读:Java 23种设计模式实战系列
    • 九、一起交流,共同进步

一、一个经典问题:模板方法怎么写?

模板方法模式(Template Method Pattern)是 GoF 23 种设计模式中最简单也最常用的行为型模式之一。它的核心思想是:在父类中定义算法骨架,将某些步骤延迟到子类实现

教科书上的标准实现通常是这样的:

// 抽象父类:定义算法骨架publicabstractclassAbstractWaybillFetcher{// 模板方法:固定流程publicfinalvoidfetch(){validate();// 1. 校验Objectrequest=buildRequest();// 2. 构建请求Stringresponse=callApi(request);// 3. 调用APIparseResponse(response);// 4. 解析响应save();// 5. 持久化}privatevoidvalidate(){/* 通用校验 */}protectedabstractObjectbuildRequest();// 子类实现protectedabstractvoidparseResponse(Stringresponse);protectedabstractvoidsave();protectedStringcallApi(Objectrequest){/* 通用调用 */return"";}}// 子类:奇门实现publicclassQiMenWaybillFetcherextendsAbstractWaybillFetcher{@OverrideprotectedObjectbuildRequest(){/* 构建奇门SDK请求 */}@OverrideprotectedvoidparseResponse(Stringresponse){/* 解析奇门响应 */}@Overrideprotectedvoidsave(){/* 保存运单 */}}

这种写法用了二十多年,看起来毫无问题。但在我们的多平台电子面单架构中,却主动放弃了这个“标准答案”,选择了一个看似“离经叛道”的做法:用组合代替继承

🏭设计模式视角:模板方法模式是 GoF 23 种行为型模式之一,理解它的两种实现方式(继承 vs 组合)对于判断“何时用继承、何时用组合”至关重要。在《Java 23种设计模式:从踩坑到精通》系列的第22篇(策略模式)第23篇(模板方法模式)中,我深度对比了两种模式的适用场景与选型边界,欢迎延伸阅读。


二、我们的选择:组合式模板

在我们的架构中,模板类WaybillFetchTemplate是一个普通的、非抽象的类,它不要求任何子类继承它:

publicclassWaybillFetchTemplate{privatefinalRequestStrategyrequestStrategy;privatefinalParseStrategyparseStrategy;privatefinalExceptionStrategyexceptionStrategy;privatefinalApiInvokerapiInvoker;privatefinalWaybillPersistencepersistence;// 通过构造函数注入三个策略对象publicWaybillFetchTemplate(RequestStrategyreq,ParseStrategyparse,ExceptionStrategyex,ApiInvokerinvoker,WaybillPersistencepersist){this.requestStrategy=req;this.parseStrategy=parse;this.exceptionStrategy=ex;this.apiInvoker=invoker;this.persistence=persist;}publicbooleanexecute(WaybillContextctx){// 1. 构建请求(策略)Objectrequest=requestStrategy.buildRequest(ctx);// 2. 调用API(ApiInvoker)Stringresponse=apiInvoker.invoke(ctx,request,traceId);// 3. 业务异常判断(策略)if(!exceptionStrategy.isBusinessSuccess(response)){markException(ctx.getTicket(),exceptionStrategy.extractErrorMsg(response));returnfalse;}// 4. 解析响应(策略)List<Detail>details=parseStrategy.parseResponse(ctx,response);if(details.isEmpty()){markException(ctx.getTicket(),"未获取到运单号");returnfalse;}// 5. 持久化persistence.saveAndBind(ctx.getTicket(),details,ctx.isFirst());returntrue;}}

使用时,通过工厂注入不同平台的策略实例:

// 奇门RequestStrategyreq=newQiMenRequestStrategy();ParseStrategyparse=newQiMenParseStrategy();ExceptionStrategyex=newQiMenExceptionStrategy();WaybillFetchTemplatetemplate=newWaybillFetchTemplate(req,parse,ex,invoker,persistence);template.execute(ctx);// 抖音req=newDouYinRequestStrategy();parse=newDouYinParseStrategy();ex=newDouYinExceptionStrategy();template=newWaybillFetchTemplate(req,parse,ex,invoker,persistence);template.execute(ctx);

可以看到,WaybillFetchTemplate没有任何abstract方法,也没有任何子类。它的“可变部分”全部通过构造函数注入的策略对象来实现。


三、为什么不用继承?四个核心理由

理由一:策略已经用接口独立了,继承会造成冗余

在我们的架构中,平台差异已经被三个策略接口完美隔离:

publicinterfaceRequestStrategy{ObjectbuildRequest(WaybillContextctx);}publicinterfaceParseStrategy{List<Detail>parseResponse(WaybillContextctx,Stringresponse);}publicinterfaceExceptionStrategy{booleanisBusinessSuccess(Stringresponse);}

如果再用一个抽象的AbstractWaybillFetchTemplate让各平台子类去实现,等于把同样的差异逻辑在两个地方重复定义。子类既要实现策略接口,又要覆写模板的抽象方法——这是典型的“过度抽象”。

理由二:避免类爆炸

假设我们用继承方式实现,每接入一个新平台就要写一个子类:

AbstractWaybillFetchTemplate ├── QiMenWaybillFetchTemplate ├── DouYinWaybillFetchTemplate ├── DouYinDaiFaWaybillFetchTemplate ├── JDWaybillFetchTemplate └── PDDWaybillFetchTemplate

十几个平台就是十几个子类,而它们之间的唯一区别只是策略不同。用组合,只需一个WaybillFetchTemplate,通过注入不同策略实例即可覆盖所有平台。

理由三:运行时灵活性

组合允许在运行时动态替换策略。比如,通过工厂根据平台编码返回不同的策略组合:

RequestStrategyreq=strategyFactory.getRequestStrategy(platformCode,original);ParseStrategyparse=strategyFactory.getParseStrategy(platformCode,original);ExceptionStrategyex=strategyFactory.getExceptionStrategy(platformCode,original);WaybillFetchTemplatetemplate=newWaybillFetchTemplate(req,parse,ex,...);

继承的类关系在编译期就固定了,无法做到这种运行时动态组装。

理由四:更好的可测试性

组合式模板可以单独测试,注入 Mock 的策略对象即可,无需启动 Spring 容器或继承任何基类:

@TestpublicvoidtestTemplate(){RequestStrategymockReq=ctx->"mock request";ParseStrategymockParse=(ctx,resp)->Collections.singletonList(mockDetail);ExceptionStrategymockEx=resp->true;WaybillFetchTemplatetemplate=newWaybillFetchTemplate(mockReq,mockParse,mockEx,mockInvoker,mockPersistence);booleanresult=template.execute(ctx);assertTrue(result);}

如果用继承,测试时需要创建匿名子类或启动整个依赖链,成本高得多。


四、对比表格:继承 vs 组合实现模板方法

维度继承方式组合方式
代码量每个平台一个子类一个模板类 + 策略接口
扩展性新增平台需新增子类新增平台只需新增策略实现
灵活性编译期绑定运行时动态替换
测试性需创建子类实例直接 Mock 策略注入
类数量N 个平台 → N 个子类1 个模板类
复用性子类间无法复用策略策略可跨平台组合
适合场景步骤固定、差异集中、子类数量少步骤固定、差异分散、实现类数量多

五、什么时候该用继承?

组合不是银弹。在以下场景中,继承方式仍然更合适:

  • 需要共享实例状态:比如父类持有commonDaologger等字段,子类直接使用。这正是我们架构中DefaultBaseManager用抽象类的原因。
  • 子类数量极少:只有两三个子类时,继承的简洁性优于组合的灵活性。
  • 步骤之间有强关联:模板的某些步骤需要访问父类的内部状态,不适合拆分成独立接口。

在我们的电子面单架构中,策略已经通过接口独立,且平台数量可能增长到十几个,组合的优势远大于继承。


六、延伸:顺丰子母件——模板层分支的威力

组合式模板还有一个额外优势:在模板层增加流程分支非常自然

顺丰超过 10 件需要走子母件模式(分批调用 API),我们的处理方式是直接在WaybillFetchTemplate中增加一个executeSFMoreTen方法:

publicbooleanexecute(WaybillContextctx){if(isSFMoreTen(ctx)){returnexecuteSFMoreTen(ctx,traceId);// 子母件分支}returnexecuteNormal(ctx,traceId);// 普通分支}

如果用继承,子母件逻辑要么重复在每个子类中实现,要么在父类中增加复杂的分支判断,很容易失控。组合方式让模板层成为唯一的流程控制点,分支逻辑清晰可维护。


七、总结:模板方法选型决策框架

结合我们的实战经验,可以提炼出以下决策框架:

  1. 先看差异是否已经抽象成接口:如果平台差异已经通过策略接口独立,优先用组合。
  2. 看平台数量:超过 5 个平台时,组合避免类爆炸的优势明显。
  3. 看是否需要运行时灵活替换:需要则用组合,不需要则继承也可。
  4. 看是否需要共享实例状态:如果需要且差异未接口化,继承更合适。

面试绝杀一句话:模板方法模式的核心是“固定流程骨架,差异化具体步骤”。这个骨架既可以用继承实现,也可以用组合实现。在我们的多平台电子面单架构中,因为平台差异已经通过三层策略接口独立,组合方式更灵活、更简洁。


八、系列导航与参考

本篇文章是「电商多平台电子面单对接实战」的第八篇(设计模式篇),聚焦模板方法模式在真实项目中的选型实践。

系列文章目录

  • 开篇:从“能跑就行”到“整洁架构”
  • 第一篇:奇门对接顺丰电子面单
  • 第二篇:抖音代发电子面单对接
  • 第三篇:抖音普通订单电子面单对接
  • 第四篇:多平台统一架构设计
  • 第五篇:策略工厂复合Key路由改造
  • 第六篇:快递公司前置校验改造
  • 第七篇:解析器职责分离改造
  • 第八篇:模板方法的组合与继承抉择(本文)
  • 后续:京东、拼多多等平台专项篇

延伸阅读:Java 23种设计模式实战系列

本文中模板方法选型的核心——组合优于继承,以及背后涉及的策略模式、模板方法模式等设计理念,在《Java 23种设计模式:从踩坑到精通》系列中有更体系化的讲解。如果你对以下问题感兴趣,推荐延伸阅读:

  • 策略模式 vs 模板方法模式:两者如何选择?各适用于什么场景?
  • 组合优于继承:六大设计原则中的这一条,在真实项目中如何落地?
  • 工厂模式 + 策略模式:如何配合实现复合Key路由?

📖《Java 23 种设计模式:从踩坑到精通》

  • 系列开篇:从踩坑到精通 —— 总览与导航
  • 策略模式 —— 算法族的封装与切换
  • 模板方法模式 —— 定义算法骨架,交给子类填充细节

💡学习建议:电子面单系列侧重业务落地,设计模式系列侧重理论体系。两者搭配阅读,既能应对面试,又能反哺项目,形成“理论→实战”的闭环。


九、一起交流,共同进步

技术之路,一个人走得快,一群人走得远。
如果您在项目中也有过“继承还是组合”的纠结,希望本文的实战经验能给您带来启发。

  • 📌关注我:点击上方“关注”,第一时间获取系列更新推送。
  • 💬留言讨论:您在实际项目中选择过组合式模板方法吗?遇到过什么坑?欢迎在评论区分享。
  • 🔗分享转发:如果本文对您有帮助,请点赞收藏分享,让更多同行看到。

标签#Java#设计模式#模板方法模式#组合优于继承#电子面单#架构设计#重构