模板方法模式实战:为什么组合优于继承? 模板方法模式实战为什么组合优于继承「Java面试·实战笔记」系列第 3 篇第1篇《接口与抽象类到底怎么分通俗比喻基础选型框架》 —— 用快递家族生活化比喻讲清概念扫清八股误区。第2篇《从多平台电子面单架构看接口与抽象类的真实选型》 —— 基于生产代码拆解 DefaultBaseManager、三层策略接口、组合式模板的选型决策。本文是第3篇聚焦前两篇留下的核心悬念——模板方法用继承还是组合深挖两种实现方案的差异对比继承耦合缺陷与组合模式的扩展性附完整可运行Demo。这一篇会直接拆解真实生产代码DefaultBaseManager为什么是抽象类、三层策略为什么是接口、模板方法为什么用组合实现——这些来自日均10万订单的电子面单系统AI绝对给不出这种级别的生产案例。关于“组合优于继承”继承和组合并非对立而是各有分工。在本文的架构中继承管基础设施DAO、日志、事务组合管业务编排取号流程。标题中的“组合优于继承”特指在模板方法模式的实现上组合比继承更灵活。下文会详细展开。⚠️阅读提示如果你读完后产生疑问——“这到底算模板方法模式还是策略模式”——可以先跳到第3节末尾的「设计说明」和第4.1节的总结段落。简单说策略模式负责“可变步骤的具体实现”模板方法模式负责“固定流程的统一编排”二者是互补关系。而模板方法这个“固定流程”既可以用继承实现教科书也可以用组合实现本文。只要遵循“固定流程骨架差异化具体步骤”的思想就是模板方法模式的落地。文章目录模板方法模式实战为什么组合优于继承1. 回顾前两篇讲了什么这一篇要解决什么2. 抽象类的正确用法基础设施复用不是业务模板3. 接口的正确用法隔离平台差异4. 模板方法教科书用继承我们选了组合4.1 教科书的标准写法4.2 我们的实际写法4.3 为什么这里组合更好4.4 继承式模板方法的唯一适用场景5. 组合式模板的额外优势顺丰子母件可运行Demo组合式模板方法最小示例6. 面试实战模板方法模式如何回答才能拿高分6.1 面试官问模板方法模式在项目中用过吗怎么用的6.2 面试官追问组合式模板方法和策略模式有什么区别你这不是策略模式吗6.3 面试官追问为什么不直接用继承Java不是单继承吗用接口组合不是更受限吗6.4 面试场景模拟面试官连环追问7. 总结1. 回顾前两篇讲了什么这一篇要解决什么在第1篇中我们用快递家族抽象类和能力合同接口的比喻建立了基础认知抽象类is-a 关系适合模板方法和状态共享接口can-do 关系适合行为契约和多态解耦在第2篇中我们深入真实生产代码看到了这些概念在架构中的实际落位——DefaultBaseManager作为抽象类提供基础设施三层策略接口隔离平台差异。但第2篇留下了一个核心悬念WaybillFetchTemplate这个流程编排器为什么没有用教科书式的抽象类继承而是用了组合方式这一篇就来彻底讲透这个问题。真实项目中我们面对的是这样的场景需要对接奇门、抖音、京东、拼多多等十几个电商平台每个平台的取号流程完全一样校验→构建请求→调用API→判断成功→解析→持久化但每一步的具体实现各不相同奇门用淘宝SDK、抖音用HTTP签名、京东有自己的一套鉴权问题来了这个“固定流程可变步骤”的场景教科书上告诉你要用模板方法模式。但模板方法用继承实现还是用组合实现我们的选择可能和你在教科书上学到的不一样。在看具体的代码之前先给出一张决策表帮你快速判断自己的场景该选哪种场景选型需要共享实例字段DAO、Logger等抽象类继承流程固定、步骤可变但步骤已接口化组合2. 抽象类的正确用法基础设施复用不是业务模板在我们的架构中只有一个抽象类被广泛继承DefaultBaseManager。它的核心定义如下publicabstractclassDefaultBaseManager{AutowiredprotectedCommonDaocommonDao;// 数据库访问对象protectedLogloggerLogFactory.getLog(getClass());// org.apache.commons.logging.LogprotectedTransactionStatusbeginTxPropagationRequiresNew(){...}protectedvoidcommitTransaction(TransactionStatusstatus){...}protectedvoidrollbackTransaction(TransactionStatusstatus){...}}所有的业务服务都继承它publicclassWaybillFetchServiceextendsDefaultBaseManager{publicbooleanfetchWaybill(TocWmsPickTicketticket,...){// 直接使用父类的 commonDaoctx.getExt().put(WaybillContext.KEY_COMMON_DAO,this.commonDao);// 直接使用父类的 loggerlogger.info(开始取号...);// 直接使用父类的事务管理TransactionStatusstatusbeginTxPropagationRequiresNew();}}为什么它是抽象类因为它需要共享实例状态。commonDao是一个实例字段由Spring注入所有子类共享。接口做不到这一点——接口只能定义常量不能定义实例变量。但我们没有让它做业务模板。DefaultBaseManager只管基础设施不管取号流程。取号流程的“骨架”被交给了另一个组件。3. 接口的正确用法隔离平台差异在我们的架构中平台差异被三个策略接口彻底隔离publicinterfaceRequestStrategy{ObjectbuildRequest(WaybillContextctx);}publicinterfaceParseStrategy{ListTocPickTicketWayBillDetailsNewparseResponse(WaybillContextctx,Stringresponse);}publicinterfaceExceptionStrategy{booleanisBusinessSuccess(Stringresponse);StringextractErrorMsg(Stringresponse);}设计说明这里的三个策略接口属于策略模式的范畴负责“可变步骤的具体实现”而下文将要介绍的WaybillFetchTemplate属于模板方法模式的范畴负责“固定流程的统一编排”。二者是互补关系而非替代关系。为什么这些是接口can-do 关系奇门“能构建请求”、抖音“能解析响应”这是一种能力契约不是“是一种”的关系。无状态策略实现类不持有实例变量所有参数通过WaybillContext传入。无状态的类用接口最自然。多实现同一平台的不同子渠道如抖音普通和抖音代发可以实现同一接口的不同版本。4. 模板方法教科书用继承我们选了组合4.1 教科书的标准写法严格来说GoF的模板方法模式是通过继承实现的——在父类中定义算法骨架子类重写特定步骤。教科书的代码长这样publicabstractclassAbstractWaybillFetcher{publicfinalvoidfetch(){validate();ObjectrequestbuildRequest();StringresponsecallApi(request);parseResponse(response);save();}privatevoidvalidate(){/* 通用校验 */}protectedabstractObjectbuildRequest();// 子类实现protectedabstractvoidparseResponse(Stringresponse);protectedabstractvoidsave();}publicclassQiMenWaybillFetcherextendsAbstractWaybillFetcher{...}publicclassDouYinWaybillFetcherextendsAbstractWaybillFetcher{...}但在实践中只要遵循“固定流程骨架差异化具体步骤”的思想无论采用继承还是组合都可以视为模板方法模式的落地。下面是我们基于组合的实现方式。4.2 我们的实际写法publicclassWaybillFetchTemplate{privatefinalRequestStrategyrequestStrategy;privatefinalParseStrategyparseStrategy;privatefinalExceptionStrategyexceptionStrategy;privatefinalApiInvokerapiInvoker;privatefinalWaybillPersistencepersistence;// 通过构造函数注入三个策略对象// 实际项目通过 Spring 自动装配调用方无需手动传参publicWaybillFetchTemplate(RequestStrategyreq,ParseStrategyparse,ExceptionStrategyex,ApiInvokerinvoker,WaybillPersistencepersist){this.requestStrategyreq;this.parseStrategyparse;this.exceptionStrategyex;this.apiInvokerinvoker;this.persistencepersist;}publicbooleanexecute(WaybillContextctx){// 生成全链路追踪IDStringtraceIdctx.getTicket().getCode()_System.currentTimeMillis();// 1. 构建请求策略ObjectrequestrequestStrategy.buildRequest(ctx);// 2. 调用APIStringresponseapiInvoker.invoke(ctx,request,traceId);// 3. 业务异常判断策略if(!exceptionStrategy.isBusinessSuccess(response)){markException(ctx.getTicket(),exceptionStrategy.extractErrorMsg(response));returnfalse;}// 4. 解析响应策略ListDetaildetailsparseStrategy.parseResponse(ctx,response);if(details.isEmpty()){returnfalse;}// 5. 持久化persistence.saveAndBind(ctx.getTicket(),details,ctx.isFirst());returntrue;}}它没有任何abstract方法也没有任何子类。平台差异全部通过构造函数注入的策略对象来解决。4.3 为什么这里组合更好理由一策略已经用接口独立了继承会造成冗余三个策略接口已经完美地隔离了平台差异。如果用继承每个平台既要实现策略接口又要继承模板抽象类等于把同样的差异逻辑在两个地方重复定义。理由二避免类爆炸如果用继承十几个平台就是十几个子类AbstractWaybillFetchTemplate ├── QiMenWaybillFetchTemplate ├── DouYinWaybillFetchTemplate ├── DouYinDaiFaWaybillFetchTemplate ├── JDWaybillFetchTemplate └── PDDWaybillFetchTemplate用组合只需一个WaybillFetchTemplate通过注入不同策略实例即可覆盖所有平台。理由三运行时灵活性组合允许在运行时动态替换策略。比如通过工厂根据平台编码返回不同的策略组合RequestStrategyreqstrategyFactory.getRequestStrategy(platformCode,original);ParseStrategyparsestrategyFactory.getParseStrategy(platformCode,original);ExceptionStrategyexstrategyFactory.getExceptionStrategy(platformCode,original);WaybillFetchTemplatetemplatenewWaybillFetchTemplate(req,parse,ex,invoker,persistence);关于策略工厂StrategyFactory内部维护MapString, RequestStrategy等映射通过复合KeyplatformCode _ platFormOriginal定位策略实例。新增平台时只需添加策略实现类并注册到工厂WaybillFetchTemplate本身无需修改。工厂的完整设计见系列中《策略工厂复合Key路由改造》一文。继承的类关系在编译期就固定了做不到这种动态组装。理由四更容易测试可以单独测试WaybillFetchTemplate注入 Mock 的策略对象无需启动 Spring 容器TestpublicvoidtestTemplate(){RequestStrategymockReqctx-mock;ParseStrategymockParse(ctx,resp)-Collections.singletonList(mockDetail);ExceptionStrategymockExresp-true;WaybillFetchTemplatetemplatenewWaybillFetchTemplate(mockReq,mockParse,mockEx,...);assertTrue(template.execute(ctx));}4.4 继承式模板方法的唯一适用场景在我们的架构中继承式模板方法的适用场景只有一个需要共享实例状态的基础设施复用。这正是DefaultBaseManager用抽象类的原因。5. 组合式模板的额外优势顺丰子母件组合方式还有一个意外收获在模板层增加流程分支非常自然。顺丰超过10件需要走子母件模式分批调用API我们直接在WaybillFetchTemplate中加了一个分支方法publicbooleanexecute(WaybillContextctx){if(isSFMoreTen(ctx)){returnexecuteSFMoreTen(ctx,traceId);// 子母件分支多次调用}returnexecuteNormal(ctx,traceId);// 普通分支单次调用}executeSFMoreTen内部会循环调用requestStrategy.buildRequest()和parseStrategy.parseResponse()策略实例不变只是调用次数不同。组合方式让分支逻辑只需在模板层控制无需修改任何策略实现。如果用继承子母件逻辑要么在每个子类中重复实现要么在父类中增加复杂的分支判断。组合方式让模板层成为唯一的流程控制点。可运行Demo组合式模板方法最小示例下面是一个极简的可运行示例帮助你直观理解“组合式模板方法”的核心思想——固定流程骨架通过注入不同策略改变行为。publicclassTemplateMethodDemo{// 策略接口可变步骤interfaceBrewStrategy{Stringbrew();// 冲泡}interfaceCondimentStrategy{Stringadd();// 加调料}// 模板类固定流程骨架组合方式非继承staticclassBeverageTemplate{privatefinalBrewStrategybrewStrategy;privatefinalCondimentStrategycondimentStrategy;publicBeverageTemplate(BrewStrategybrew,CondimentStrategycond){this.brewStrategybrew;this.condimentStrategycond;}// 模板方法固定流程publicfinalvoidmake(){System.out.println(1. 烧水);// 固定步骤System.out.println(2. brewStrategy.brew());// 可变步骤策略System.out.println(3. 倒入杯中);// 固定步骤System.out.println(4. condimentStrategy.add());// 可变步骤策略System.out.println(完成\n);}}// 具体策略实现staticclassTeaBrewimplementsBrewStrategy{publicStringbrew(){return用热水浸泡茶叶;}}staticclassTeaCondimentimplementsCondimentStrategy{publicStringadd(){return加柠檬;}}staticclassCoffeeBrewimplementsBrewStrategy{publicStringbrew(){return用热水冲泡咖啡粉;}}staticclassCoffeeCondimentimplementsCondimentStrategy{publicStringadd(){return加糖和牛奶;}}// 客户端publicstaticvoidmain(String[]args){// 制作茶注入茶相关的策略newBeverageTemplate(newTeaBrew(),newTeaCondiment()).make();// 制作咖啡注入咖啡相关的策略模板类不变newBeverageTemplate(newCoffeeBrew(),newCoffeeCondiment()).make();// 制作黑咖啡不加调料注入空实现策略newBeverageTemplate(newCoffeeBrew(),()-不加调料).make();}}运行输出1. 烧水 2. 用热水浸泡茶叶 3. 倒入杯中 4. 加柠檬 完成 1. 烧水 2. 用热水冲泡咖啡粉 3. 倒入杯中 4. 加糖和牛奶 完成 1. 烧水 2. 用热水冲泡咖啡粉 3. 倒入杯中 4. 不加调料 完成关键设计点固定骨架make()方法锁定了“烧水→冲泡→倒杯→加调料”的执行顺序。可变步骤BrewStrategy和CondimentStrategy是两个策略接口子类无需继承模板类只需实现策略接口。组合注入BeverageTemplate通过构造函数接收策略对象运行时灵活替换。零子类没有TeaMaker、CoffeeMaker等继承类所有差异由策略实例承载。这个Demo的核心思想和我们的WaybillFetchTemplate完全一致——固定流程骨架 策略接口注入 零子类。只不过饮品Demo只有两个策略接口电子面单系统有三个Request / Parse / Exception原理完全相同。6. 面试实战模板方法模式如何回答才能拿高分学完理论来看面试中怎么用。以下是面试官常问的模板方法相关问题及高分回答模板。6.1 面试官问模板方法模式在项目中用过吗怎么用的回答思路先一句话概括模式本质再讲项目场景最后说为什么用组合而不是继承。高分回答模板方法模式的核心是“固定流程骨架差异化具体步骤”。我们在多平台电子面单系统中大量使用了它。具体来说奇门、抖音、京东等十几个平台的取号流程完全一样——校验→构建请求→调用API→解析→持久化——但每个平台每一步的实现都不同。我们用WaybillFetchTemplate作为编排器把固定流程写死在execute()方法里把可变步骤抽象成RequestStrategy、ParseStrategy、ExceptionStrategy三个策略接口通过构造函数注入。这里有一个关键设计决策教科书上的模板方法通常用继承实现——父类定义骨架子类重写抽象方法。但我们选择了组合方式因为策略已经通过接口独立了组合更灵活、更容易测试、还能避免十几个平台产生十几个子类的“类爆炸”问题。新增一个平台只需新增三个策略实现类并注册到工厂核心流程代码零改动。这就是组合优于继承的典型实践。6.2 面试官追问组合式模板方法和策略模式有什么区别你这不是策略模式吗回答思路先承认观察敏锐再讲清二者分工最后用项目代码佐证。高分回答这个问题问得好很多人都会混淆。二者确实经常配合使用但分工不同策略模式负责“可变步骤的具体实现”——比如奇门怎么构建请求、抖音怎么解析响应。每个策略接口封装一种算法变体。模板方法模式负责“固定流程的统一编排”——WaybillFetchTemplate的execute()方法控制着“构建→调用→判断→解析→持久化”的执行顺序这个骨架对任何平台都一样。简单说策略管“怎么做”模板管“什么时候做”。它们不是互斥关系而是互补关系。在我们的项目中三层策略接口是策略模式WaybillFetchTemplate是组合式模板方法二者配合工作。6.3 面试官追问为什么不直接用继承Java不是单继承吗用接口组合不是更受限吗回答思路承认Java单继承的限制但重点讲组合的真实收益。高分回答Java单继承确实是客观限制但我们在决策时优先考虑的不是“能不能”而是“好不好”。如果用继承十几个平台需要十几个XXXWaybillFetcher子类每个子类既要继承模板类又要实现策略接口因为解析逻辑可能被其他组件复用这就造成了职责重复。而且分支流程如顺丰子母件需要在每个子类中重复编写或在父类中加复杂判断。用组合只需一个WaybillFetchTemplate通过注入不同策略实例覆盖所有平台。分支流程直接在模板层控制策略实例不变。另外组合可以 Mock 注入单元测试不需要启动Spring容器继承则必须创建子类实例测试成本高得多。所以不是“能不能”的问题而是组合在灵活性、可测试性、可维护性上都明显优于继承。6.4 面试场景模拟面试官连环追问面试官模板方法模式在项目中用过吗你用过我们在多平台电子面单系统中用组合式模板方法编排取号流程……面试官等等组合式模板方法GoF定义不是通过继承吗你GoF的经典定义确实是通过继承但模式的核心是思想而非具体实现——“固定流程骨架差异化具体步骤”。只要遵循这个思想用继承和组合都可以。我们选组合是因为平台差异已经通过策略接口独立组合比继承更灵活。面试官那这不就是策略模式吗你策略模式负责“可变步骤的具体实现”模板方法负责“固定流程的统一编排”二者是互补关系。打个比方策略模式告诉你“每个工人怎么做自己的工序”模板方法告诉你“整个流水线的工序顺序”。我们项目中三层策略接口是策略模式WaybillFetchTemplate是组合式模板方法二者配合工作。面试官如果只允许你用一个词总结你的设计你会选什么你组合。继承管基础设施组合管业务编排。这就是我们在日均30万订单系统中得出的核心结论。7. 总结在我们的多平台电子面单架构中抽象类DefaultBaseManager用于基础设施复用DAO、日志、事务因为它需要共享实例状态。三层策略接口用于隔离平台差异因为它们是 can-do 关系、无状态、需要多实现。模板方法WaybillFetchTemplate用组合实现而非继承。因为策略已经接口化组合更灵活、更简洁。最后用一句话总结本文的核心观点助你在面试中脱颖而出面试绝杀一句话模板方法模式的核心是“固定流程骨架差异化具体步骤”。这个骨架既可以用继承实现也可以用组合实现。如果差异已经通过接口独立组合优于继承——这就是我们在日均30万订单的电子面单系统中得出的结论。❓评论区挑战问题以下关于模板方法模式的说法哪一个是正确的A. 模板方法模式只能通过继承实现父类定义骨架子类重写抽象方法。B. 模板方法模式可以通过组合实现将可变步骤封装为策略接口通过注入不同策略实现流程定制。C. 模板方法模式与策略模式是互斥的不能在同一系统中共存。D. 模板方法模式违反了开闭原则因为新增变体需要修改父类。提示结合本文第4节的核心观点——“固定流程骨架差异化具体步骤”既可用继承也可用组合实现。答案B解析A错误组合式模板方法同样是模板方法思想的落地见4.1节B正确本文的WaybillFetchTemplate就是组合式模板方法的典型实现C错误策略模式负责可变步骤的具体实现模板方法负责流程编排二者是互补关系见第3节设计说明D错误无论是继承式还是组合式模板方法新增变体都无需修改骨架代码符合开闭原则。系列导航上一篇从多平台电子面单架构看接口与抽象类的真实选型下一篇预告《三层策略模式拆解彻底解耦多平台差异化逻辑》内容预告拆解 Request/Parse/Exception 三层策略设计思路讲解策略工厂复合键路由、多平台扩展落地规范。 关注本专栏每周更新用生产代码讲透面试考点。