模板方法模式精讲:从泡咖啡到订单重构,一次看懂骨架与钩子
发布时间:2026/10/16 9:24:57 锦皓数字建站

1. 从一壶手冲咖啡说开去模板方法模式到底解决了什么整理 Design Pattern 学习笔记的时候我给自己留了一个叫 Demllie Design Pattern 的系列专门记录那些看一遍觉得懂了动手写才发现没懂的模式。模板方法模式Template Method是我在这个系列里最早深入的一个起因很朴素我接手的某个项目里订单同步、报表生成、消息推送三个模块各写了一套流程流程的头尾几乎一样只有中间几步不同三份代码加起来一千多行重复逻辑。改一个参数要同步改三个地方漏改一次就出一回线上问题。那段时间我一直在想如果能把流程骨架固定下来只让每个场景填自己不一样的那几步是不是就能少写很多重复代码后来重新翻设计模式发现模板方法模式早把这件事讲透了父类定义算法的骨架和顺序子类只负责实现那些会变化的步骤。这种思路在生活里随处可见——同样是冲泡饮品这件事泡咖啡和泡茶的前半段几乎一模一样烧水、放原料、冲水、倒杯、加料区别只在原料和加什么料。Java 的类库、服务端框架、测试框架里都能看到它的影子。但很多人学这个模式有个误区以为它就是继承 抽象方法于是到处建抽象类结果把继承树搞得又深又脆。这篇笔记我会从问题场景、结构拆解、完整代码、边界对比、实战重构五个角度讲清楚它什么时候是良药什么时候会变成毒药。适合谁看如果你已经写过一段时间的面向对象代码能理解继承和多态但还没系统看过设计模式或者你看过概念但不知道代码该怎么组织、钩子方法到底有什么用——这篇内容就是为你准备的。我会把所有步骤拆到能直接照着写。1.1 泡咖啡和泡茶两段流程为什么长得这么像先看一个经典例子。我们用程序去描述泡咖啡和泡茶最容易想到的写法是写两个独立的方法泡咖啡烧水 - 用沸水冲泡咖啡粉 - 把咖啡倒进杯子 - 加糖加奶泡茶烧水 - 用沸水浸泡茶叶 - 把茶倒进杯子 - 加柠檬两个方法的第一步和第三步都是完全相同的逻辑。如果直接把代码复制两份第一版跑得通等产品经理说烧水之前要加一道水质检测你就得在两个方法里各改一遍。再往后加一个泡可可、泡奶茶每加一种饮品公共逻辑就多一份拷贝。这不是代码洁癖问题是维护成本问题。重复出现三次以上的流程性代码意味着你缺了一层抽象。模板方法模式的思路就是把烧水 - 冲泡 - 倒杯 - 加料这个流程顺序固定下来作为骨架把冲泡什么原料和加什么料这两步开放给子类。1.2 所谓模板是先把步骤定死再把细节放开模板方法模式的定义很简短在一个方法中定义算法的骨架把某些步骤延迟到子类实现。关键是分清哪些东西该被固定哪些东西该被放开。固定的东西是步骤顺序和公共行为。比如所有饮品都要烧水、都要倒进杯子这些行为父类直接写好子类不许改。放开的东西是可变步骤。咖啡冲咖啡粉茶泡茶叶加料也不同这些就定义成抽象方法让子类各自填。这种设计的最大好处是复用骨架。父类的方法调用子类的方法看起来很平常但它翻转了控制方向——不是子类调父类而是父类在某个时机主动调用子类的实现。这个思想后来衍生出一整类框架设计几乎所有依赖倒置、控制反转的思路都能在这里找到影子。2. 骨架、钩子和好莱坞规则模板方法的结构拆解前面说的是直觉这一节我们把它落到代码结构上。模板方法模式有三个核心成员抽象基类AbstractClass、具体子类ConcreteClass、以及基类里的模板方法templateMethod。理解这三者的分工比记住类图重要得多。2.1 三种方法的分工终态方法、抽象方法、钩子方法先看抽象基类里会出现哪些方法。它们按开放程度分成三类这也是整个模式最容易混淆的地方方法类型在基类中的状态子类能不能改典型用途具体方法终态方法已经有完整实现通常用final修饰不能改公共步骤、模板方法本身抽象方法只声明不实现必须实现每个子类差异最大的步骤钩子方法hook有默认实现通常是空方法或返回默认值可选覆盖在特定位置给子类插一脚模板方法本身往往就是一个final方法。它内部按固定顺序调用其他方法包括抽象方法和钩子方法。因为模板方法是整个流程的总调度如果允许子类重写它骨架就散了所以用final锁死。这里重点说一下钩子方法。它是我觉得模板方法模式里最妙、也最容易被忽略的设计。钩子可以是一个空方法也可以是一个返回布尔值的判断方法。基类在流程的某个位置调用它默认什么都不做子类如果关心这个位置就覆盖它。举个例子某订单流程里有一段是否需要额外审计的判断。普通订单不需要大额会员订单需要。这个判断就可以设计成钩子方法// 基类里默认不审计 protected boolean needExtraAudit(Order order) { return false; } // 子类按业务规则覆盖 Override protected boolean needExtraAudit(Order order) { return order.getAmount() 5000; }钩子的价值在于它让子类可以精准地在流程的某个缝隙里插入逻辑又不需要改动流程顺序。没有钩子你要么把所有判断堆在基类里写成 if-else要么子类重写整个模板方法破坏骨架的复用。2.2 好莱坞原则别调用我们我们会调用你模板方法模式背后站着一条著名的设计原则好莱坞原则Hollywood Principle——别调用我们我们会调用你。翻译成代码语言就是子类不要主动去调用父类的流程父类的模板方法会在合适的时机调用子类的方法。很多初学者写继承代码时习惯在子类里super.process()然后前后插入逻辑这是反好莱坞原则的。模板方法模式下子类只需要被动地提供零件由父类来决定零件的组装顺序和调用时机。这样做的好处是你很难在子类中把流程顺序改乱——顺序被锁在父类的final方法里。这一点也是模板方法模式和普通继承的重要区别。普通继承是为了复用字段和公共方法子类可以随意调用父类方法模板方法模式则是把流程控制权上交给父类子类只剩下填空权。控制权的转移才是这个模式真正的威力所在。2.3 类结构怎么写一个抽象类 一组具体子类类结构其实很简单。抽象基类里包含一个final的模板方法规定全部执行步骤一个或多个抽象方法必须由子类实现一个或多个钩子方法子类按需覆盖若干个具体方法存放所有子类共用的逻辑具体子类只做一件事实现或覆盖那些抽象方法和钩子方法不触碰模板方法。当你发现自己需要在一个子类里重写模板方法本身时就要警觉——要么你的骨架设计得不对要么这个场景根本不适合用模板方法模式。这一节先讲结构下一节我用一个电商订单处理的完整案例把这几类方法全部串起来。看着代码理解结构比对着类图硬背快得多。3. 用一份订单处理器验证理论完整代码带你走一遍理论说多了容易飘直接看代码。我选一个电商场景订单从下单到处理完成需要经过校验、算折扣、扣库存、发通知四个固定步骤某些订单还需要额外的人工审计。这是一个非常典型的流程固定、步骤多变的场景。先设计骨架。订单处理的抽象基类如下public abstract class OrderProcessor { // 模板方法定义订单处理的完整骨架 public final void processOrder(Order order) { validate(order); Discount discount computeDiscount(order); reserveStock(order); notifyCustomer(order, discount); if (needExtraAudit(order)) { audit(order); } } // 抽象方法每种订单类型都有自己的校验规则 protected abstract void validate(Order order); // 抽象方法折扣算法差异很大交给子类 protected abstract Discount computeDiscount(Order order); // 抽象方法普通订单和会员订单扣库存逻辑不同 protected abstract void reserveStock(Order order); // 具体方法发通知的逻辑几乎相同直接实现在父类 protected void notifyCustomer(Order order, Discount discount) { // 组装通知文案、调用消息通道所有订单共用 System.out.println(订单 order.getOrderNo() 已处理折扣金额 discount.getAmount()); } // 钩子方法默认不需要额外审计 protected boolean needExtraAudit(Order order) { return false; } // 钩子配套方法真正执行审计 protected void audit(Order order) { // 默认空实现子类按需覆盖 } }这个基类里processOrder就是传说中的模板方法。它是final子类动不了。它固定了七个执行步骤先校验再算折扣再扣库存再通知最后看情况审计。注意第6、7行——if (needExtraAudit(order))这个判断就是钩子的典型用法。基类里默认不需要审计子类一旦覆盖这个方法流程就会自动多出审计这一步。3.1 两个子类普通订单和会员订单再来写两个具体子类。第一个是普通订单处理器public class NormalOrderProcessor extends OrderProcessor { Override protected void validate(Order order) { if (order.getItems() null || order.getItems().isEmpty()) { throw new IllegalArgumentException(订单没有商品明细); } } Override protected Discount computeDiscount(Order order) { return Discount.none(); } Override protected void reserveStock(Order order) { // 普通订单按商品明细逐项扣减库存 for (OrderItem item : order.getItems()) { StockService.deduct(item.getSkuId(), item.getQuantity()); } } }第二个是会员订单处理器它复用了基类的大部分流程只改三处校验规则增加会员等级判断折扣变成会员折扣送积分。另外它覆写了钩子方法让超过五千元的大额订单进入人工审计public class MemberOrderProcessor extends OrderProcessor { Override protected void validate(Order order) { if (order.getMemberLevel() 2) { throw new IllegalArgumentException(会员等级不足); } } Override protected Discount computeDiscount(Order order) { return Discount.percent(0.9); // 会员统一九折 } Override protected void reserveStock(Order order) { // 会员订单扣库存后还额外加积分 for (OrderItem item : order.getItems()) { StockService.deduct(item.getSkuId(), item.getQuantity()); } MemberService.addPoints(order.getMemberId(), 100); } // 覆盖钩子大额订单触发审计 Override protected boolean needExtraAudit(Order order) { return order.getAmount() 5000; } Override protected void audit(Order order) { AuditService.submit(order.getOrderNo(), 大额会员订单人工复核); } }看明白了吗两个子类没有一个重写processOrder但最终执行的步骤序列完全不同普通订单只有四步会员订单在特定条件下会多出审计这一步。这就是模板方法的价值——子类通过实现抽象方法影响每步怎么做通过覆盖钩子方法影响流程要不要多走一步。3.2 运行效果与三个容易忽略的细节写个调用方看一眼效果OrderProcessor processor new MemberOrderProcessor(); processor.processOrder(memberOrder);控制台会按顺序输出校验通过、折扣计算、库存扣减、通知发送、审计提交这几个环节的操作。整个流程的顺序是基类定死的子类想插队是不可能的。跑通这段代码后有三个细节值得单独拿出来强调。第一模板方法应该用final修饰。很多教材没强调这点实战中尤其重要。如果子类能随意覆盖模板方法骨架就不复存在模式直接失效。一些团队规范里甚至会用代码检查工具强制模板方法是终态方法。第二抽象方法不要有默认实现。有时开发者图省事给抽象方法写了个空实现然后子类想实现就实现不想实现就空着。这会让哪些步骤必须提供变得模糊子类写起来没谱。抽象方法就该强制子类必须实现把差异显式地暴露出来。第三钩子方法的打开姿势要克制。钩子越多子类需要了解的流程细节就越多。我见过一个模板类里塞了七个钩子方法子类开发人员打开基类的一瞬间就懵了。一般两到三个钩子就够用钩子太多说明骨架的抽象粒度没找好该拆出多个模板方法了。4. 模板方法容易踩的三道边界和策略模式、工厂方法、回调的关系设计模式学到中后期真正困扰人的不是单个模式怎么用而是多个模式长得太像不知道该选哪个。模板方法尤其容易和策略模式、工厂方法、回调机制混淆。这几道边界理不清代码里就会出现用模板方法的壳装策略模式的心之类的缝合怪。4.1 模板方法 vs 策略模式一个用继承一个用组合策略模式的意图是把一族算法封装起来让它们可以互相替换——执行算法的对象持有一个策略接口运行时可切换。模板方法模式则把算法骨架放在父类里通过继承让子类扩展步骤。选哪个有一个很实用的判断标准如果子类之间共享的是步骤顺序和部分步骤实现且变体数量在可枚举范围内优先模板方法。如果算法本身是一个完整的、可能运行时切换的选择项用策略模式。举个例子折扣计算如果只是九折、八折、满减这些固定档位定义一个策略接口或枚举策略即可但如果整个订单处理流程有固定骨架、只有校验和扣库存两步不同用模板方法更自然。还有一层继承会带来强耦合子类一旦创建它的父类骨架就很难大幅改动。所以模板方法适合流程相对稳定的领域。如果你预判业务流程会频繁调整步骤顺序那不如把所有步骤拆成策略接口用组合的方式编排。4.2 模板方法 vs 工厂方法一个管流程一个管创建工厂方法模式里子类决定创建什么对象模板方法模式里子类决定步骤怎么做。两者的共同点是都依赖父类定义骨架、子类填补细节但关注点完全不同。有时候它们会合作。比如模板方法里需要创建一个资源对象这个对象的创建逻辑因订单类型而异于是模板方法内部调用一个createResource()抽象方法由子类实现——这一步本身就成了工厂方法。实践中经常出现模板方法内部嵌套工厂方法的组合用法这不冲突但要注意一个模板方法里的抽象步骤不要超过三四个否则子类实现的负担会过重。如果出现这种情况可以考虑把其中一组相关步骤再抽成一个独立的策略接口而不是全部压在一个继承体系里。4.3 模板方法 vs 回调机制同样都是好莱坞长相不同回调机制Callback比如接口回调、函数式编程里的Consumer、Runnable思想内核和好莱坞原则一模一样框架在合适的时机调用你传入的函数。很多框架文档里说的扩展点钩子本质上就是回调。区别在于表达方式。模板方法是把扩展点内嵌在继承体系里子类通过覆盖方法来实现扩展回调是把扩展逻辑作为参数传入类之间是组合关系更灵活但可读性稍差——你要在调用现场看传进去的函数会在哪个时机被调用常常得翻框架源码。我的使用习惯是自己维护的、规模可控的业务流程用模板方法语义清晰外部框架提供的扩展点用回调因为它不需要你继承框架类侵入性低。模板方法也不是越纯越好实战中两者常常混用没必要为了模式纯洁性拒绝另一种写法。4.4 什么情况下别急着用模板方法模板方法不是万能药。以下三种信号出现时我会提醒自己冷静一下。第一继承链即将超过两层。三层以上的继承会让任何一个改动都波及一大片子类排查起来极其痛苦。第二子类需要覆盖的方法过多覆盖率超过一半的抽象方法大概率是骨架设计不合理——你只是借了个壳没借到真正公用的东西。第三流程顺序本身不稳定。产品需求三天两头调整步骤顺序模板方法里每次调顺序都要动基类远不如把步骤拆成独立组件、用列表编排来得灵活。这三个信号背后其实是同一个判断模板方法的收益来自固定性——固定的部分越值钱模式越有用可变化的部分越多、变化越剧烈模式的收益就越低。判断一个场景适不适合用就先数数有哪些是稳定的、有哪些是易变的。5. 实战重构建议怎样识别该用模板方法的重复代码最后聊点真正能落地的经验。前面说了一堆理论但回到日常开发最现实的问题永远是我手头这些代码到底哪些该重构成模板方法我有一套自己的识别信号和操作步骤这套方法帮我在两个项目里做了成功的重构其中一个重构后删掉了四百多行重复代码。5.1 三个识别信号步骤顺序相同、中间步骤不同、整体流程固定我判断一个场景是否适合模板方法只看三个信号两个以上流程的步骤顺序完全相同只是每步的内部实现不同流程里有一部分步骤是多个流程共用的复制粘贴过至少一次流程的先后顺序在未来较长时间内不会大改三选二基本就能上模板方法了。举个真实的例子某系统里有月度报表、日度报表、自定义报表三种生成任务。每个任务的执行过程都是取数 - 清洗 - 聚合 - 导出 - 归档但取数的数据源不同、清洗规则不同、聚合维度不同导出和归档的逻辑几乎完全一样。这三个报表模块每个都各自写了一套完整流程导出和归档代码复制了三份。你看第一和第二两个信号都命中了。重构方案就是抽出ReportGenerator抽象基类模板方法定义五个步骤导出和归档作为具体方法放进基类取数、清洗、聚合变成三个抽象方法三个报表生成器各写各的实现。改完之后新增一种报表只需要继承基类实现三个方法不用再关心导出和归档怎么处理。5.2 重构中的命名与注释经验让钩子方法看得懂模板方法模式写起来简单但维护起来有个隐性问题子类的开发者能看到的只是一个个方法他们怎么知道每个方法会在流程的哪个时机被调用这个问题靠代码本身很难表达清楚所以要靠命名和注释。我在基类里有一条硬性规范模板方法上必须写清楚步骤顺序和每个步骤的意图每个抽象方法、钩子方法都要在注释里标注在模板方法的第几步被调用默认行为是什么子类覆盖后会影响什么。比如钩子方法的标准注释是// 在 processOrder 第四步 notifyCustomer 之前调用 // 默认不审计如需人工复核请重写并返回 true同时实现 audit protected boolean needExtraAudit(Order order) { return false; }命名也有套路。钩子方法一般用need、should、skip这类词开头让别人一眼看出它是个布尔钩子配套的动作方法用动词开头比如audit、doClean。这样的命名能让子类开发者不用打开整个基类源码就知道自己该覆写哪些方法。5.3 测试策略基类测流程子类测差异模板方法模式的测试策略和它整体设计一样分层。我在项目里约定两套测试一套测基类的流程编排另一套测子类的差异实现。基类测试用真实子类或测试替身验证模板方法内部方法的调用顺序、次数和参数。比如用 Mock 的订单对象跑一遍processOrder断言validate在reserveStock之前被调用notifyCustomer恰好被调用一次钩子返回默认值时audit没有被调用。这能防止有人不小心改了模板方法的流程顺序而不自知。子类测试则聚焦每个子类对抽象方法、钩子方法的实现是否符合预期。比如会员订单处理器在大额订单时返回needExtraAudit() true普通订单处理器永远返回false。这样分层下来即使新增一个订单类型也只需要补新子类的差异测试基类测试完全不用动。5.4 最后说句心里话设计模式不是集邮学模板方法模式最忌讳的是为了用而用。我有段时间看到重复代码就想着建抽象类结果把一个只出现过两个变体的简单逻辑拆成了四层继承树后一个接手的人花了整整两天才理清。后来我给自己定了一条规矩至少出现两个真实重复的流程并且能明确说出固定步骤是哪些才引入模板方法。如果只是预判未来会重复先忍住等第二次出现再抽。这个模式真正的力量不是让你少写几个方法而是强迫你思考这段流程里什么才是雷打不动的骨架什么才是允许变化的血肉想清楚了代码会干净很多。如果你读完这篇正准备拿手头的代码试试手我的建议是从一个你已经复制粘贴过两次的流程开始先把公共步骤列出来再标出每步的差异最后决定要不要用模板方法收敛。整个过程不需要一次性做到完美能让自己少改一处重复代码就已经值了。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。