Java 23 种设计模式:从踩坑到精通 | 模版方法模式 —— 定义算法骨架,交给子类填充细节

发布时间:2026/7/3 18:38:55
Java 23 种设计模式:从踩坑到精通 | 模版方法模式 —— 定义算法骨架,交给子类填充细节 Java 23 种设计模式从踩坑到精通 | 模版方法模式 —— 定义算法骨架交给子类填充细节摘要当多个类有相似的流程只有部分步骤不同时复制粘贴代码会导致大量冗余且修改流程时必须逐个更新所有类。模板方法模式通过在父类中定义一个算法的骨架将可变部分延迟到子类中实现从而在保证流程一致性的同时允许子类灵活定制特定步骤。本文从饮品制作流程出发完整讲解模板方法模式的原理、UML、钩子方法等实现技巧与策略模式进行对比并结合电子面单编排器实战、JDKAbstractList、ServletHttpServlet、SpringJdbcTemplate等框架应用帮你掌握“固定骨架、填充细节”的设计精髓。️本文阅读地图3 分钟速览为什么泡茶和冲咖啡的代码几乎一样却不能复用模板方法 vs 抽象方法 vs 钩子方法手写饮品制作模板茶、咖啡、钩子控制加调料数据同步框架固定流程 可变步骤实战案例电子面单编排器——模板方法用继承还是组合JDKAbstractList/ ServletHttpServlet/ SpringJdbcTemplate如何体现面试必问模板方法 vs 策略到底怎么选《Java 23 种设计模式从踩坑到精通》开篇系列介绍与目录 上一篇策略模式 当前模板方法模式 下一篇访问者模式 返回系列总目录文章目录Java 23 种设计模式从踩坑到精通 | 模版方法模式 —— 定义算法骨架交给子类填充细节1. 从“泡茶和冲咖啡”的相似流程说起1.1 你的场景该不该用模板方法2. 模式定义与 UML 结构图文解析配合上述 UML 图3. 代码实现饮品制作模板3.1 抽象父类3.2 具体子类茶3.3 具体子类咖啡3.4 客户端4. 代码实现数据同步框架4.1 抽象父类4.2 具体子类4.3 客户端5. 实战案例电子面单编排器——模板方法用继承还是组合5.1 我们的选择用组合实现模板方法5.2 为什么用组合而不是继承5.3 模板层分支顺丰子母件6. 模板方法模式 vs 策略模式7. 优缺点一览8. 框架与实践中的应用8.1 ServletHttpServlet8.2 SpringJdbcTemplate、RestTemplate、TransactionTemplate8.3 JDKAbstractList / AbstractSet9. 面试必问 面试官追问连环炮10. 六大设计原则在模板方法模式中的体现 《Java 23 种设计模式从踩坑到精通》快速导航实战配套电商多平台电子面单对接实战1. 从“泡茶和冲咖啡”的相似流程说起泡茶和冲咖啡的步骤非常相似泡茶烧水 → 用热水泡茶叶 → 倒入茶杯 → 加柠檬冲咖啡烧水 → 用热水冲咖啡粉 → 倒入咖啡杯 → 加糖和牛奶如果分别实现这两个类大量代码重复烧水、倒杯这些步骤完全一样只有“泡什么”和“加什么调料”不同。更糟的是如果某天流程需要调整比如必须用 80℃ 水就必须修改所有相关类。如果以后还要增加“泡枸杞”或“冲可可”复制粘贴会让代码迅速膨胀。模板方法模式Template Method Pattern正是为此而生它在父类中定义算法的骨架将某些步骤延迟到子类中实现。子类可以在不改变算法整体结构的情况下重新定义算法中的某些步骤。1.1 你的场景该不该用模板方法判断标准是 → 用模板方法否 → 用其他方式多个类有完全相同的流程仅部分步骤不同✅❌需要控制流程的执行顺序防止子类篡改✅❌需要在父类中提供“钩子”让子类可选地干预流程✅❌需要动态替换整个算法而非仅修改部分步骤❌考虑策略模式2. 模式定义与 UML 结构模板方法模式定义一个操作中的算法的骨架而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。它属于行为型设计模式。图文解析配合上述 UML 图模板方法模式的核心角色抽象父类AbstractClass定义模板方法和抽象/钩子方法。模板方法定义算法骨架并调用抽象方法和钩子方法。模板方法通常被声明为final防止子类篡改流程。具体子类ConcreteClass实现抽象类中定义的抽象方法并根据需要重写钩子方法。核心机制好莱坞原则——“Don’t call us, we’ll call you.” 父类控制整体流程在需要的时候回调子类的方法。子类永远不需要主动调用父类的模板方法父类会在合适时机调用子类的实现。3. 代码实现饮品制作模板3.1 抽象父类publicabstractclassBeverageMaker{// 模板方法声明为 final 防止子类改变流程publicfinalvoidmakeBeverage(){boilWater();// 通用步骤brew();// 可变步骤子类实现pourInCup();// 通用步骤if(wantCondiments()){// 钩子方法决定是否加调料addCondiments();// 可变步骤子类实现}}// 通用步骤父类实现子类不可重写privatevoidboilWater(){System.out.println(把水烧到 100°C);}privatevoidpourInCup(){System.out.println(倒入杯中);}// 抽象方法子类必须实现protectedabstractvoidbrew();protectedabstractvoidaddCondiments();// 钩子方法子类可以选择重写默认返回 trueprotectedbooleanwantCondiments(){returntrue;}}白话模板方法makeBeverage()就是“泡饮品的标准流程”final保证没人能改流程顺序。brew()和addCondiments()是子类必须实现的“填空”。wantCondiments()是钩子可以控制是否加调料。3.2 具体子类茶publicclassTeaMakerextendsBeverageMaker{Overrideprotectedvoidbrew(){System.out.println(用热水浸泡茶叶);}OverrideprotectedvoidaddCondiments(){System.out.println(加柠檬);}}3.3 具体子类咖啡publicclassCoffeeMakerextendsBeverageMaker{Overrideprotectedvoidbrew(){System.out.println(用热水冲泡咖啡粉);}OverrideprotectedvoidaddCondiments(){System.out.println(加糖和牛奶);}// 重写钩子有些人喝黑咖啡不加东西OverrideprotectedbooleanwantCondiments(){returnfalse;// 演示用默认不加}}白话咖啡通过重写wantCondiments()告诉父类“我不要加调料”父类就会跳过加调料的步骤。3.4 客户端System.out.println( 制作茶 );BeverageMakerteanewTeaMaker();tea.makeBeverage();System.out.println(\n 制作咖啡 );BeverageMakercoffeenewCoffeeMaker();coffee.makeBeverage();输出 制作茶 把水烧到 100°C 用热水浸泡茶叶 倒入杯中 加柠檬 制作咖啡 把水烧到 100°C 用热水冲泡咖啡粉 倒入杯中咖啡跳过了加调料步骤因为wantCondiments()返回了false。4. 代码实现数据同步框架4.1 抽象父类publicabstractclassDataSyncTemplate{publicfinalvoidsync(){connect();extractData();transformData();loadData();disconnect();logSync();}privatevoidconnect(){System.out.println(【通用】建立数据库连接);}protectedabstractvoidextractData();// 钩子方法默认不转换protectedvoidtransformData(){System.out.println(【默认】无需数据转换);}protectedabstractvoidloadData();privatevoiddisconnect(){System.out.println(【通用】关闭数据库连接);}privatevoidlogSync(){System.out.println(【通用】记录同步日志);}}白话数据同步的标准流程连上 → 取数据 → 转格式 → 存数据 → 断开 → 记日志。extractData()和loadData()交给子类实现transformData()是可选的转换步骤。4.2 具体子类publicclassMySQLToRedisSyncextendsDataSyncTemplate{OverrideprotectedvoidextractData(){System.out.println(从 MySQL 查询待同步数据);}OverrideprotectedvoidtransformData(){System.out.println(将 MySQL 行数据转为 Redis Hash 结构);}OverrideprotectedvoidloadData(){System.out.println(批量写入 Redis);}}4.3 客户端newMySQLToRedisSync().sync();流程固定细节可变。新增一个PostgreSQLToElasticsearchSync只需继承父类实现两个抽象方法即可。5. 实战案例电子面单编排器——模板方法用继承还是组合理论学完了来看看模板方法模式在真实项目中是怎么用的。在多平台电子面单系统中取号流程是一个标准的“骨架可变步骤”场景校验订单 → 构建请求 → 调用API → 判断成功 → 解析响应 → 持久化。这个骨架对所有平台都一样但每一步的具体实现因平台而异。5.1 我们的选择用组合实现模板方法传统模板方法模式用继承实现——父类定义骨架子类覆写抽象方法。但在我们的电子面单架构中选择了组合方式。核心编排器WaybillFetchTemplate是一个普通的类通过构造函数注入三个策略对象publicclassWaybillFetchTemplate{privatefinalRequestStrategyrequestStrategy;privatefinalParseStrategyparseStrategy;privatefinalExceptionStrategyexceptionStrategy;privatefinalApiInvokerapiInvoker;privatefinalWaybillPersistencepersistence;// 通过构造函数注入策略publicWaybillFetchTemplate(RequestStrategyreq,ParseStrategyparse,ExceptionStrategyex,ApiInvokerinvoker,WaybillPersistencepersist){this.requestStrategyreq;this.parseStrategyparse;this.exceptionStrategyex;this.apiInvokerinvoker;this.persistencepersist;}// 模板方法固定取号流程骨架publicbooleanexecute(WaybillContextctx){// 1. 构建请求策略ObjectrequestrequestStrategy.buildRequest(ctx);// 2. 调用API统一StringresponseapiInvoker.invoke(ctx,request,traceId);// 3. 业务异常判断策略if(!exceptionStrategy.isBusinessSuccess(response)){markException(ctx.getTicket(),exceptionStrategy.extractErrorMsg(response));returnfalse;}// 4. 解析响应策略ListDetaildetailsparseStrategy.parseResponse(ctx,response);if(details.isEmpty()){markException(ctx.getTicket(),未获取到运单号);returnfalse;}// 5. 持久化统一persistence.saveAndBind(ctx.getTicket(),details,ctx.isFirst());returntrue;}}使用方式——运行时注入不同平台的策略// 奇门取号RequestStrategyreqnewQiMenRequestStrategy();ParseStrategyparsenewQiMenParseStrategy();ExceptionStrategyexnewQiMenExceptionStrategy();WaybillFetchTemplatetemplatenewWaybillFetchTemplate(req,parse,ex,invoker,persistence);template.execute(ctx);// 抖音取号reqnewDouYinRequestStrategy();parsenewDouYinParseStrategy();exnewDouYinExceptionStrategy();templatenewWaybillFetchTemplate(req,parse,ex,invoker,persistence);template.execute(ctx);5.2 为什么用组合而不是继承维度继承方式组合方式类数量N 个平台 → N 个子类1 个模板类 3 个策略接口灵活性编译期绑定运行时动态替换策略测试性需创建子类实例直接 Mock 策略注入复用性子类间无法复用策略策略可跨平台组合关键设计策略已经通过接口独立组合方式避免了类爆炸同时保留了运行时替换策略的灵活性。这就是“组合优于继承”的最佳实践。5.3 模板层分支顺丰子母件组合方式还有一个额外优势——在模板层增加流程分支非常自然。顺丰超过 10 件需要走子母件模式分批调用 API我们直接在模板类中增加一个分支方法publicbooleanexecute(WaybillContextctx){if(isSFMoreTen(ctx)){returnexecuteSFMoreTen(ctx);// 子母件分支多次调用}returnexecuteNormal(ctx);// 普通分支单次调用}如果用继承子母件逻辑要么重复在每个子类中实现要么在父类中增加复杂的分支判断很容易失控。组合方式让模板层成为唯一的流程控制点。 这套架构的完整设计、以及“模板方法用继承还是组合”的深度对比详见电子面单实战系列的《多平台统一架构设计》和《模板方法的组合与继承抉择》。6. 模板方法模式 vs 策略模式对比维度模板方法模式策略模式定义方式继承子类重写父类方法组合接口实现控制权父类控制整体流程子类只填充细节客户端选择策略策略控制算法粒度控制算法的骨架封装整个算法复用方式复用父类的通用代码复用策略接口和具体策略类典型应用HttpServlet、JdbcTemplate、AbstractListComparator、支付方式、折扣策略简单记忆模板方法是“继承复用”固定骨架填空策略模式是“组合复用”替换整个算法。模板方法强调“流程一致性”策略模式强调“算法可替换”。7. 优缺点一览优点缺点代码复用公共逻辑集中到抽象类避免代码重复继承限制子类必须继承抽象类Java 单继承限制灵活性流程控制父类控制整体流程保证一致性钩子过多导致复杂度钩子方法太多会让父类难以理解易扩展新增子类只需实现可变部分符合开闭原则子类受限子类必须遵循父类定义的算法骨架无法彻底改变流程符合开闭原则骨架固定可变部分灵活扩展调试困难调用栈在父类与子类之间跳转排查问题需要跨类追踪8. 框架与实践中的应用8.1 ServletHttpServlet这是模板方法模式最经典的例子。HttpServlet的service()方法是模板方法它根据 HTTP 方法类型调用doGet()、doPost()等钩子方法。开发者只需继承HttpServlet并重写这些方法无需关心请求解析和响应分发。publicclassMyServletextendsHttpServlet{OverrideprotectedvoiddoGet(HttpServletRequestreq,HttpServletResponseresp){// 处理 GET 请求}OverrideprotectedvoiddoPost(HttpServletRequestreq,HttpServletResponseresp){// 处理 POST 请求}}8.2 SpringJdbcTemplate、RestTemplate、TransactionTemplate这些XXXTemplate类都遵循模板方法模式。以JdbcTemplate为例它定义了数据库操作的流程获取连接 → 准备语句 → 执行查询 → 处理结果 → 释放资源。开发者只需通过回调接口实现结果映射等可变部分。8.3 JDKAbstractList/AbstractSetJava 集合框架中AbstractList提供了get(int index)、size()等抽象方法子类只需实现这些方法indexOf()、contains()等通用方法自动可用。这是模板方法模式的典型应用。9. 面试必问 面试官追问连环炮基础必问模板方法模式的核心角色→ 抽象父类定义模板方法、具体子类实现可变步骤。模板方法为什么通常声明为final→ 防止子类篡改算法骨架保证流程一致性。HttpServlet.service()是什么模式→ 模板方法模式。面试官追问“模板方法模式和策略模式如何选择” 流程固定、只需修改部分步骤 → 模板方法需要整体替换算法、动态切换 → 策略模式。“钩子方法越多越好吗” 不是。钩子过多会让父类的意图模糊子类难以理解哪些是必须实现、哪些是可选的。适量即可。“模板方法模式如何避免子类忘写关键步骤” 关键步骤定义为abstract编译器会强制子类实现可选步骤使用钩子方法并提供默认值。“你在项目中用过模板方法模式吗能举个具体的例子吗” 可以讲电子面单的编排器设计——取号流程骨架固定策略接口填充平台差异并用组合代替继承避免了类爆炸。完整的架构设计见电子面单实战系列《多平台统一架构设计》和《模板方法的组合与继承抉择》。恭喜如果你能立刻说出HttpServlet是模板方法模式JdbcTemplate也遵循模板方法思想并清楚模板方法 vs 策略的区别还能用电子面单案例回答“项目里怎么用的”你已经掌握了 Java 框架中最核心的“继承复用”设计思想。10. 六大设计原则在模板方法模式中的体现设计原则在模板方法模式中的体现单一职责原则SRP抽象父类管理算法骨架子类负责具体步骤开闭原则OCP新增不同实现只需新增子类无需修改抽象父类里氏替换原则LSP所有子类都可替换抽象父类依赖倒置原则DIP父类依赖抽象方法子类实现具体逻辑接口隔离原则ISP抽象父类只定义必要的抽象方法和钩子不臃肿迪米特法则LoD客户端只与抽象父类交互不需要了解子类细节 《Java 23 种设计模式从踩坑到精通》快速导航开篇系列介绍与目录上一篇策略模式 —— 算法族的封装与切换告别 if-else当前模版方法模式 —— 定义算法骨架交给子类填充细节你在这里下一篇访问者模式 —— 数据结构稳定但操作多变试试访问者 即将发布创建型模式汇总单例、工厂、建造者、原型结构型模式汇总适配器、装饰器、代理……行为型模式汇总观察者、策略、模板方法……实战配套电商多平台电子面单对接实战本文第5节展示的电子面单编排器设计在我们的电子面单实战系列中有完整的落地代码和设计演进过程。如果你对以下问题感兴趣推荐延伸阅读模板方法用继承还是组合编排器为什么选择了组合方式策略模式 模板方法模式如何配合实现流程与策略的完全解耦顺丰子母件如何在模板层优雅地增加流程分支《电商多平台电子面单对接实战》系列开篇从“能跑就行”到“整洁架构”多平台统一架构设计 —— 编排器策略模式的完整落地模板方法的组合与继承抉择 —— 为什么编排器没用抽象类学习建议设计模式系列讲“为什么这么用”电子面单系列讲“怎么用”。两者搭配理论实战闭环。 关注《Java 23 种设计模式从踩坑到精通》用 25 篇文章彻底吃透设计模式。福利预告全系列代码及 UML 源码将在完结时统一打包开放点击「关注」「收藏」第一时间获取。下一篇访问者模式数据结构稳定但操作多变试试访问者 即将发布敬请关注 除了设计模式我也在深挖智能物流实战WMS、托盘调度、机器学习落地。欢迎点击头像看看专栏 《出版社物流WMS智能调度实战》。技术相通思路可鉴。