ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

模板方法模式:从定义到框架源码,面试这样答稳拿高分

模板方法模式:从定义到框架源码,面试这样答稳拿高分 1. 面试开场模板方法模式到底是干什么的1.1 一个从业务流程里长出来的设计模式前几天我有个学弟跑来诉苦说面试官让他讲讲模板方法模式他张口就是“定义一个操作中的算法的骨架而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤”——这是《设计模式》书里的原话背得很溜。结果面试官一句话就把他问懵了“那你项目里哪里用过你平时写代码有没有用过这个思想”这其实是很多Java程序员面试设计模式时的通病定义背得滚瓜烂熟一让举场景就哑火。今天我把模板方法模式这个面试高频考点掰开揉碎讲透不光讲定义更讲清楚它解决的是什么问题、在JDK和Spring这些框架里是怎么落地的、面试官追问的坑都在哪里。不管你是刚开始准备Java面试还是项目里想重构重复代码这篇都值得花十分钟看完。1.2 模板方法模式的官方定义与核心思想先看官方定义。模板方法模式Template Method Pattern属于行为型设计模式它的核心是在一个方法中定义算法的骨架并将某些步骤推迟到子类中实现。模板方法使得子类可以在不改变算法结构的情况下重新定义算法中的某些特定步骤。听上去有点绕我打个比方。你去饭店点餐不管是宫保鸡丁还是番茄炒蛋流程永远是一样的下单→后厨备菜→烹饪→出锅上菜。每家饭店的具体做菜方式不同但这个流程骨架是固定的。模板方法模式就是干这个事的——把流程骨架定死在父类里把“怎么做这道菜”留给各家饭店自己发挥。用Java术语翻译一遍父类里定义一个模板方法里面调用了若干个抽象方法或具体方法其中一部分抽象方法留给子类去实现。父类管“什么时候做”和“做的顺序”子类管“具体怎么做”。这就是模板方法模式的全部思想没有高深的东西本质就是“继承多态”的组合运用。2. 模板方法模式的三个核心角色以及它的语法讲究2.1 三个核心角色抽象模板、具体模板、客户端模板方法模式跟大部分设计模式一样角色划分非常清晰一共就三个角色。第一个是抽象模板类Abstract Class它负责两件事定义模板方法就是那个流程骨架以及声明若干抽象方法或钩子方法。模板方法通常用final修饰防止子类覆盖整个流程这一点很重要后面我会详细解释。第二个是具体模板类Concrete Class它继承抽象模板类实现那些抽象方法也就是把流程中“因人而异”的部分填充出来。一个抽象模板类通常对应多个具体模板类每个具体模板类代表一种流程实现方式。第三个是客户端Client它负责实例化具体的子类对象然后调用父类声明的模板方法——注意客户端直接调用的必须是模板方法而不是那些零散的步骤方法。这三个角色的关系我顺手用代码结构给你看一眼就明白// 抽象模板类 public abstract class AbstractClass { // 模板方法定义算法骨架final修饰防止子类篡改流程 public final void templateMethod() { stepOne(); stepTwo(); stepThree(); } // 抽象方法子类必须实现 protected abstract void stepOne(); protected abstract void stepTwo(); // 具体方法父类已实现子类可直接复用 private void stepThree() { System.out.println(第三步公共逻辑谁来了都一样); } }2.2 为什么模板方法要用final修饰我面试过不少人能说清模板方法模式三个角色的不少但被问到“为什么模板方法要用final修饰”时能答到点子上的没几个。关键在于“模板”两个字的分量。模板的意思是流程的骨架、顺序、步骤数量都是确定好的不允许任何人改动。如果你不把模板方法用final修饰那子类继承之后完全可以覆盖掉整个方法自己重新写一套流程——那还叫模板方法模式吗不叫了那叫子类自己定义逻辑父类的方法就形同虚设了。你可以理解为饭店老板定了“点餐→上菜→结账”的流程这是招牌规矩但如果允许每个服务员随意改流程有的先结账再上菜有的先上菜再点餐那饭店就乱套了。final关键字就是“招牌规矩”的护身符保证流程不会被子类破坏。但是模板方法里有些步骤也需要允许子类覆盖——这就是抽象方法存在的意义。抽象方法不带方法体强制子类实现实际上就是“流程已定具体环节开放谁继承谁负责”。2.3 钩子方法模板方法模式里的秘密武器面试追问的时候面试官很可能会问到一个进阶概念钩子方法Hook Method。钩子方法是一个默认有实现通常是空实现或者返回一个默认值的方法子类可以视情况决定要不要覆盖它。它跟抽象方法的区别是抽象方法是强制实现的“必答题”钩子方法是可覆盖可忽略的“选做题”。钩子方法最常见的两种用途是让子类决定某一步骤是否执行或者让子类在流程的关键节点插入额外逻辑。public abstract class DataParser { // 模板方法定义解析流程 public final void parse() { openFile(); parseHeader(); // 钩子方法默认模板即可子类可覆盖增强 parseContent(); // 抽象方法子类必须实现 closeFile(); } private void openFile() { /* 公共逻辑 */ } // 钩子方法默认空实现子类可覆盖 protected void parseHeader() { } // 抽象方法子类必须实现 protected abstract void parseContent(); private void closeFile() { /* 公共逻辑 */ } }钩子方法的引入让模板方法模式从“固定流程强制实现”升级为“固定流程可选扩展”灵活性顿时上了一个台阶。面试时你能提一句钩子方法并且举一个实际用法面试官基本就能判定你是真懂而不是背八股。3. 代码实战不用模板方法模式的写法 vs 用模板方法模式的写法3.1 真实业务场景两个接口的请求处理逻辑光讲概念不写代码那跟背八股没区别。我拿一个真实得不能再真实的场景来对比演示现在项目里有两个第三方接口要对接一个是微信支付回调一个是支付宝支付回调。两个接口接收回调后要做的事情几乎一样校验签名 → 解析业务数据 → 更新本地订单状态 → 返回结果给第三方。但是中间细节完全不同微信和支付宝的签名算法不一样解析出来的数据结构不一样返回给第三方的成功/失败报文格式也不一样。唯一一样的是什么是这四个步骤的顺序和整体流程。这个场景简直是为模板方法模式量身定做的。3.2 第一版不用模板方法模式的写法很多同学第一版代码会怎么写两个类各写各的流程复制粘贴public class WechatPayCallback { public String handleCallback(String requestData) { // 1. 验签 boolean signValid verifySign(requestData); if (!signValid) { return fail; } // 2. 解析数据 OrderInfo orderInfo parseData(requestData); // 3. 更新订单状态 updateOrderStatus(orderInfo); // 4. 返回结果 return success; } private boolean verifySign(String requestData) { // 微信签名校验逻辑... return true; } private OrderInfo parseData(String requestData) { // 微信报文解析逻辑... return new OrderInfo(); } private void updateOrderStatus(OrderInfo orderInfo) { // 更新订单逻辑... } } public class AlipayCallback { public String handleCallback(String requestData) { // 1. 验签 boolean signValid verifySign(requestData); if (!signValid) { return fail; } // 2. 解析数据 OrderInfo orderInfo parseData(requestData); // 3. 更新订单状态 updateOrderStatus(orderInfo); // 4. 返回结果 return success; } // 细节实现各不相同... }你看出问题来了吗两个类的handleCallback方法几乎一模一样唯一的区别在verifySign、parseData这些具体步骤的实现。这就是典型的“流程重复、细节不同”的代码坏味道。如果将来要接入新的支付渠道比如银联你又得复制一遍整个流程——代码越来越臃肿改一个公共逻辑比如增加日志就得四处同步修改漏改一处就出线上事故。换句话说你复制的不只是代码还是潜在的bug和维护成本。3.3 第二版用模板方法模式重构用模板方法模式重构之后结构变成这样// 抽象模板类 public abstract class PayCallbackTemplate { // 模板方法流程骨架final保护 public final String handleCallback(String requestData) { boolean signValid verifySign(requestData); if (!signValid) { return fail; } OrderInfo orderInfo parseData(requestData); updateOrderStatus(orderInfo); return success; } // 抽象方法子类必须实现各自的验签逻辑 protected abstract boolean verifySign(String requestData); // 抽象方法子类必须实现各自的解析逻辑 protected abstract OrderInfo parseData(String requestData); // 具体方法公共逻辑父类实现 private void updateOrderStatus(OrderInfo orderInfo) { System.out.println(更新订单状态通用逻辑); // 这里可以做统一的订单状态流转、发送通知等公共操作 } }两个子类只需要安心实现自己的差异部分public class WechatPayCallback extends PayCallbackTemplate { Override protected boolean verifySign(String requestData) { // 微信验签逻辑 return true; } Override protected OrderInfo parseData(String requestData) { // 微信报文解析 return new OrderInfo(); } } public class AlipayCallback extends PayCallbackTemplate { Override protected boolean verifySign(String requestData) { // 支付宝验签逻辑 return true; } Override protected OrderInfo parseData(String requestData) { // 支付宝报文解析 return new OrderInfo(); } }看到区别了吗公共流程只写一份子类各管各的差异逻辑。将来接入银联或者抖音支付新建一个类继承PayCallbackTemplate实现自己那两个抽象方法就行了一行公共逻辑都不用改。模板方法模式解决的核心问题就是把不变的流程骨架和可变的步骤实现彻底分离用继承关系把两者优雅地组合在一起。3.4 重构前后对比这段代码的价值在哪我上面这套代码大概是面试中最容易让面试官产生好感的回答方式了。原因很简单它展示了你的抽象能力——你能从两个具体的业务场景中找到共同点并把这个共同点提出来放进父类还展示了你的工程思维——你衡量了扩展成本将来的新增渠道只是新增一个子类而不是再复制一遍流程。面试官追问“为什么要用模板方法模式”的时候你就用我上面那个对比回答第一消除重复代码公共逻辑只写一份第二扩展性好新增支付渠道只需新增子类第三保证流程一致性final修饰的模板方法确保所有子类都严格按同一套流程执行不会被某个特立独行的子类带偏。4. 框架源码里的模板方法模式JDK和Spring都在用4.1 JDK里的模板方法模式AbstractList和Arrays面试官特别喜欢问“你熟悉哪些JDK源码里的设计模式”因为这个问题能看出你平时看不看源码。模板方法模式在JDK里遍地都是最典型的就是java.util.AbstractList。AbstractList是一个抽象类它实现了List接口并提供了一个关键的模板方法iterator()这个方法的流程是固定的返回一个Itr迭代器对象该迭代器内部实现了hasNext()、next()、remove()等逻辑。而AbstractList把get(int index)和size()定义为抽象方法子类ArrayList、LinkedList必须实现这两个方法——这就构成了模板方法模式流程骨架在AbstractList定义具体实现交给子类完成。再看java.util.AbstractMap它也用了同样的套路把entrySet()抽象方法留给子类而get()、containsKey()等公共方法通过调用entrySet()来协同完成。这属于“父类骨架子类细节”的经典组合。4.2 Spring里的模板方法模式JdbcTemplate和RestTemplate到了Spring全家桶模板方法模式更是刷屏级的存在。JdbcTemplate的execute()方法就是教科书级的模板方法。Spring把整个JDBC操作流程封装好获取连接→创建语句→执行SQL→处理结果集→释放资源这个流程骨架是固定的不允许你更改。但作为一个使用者你需要覆盖的核心步骤——也就是SQL语句和结果集映射——通过回调接口或者匿名内部类的方式注入进去。严格来说JdbcTemplate用的是回调Callback跟“继承抽象方法”的模板方法模式略有区别但设计思想是同源的框架定流程用户填细节。面试中能把这个弯子绕清楚同时说出“JdbcTemplate是模板方法思想的变种它使用组合而非继承来实现步骤定制”妥妥的加分项。再比如RestTemplate它封装了HTTP请求的标准流程组建URL→发起请求→处理响应→转换消息体。这些流程固定不变但对不同接口调用方来说具体请求头和响应反序列化方式各不相同通过覆盖doExecute()等方法就能定制差异部分。这同样是模板方法思想的框架级落地。4.3 Servlet里的模板方法HttpServlet一个面试加分细节还有一个很多面试者容易忽略的例子是HttpServlet。你写Servlet的时候继承HttpServlet重写doGet()或者doPost()但HttpServlet里真正的入口方法service()却是模板方法。它负责的是拿到请求之后判断HTTP方法类型是GET还是POST再分发给对应的doGet或doPost方法。这个流程骨架在父类写死子类不需要关心“怎么分发”只需要关心“具体怎么处理”。我每次面到设计模式这块候选人能主动提到HttpServlet这个例子的十个里大概只有一两个。你能提出来就说明你平时不光看框架连Java Web的老底子都在脑子里面试官对你是会高看一眼的。5. 模板方法模式 vs 策略模式面试最常问的两个对比5.1 一位极其阴险的追问“模板方法模式和策略模式有什么区别”这个问题我几乎逢面试必问。这两个模式都属于行为型设计模式功能上都是“算法可以变”但从实现方式到适用场景完全是两条路。模板方法模式用的是继承它把算法骨架放在父类里子类通过继承父类并实现抽象方法来定制算法。它的重点是“骨架固定、步骤可变”适合流程本身相对稳定、只有局部环节不同的场景。我上面写的支付回调就是经典代表。策略模式用的是组合它把算法整体封装成独立的策略类客户端通过持有不同策略对象来更换算法。它的重点是“整个算法都可以替换”适合算法维度较多、彼此独立、可能动态切换的场景。比如一个订单价格计算器有普通价、会员价、促销价三种计价策略客户端运行时按需选择。用一句话总结差异模板方法是“流程固定细节多态”策略模式是“整体算法可插拔”。模板方法模式的“变”体现在局部步骤上策略模式体现的“变”则体现在整体算法的替换上。5.2 追问升级你都遇到过哪些场景应该选哪个面试官不会满足于“区别是什么”他一定会追问“你项目里怎么选”我把我的选择标准分享给你如果一个流程的步骤数量和先后顺序基本不会变变的只是其中某几步的具体逻辑优先选模板方法模式。因为骨架固定能避免大家各写各的流程导致逻辑混乱比如支付回调、数据导入导出、资源初始化这些都是流程非常稳定的场景。如果整个算法可能整体替换甚至运行时动态切换那就选策略模式。比如第三方支付渠道的对接每个渠道的验签、下单、退款逻辑完全不同而且这些算法之间没有“步骤骨架”这一说就应该各写一个策略类由上层按需选择。回到我上面那个支付回调的案例它的验证、解析、回调这几步顺序是强固定的所以模板方法模式明显更合适。如果面试官问“那支付渠道对接为什么不用策略模式”你可以回答策略模式适合算法整体替换但回调流程的骨架是共通的用模板方法模式能最大程度复用公共步骤如果非要用策略模式就得在每个策略类里重复写一遍流程代码劳而无功。这个回答逻辑能立住面试官基本挑不出毛病。5.3 什么时候“别用”模板方法模式有经验的程序员都知道设计模式不是越多越好。模板方法模式用继承实现继承本身是一种强耦合关系父类和子类深度绑定。如果子类的数量和差异逻辑急剧增多父类也需要频繁改动或者子类之间共享的公共逻辑很少那模板方法模式反而会带来维护负担。这时候组合优于继承策略模式或者组合回调的方式更合适。我个人判断的一句话标准是如果你发现有大量子类只是为了覆盖一个方法而存在父类里的其他模板步骤几乎没人用那说明你的抽象方式选错了。过度使用模板方法模式会让你造出一个臃肿的父类子类全被它绑架牵一发而动全身。学会一个模式不难难的是知道什么时候该放手。6. 面试实战这样回答模板方法模式面试官直接给你高分6.1 面试官为什么揪着模板方法模式不放设计模式几十个面试官偏偏爱挑模板方法模式出来问。原因有三个第一它足够简单几乎所有候选人至少听说过第二它是一个非常好的“思维能力试金石”从候选人怎么讲这个模式能看出他平时写代码的习惯、抽象能力、框架理解深度第三它跟JDK和Spring关联极深从这一个点可以发散问出整整一条知识链。所以面试官问模板方法模式重点不是考你有没有背过定义而是考察你能不能把定义投影到实际代码里用你自己的话说明白建模逻辑。6.2 回答框架定义→作用→场景→代码→对比我建议你面试时按这个“五步法”回答既有条理又能控制节奏第一步一句话说定义模板方法模式在一个方法中定义算法骨架步骤延迟到子类实现。这里不要慌先亮结论。第二步说作用消除重复代码、固定流程结构、方便扩展。第三步举场景用我上面那个支付回调的例子或者你自己项目里更贴近的例子。注意一定不能只背定义不给场景没有场景的设计模式回答面试官耳朵会自动关闭。第四步现场画图或写代码如果条件允许直接在纸上写出抽象模板类和子类的代码骨架。不用写完整能把final模板方法、抽象方法、子类覆盖三个点画出来即可。第五步主动补充对比谈话间把策略模式和模板方法模式的区别提出来把钩子方法也顺带提一句。“这里我还想补充一个点模板方法模式里有一种钩子方法用于在流程节点插入可选逻辑比如……”——这么一说面试官会觉得你想的比题目本身深一层。6.3 常见追问合集与参考答案我整理了面试问到的高频追问给你逐条备好答案。追问一“模板方法模式的优点是什么”答复用公共代码流程结构统一扩展性好。新增逻辑只需新增子类符合开闭原则。追问二“缺点呢”答继承带来的高耦合父类修改可能影响所有子类类数量膨胀子类过多时维护成本上升。追问三“模板方法可以用接口实现吗”答可以但很别扭因为接口不能提供具体实现模板方法本身需要留一部分公共逻辑在父类里接口做不到这一点。所以模板方法模式的实现基础是抽象类而不是接口。能答出这一点说明你踩过设计的坑。追问四“模板方法模式和建造者模式有相似之处吗”答有一点点都是“流程固定”的思想但建造者模式强调复杂对象的构建过程拆分和逐步装配模板方法强调算法步骤的骨架复用。构建者和模板方法分别在对象构建和流程复用两个不同维度做事。追问五“你用过模板方法模式做过重构吗”答这个问题要诚实。我用过上面支付回调重构那个就是我的实战案例。如果你还没有真实案例可以说“我虽然在正式项目里没直接用过但我看过JdbcTemplate和HttpServlet源码并且自己写过Demo验证过思想”——千万别硬编造不存在的项目经验面试官追问细节你就露馅了。7. 实战避坑心得模板方法模式用得好是利器用得不好是灾难7.1 第一坑模板方法里调用被重写的方法却忘了考虑执行顺序很多人在父类模板方法里写多步骤时容易忽略一个隐性依赖问题步骤顺序不是随意的步骤之间存在前置关系。比如支付回调里“验签”必须在“解析数据”之前因为如果签名都验不过后面解析出来的数据根本没意义。实战中你可以在模板方法里加一段防御性逻辑来兜底比如在模板方法的每步之间侵入日志或者在关键节点用钩子方法来做“下一步是否执行”的判断。别小看防御性逻辑生产环境里模板方法一旦被多个子类使用公共步骤的开销会被无限放大没有日志和监控几乎是裸奔。7.2 第二坑滥用final把需要扩展的口子堵死了final修饰模板方法没问题但有些人脑子一热把抽象类里的公共方法也统统final了导致子类想做点扩展都没有任何可以下手的地方。正确的分寸是模板方法本身要final流程步骤里的具体方法可以按需开放为可重写的钩子方法。我见过一个线上事故就是这种滥用final导致的一个资深开发把数据导出逻辑写成了模板方法且全部步骤final结果业务方要新增一个自定义导出格式不得不绕过父类又复制了一遍整个流程。这正好把模板方法模式建立的意义给推翻了。7.3 第三坑层级太深多级继承让人根本看不懂模板方法模式容易诱使人把类继承层级越拉越深抽象模板类→中间抽象类→具体子类→更具体的子类。三层以内还好说超过三层读代码的人就要骂娘了。每层的职责开始模糊公共逻辑不知道在哪一层改一个需求从那个层级入手全靠猜。我的建议是保持两层继承结构一个抽象模板类下面直接挂具体子类。如果公共逻辑实在太多可以考虑用组合方式把一部分公共逻辑拆到辅助类里别硬塞到继承链中。7.4 第四坑子类间公共逻辑没提取干净一部分留在子类里这是最难看出的问题。当你写完几个子类之后回头检视发现这些子类之间还存在一小部分重复代码量很小往往就被忽略了。但所谓坏味道就是要命的子类里残存的重复代码其实就是父类抽象没提干净的信号。检验标准很简单改一个相同的需求你改完一个子类还得改其他子类那就说明公共逻辑还没完全上提。最后说一句实在的。模板方法模式是我个人认为对Java面试者性价比最高的设计模式之一它简单、高频、贴近框架源码而且能很好地展示你的抽象思维。你在准备时把定义记牢把支付回调这个场景吃透再把JdbcTemplate、HttpServlet这些框架例子顺一遍这个分数基本就稳了。别贪多嚼不烂一口气背二十个设计模式不如把模板方法模式这一个做到问不倒。按照我上面那个“五步法”来组织你的口头表达面试时你会发现自己比想象中从容得多。
返回列表