行为型模式收官:迭代器、备忘录、职责链、解释器、访问者实战解析

📅 发布时间:2026/10/13 16:20:08
行为型模式收官:迭代器、备忘录、职责链、解释器、访问者实战解析
1. 行为型模式的最后一课先把11种模式的版图铺开写行为型模式三之前我自己先对着GoF的23种设计模式总数盘了一遍。创建型5种、结构型7种剩下的11种全部属于行为型是三大类里数量最多、也是最容易让初学者崩溃的一块。很多读者学到行为型时会明显感觉节奏不对创建型和结构型更多在讲“类怎么搭、对象怎么组装”一眼能看出结构行为型却到处都是“对象之间怎么协作、怎么分配职责、怎么交流”没有一幅类图压阵立刻就觉得飘了。行为型模式的核心说穿了只有一句话把对象之间的行为协作从硬编码中抽离出来通过一系列成熟的协作模型让扩展新行为、调整调用链路、控制对象状态变得更加可控。这11种模式的分布我在教学和面试辅导中习惯把它们分成“两条半轴线”一条偏向“算法的封装与替换”包括模板方法、策略、命令另一条偏向“状态和变化的感知”包括观察者、状态、中介者剩下的迭代器、备忘录、职责链、解释器、访问者这5种更像是一群“既有流程思维又有结构思维的混合型选手”。如果你已经看完了这个系列的前两篇应该对模板方法、策略、命令、观察者、状态、中介者有了整体印象。那这一篇要补上的就是最后5种迭代器、备忘录、职责链、解释器、访问者。它们有个共同特征看起来都不算复杂但考试、面试、写业务代码时踩坑的概率非常高。因为前6种在框架和工具里你能高频见到后5种更像“你平时用它但根本没意识到这就是模式”。这篇文章我会按“每一种模式解决什么痛点、结构怎么摆、代码怎么落地、常见的坑在哪里”这种顺序来讲而不是只给一段UML图然后甩标准代码。对于准备软考、期末考和面试的人来说后5种尤其需要理解清楚“为什么要这样设计”因为选择题和八股问答考的就是你分辨模式的能力。对于想真正写代码的人我也准备了能直接跑的Java示例和实践中我会调整的细节。2. 迭代器与备忘录遍历与回滚的两种“底层思维”2.1 迭代器模式把遍历的主动权交给调用方迭代器模式可能是大家最早接触的GoF模式之一只是很少人把它当成模式来学。你用Java写一个for (String s : list)底层就是Iterator。你用C遍历std::vectorint底层的begin()和end()返回的也是迭代器。模式定义说得比较文绉绉提供一种方法顺序访问一个聚合对象中的各个元素而又不需要暴露该对象的内部表示。为什么要绕这么一圈我打一个生活中的比方。自助餐厅的取餐台是聚合对象你端着盘子从凉菜区走到热菜区再走到甜品区你不需要知道厨房内部怎么摆放食材、后厨有几个仓库你只需要有人告诉你“下一个窗口在哪”。迭代器承担的就是“取餐引导员”的角色它掌握了当前走到哪了、下一步去哪而取餐台本身不必把所有档口都摊在你面前。在实际的Java代码里迭代器模式主要体现在Iterable和Iterator这两个接口。Iterable约定“这个集合可以被遍历”Iterator约定“我可以按顺序给你下一个元素”。我们自己动手写一个极简迭代器能很清楚地看到模式的骨架。比如我有一个“消息队列”类内部用一个ArrayList存消息但我不想让外部直接拿到这个List去随便增删于是让内部类实现迭代器public class MessageQueue implements IterableMessage { private final ListMessage messages new ArrayList(); public void add(Message msg) { messages.add(msg); } Override public IteratorMessage iterator() { return new Iterator() { private int index 0; Override public boolean hasNext() { return index messages.size(); } Override public Message next() { if (!hasNext()) { throw new NoSuchElementException(没有更多消息了); } return messages.get(index); } }; } }这种写法的好处不只是“隐藏内部结构”。调用方拿到的是迭代器接口那么内部无论是ArrayList、LinkedList还是一棵二叉树都不影响外部遍历方式。更进一步你可以同时开多个迭代器各自维护自己的游标位置互不干扰如果你直接对外暴露List别人就能在遍历中途remove很容易把程序搞出并发修改异常。从面试角度我比较建议大家记住一个点迭代器模式的本质是“将遍历行为从聚合对象中分离”。问到你“为什么要用迭代器而不是直接for循环访问数组”标准回答是对数组可以用下标但对树、图、自定义聚合结构下标遍历不通用迭代器把“怎么遍历”封装在迭代器内部聚合对象和调用方都解耦了。2.2 备忘录模式给对象开一扇“后悔药”窗口备忘录模式是我见过最直白的模式之一。定义听起来也很朴素在不破坏封装性的前提下捕获一个对象的内部状态并在该对象之外保存这个状态以便之后能将对象恢复到原先保存的状态。你可以把它理解成游戏存档。你打一个RPG游戏打Boss之前存个档打输了读档再战。存档文件里记录了你当前的血量、蓝量、位置、经验值这些就是“备忘录”。游戏主体不直接暴露自己内部的所有字段而是提供一个方法生成存档另一个方法从存档恢复。外部只管保存和找回存档文件不需要知道存档里到底有哪些字段。从结构上看备忘录模式有三个角色发起人Originator需要被存档的对象、备忘录Memento保存状态的快照对象、管理者Caretaker负责保管备忘录不修改备忘录内容。关键约束在于管理者不能碰备忘录的内部数据否则就破坏了封装性。Java里实现这个约束最常见的方式是把备忘录类作为发起人的内部类甚至用私有内部类让外部根本拿不到具体的字段结构只能把它当成一个不透明对象保存。我写一个文本编辑器的撤销快照示例public class TextEditor { private StringBuilder content new StringBuilder(); public void write(String text) { content.append(text); } public String getContent() { return content.toString(); } // 生成快照 public Snapshot save() { return new Snapshot(content.toString()); } // 从快照恢复 public void restore(Snapshot snapshot) { this.content new StringBuilder(snapshot.getSavedContent()); } // 备忘录作为内部类外部只能持有引用不能修改内容 public static class Snapshot { private final String savedContent; private Snapshot(String content) { this.savedContent content; } private String getSavedContent() { return savedContent; } } }外部使用时用一个History类去保存一堆Snapshot要做撤销就调restore。这里有个容易被忽略的点Snapshot里的字段必须是不可变的。如果快照保存的是一个对象引用而发起人后续又改了那个对象的内部字段快照就被污染了。所以保存字符串时我用的是String而不是StringBuilder正是为了隔离风险。真正在业务落地时备忘录模式最常见的场景是编辑器撤销、数据库事务回滚、分布式系统中的补偿事务。你会发现这些场景都有一个共同特征——系统需要“回到过去某个安全状态”。不过我要泼一盆冷水如果你只是在做普通Web业务尽量不要为一个实体类频繁保存全量快照内存开销会失控。真要做也应该考虑增量备份、压缩或者只备份关键字段而不是无脑深拷贝整个对象图。2.3 两个模式放在一起看迭代器负责“向前”备忘录负责“向后”把迭代器和备忘录放在同一篇文章里讲很多人会觉得它们八竿子打不着。但如果你从“行为控制”的角度看它们其实是一对方向相反的机制迭代器帮你稳定、安全地向前遍历聚合对象备忘录帮你可靠地回退到历史状态。一个控制系统未来的访问顺序一个控制系统过去的状态。这种对比不是纯理论。我实际做过一个数据迁移工具要从一个老系统读取上百万条记录再根据失败情况分批回滚。读取的时候我用迭代器模式把不同数据源MySQL、Oracle接口、甚至远程Excel封装成统一的遍历入口回滚的时候我用备忘录模式保存每个批次的导入快照批次失败就能精确撤销不会影响其他批次。很多软考和面试题喜欢把这两个模式作为“低难度送分题”但同时也会把它们的细节挖得很深。比如迭代器常考“快速失败机制fail-fast”也就是集合结构在迭代过程中被修改时迭代器会立刻抛出ConcurrentModificationException备忘录则常考“快照的封装性怎么保证”“多级撤销怎么设计”。这些细节就是你和其他候选人拉开差距的地方光背UML图是答不出来的。3. 职责链与解释器层层上报与文法翻译的精髓3.1 职责链模式谁来处理由链上的节点说了算职责链模式是我在中间件代码里见到最多的行为型模式之一。它的定义是使多个对象都有机会处理请求从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链并沿着这条链传递请求直到有一个对象处理它为止。生活里的例子最好懂你在一家公司提交一笔报销审批单。小组长先看金额超过1000就往上递交部门经理再看超过5000再往上递交总监再看……每个审批人只处理自己职权范围内的请求处理不了就往后传。这种审批流天然就是一条职责链。在代码结构上职责链模式一般会让所有处理器实现同一个抽象接口接口里有一个“设置下一级处理器”的方法还有一个“处理请求”的方法。每个处理器处理不了时就把请求交给next。我用一个电商订单风控的例子来写场景比报销更贴近开发者的日常public abstract class RiskHandler { protected RiskHandler next; public void setNext(RiskHandler next) { this.next next; } public abstract boolean handle(Order order); protected boolean nextOrPass(Order order) { if (next null) { return true; // 整条链都通过了 } return next.handle(order); } } public class BlacklistHandler extends RiskHandler { Override public boolean handle(Order order) { if (checkBlacklist(order.getUserId())) { System.out.println(命中黑名单拒绝下单); return false; } return nextOrPass(order); } } public class FrequencyHandler extends RiskHandler { Override public boolean handle(Order order) { if (overFrequencyLimit(order.getUserId())) { System.out.println(下单频率过高触发限制); return false; } return nextOrPass(order); } } public class AmountHandler extends RiskHandler { Override public boolean handle(Order order) { if (order.getAmount() 50000) { System.out.println(大额订单转人工审核); return true; } return nextOrPass(order); } }你会发现职责链模式的最大价值在于请求的发送者完全不知道谁会处理自己的请求新增一个风控规则也只需要新增一个Handler并追加到链上对已有代码零修改高度符合开闭原则。像Spring MVC的拦截器、Servlet的Filter、Java日志框架里的Logger链本质上都是职责链的工程化变体。但这里我要重点提醒一个坑职责链不是非要“链到头”。设计时要想清楚三个问题第一请求走到链尾都没人处理怎么办是默认通过还是默认拒绝第二链上某个处理器抛异常是中断整条链还是跳过第三处理器之间的顺序是否敏感比如黑名单检查和频率检查顺序互换结果是否还一样我在实际项目里就见过一个很典型的事故因为某个新人在过滤器链上把“身份鉴权过滤器”排在了“参数校验过滤器”之后导致非法请求先打到了数据库参数校验通过后才发现token过期浪费了资源不说还污染了日志排查链路。3.2 解释器模式小语言的优雅实现也是类爆炸的开始解释器模式是行为型模式里最容易被高估、也最容易被误解的一个。定义是给定一门语言定义它的文法的一种表示并定义一个解释器该解释器使用该表示来解释语言中的句子。听起来非常抽象但你几乎每天在用它的工程化产物正则表达式引擎、SQL解析器、Spring的SpEL表达式、编译器的语法分析器都是解释器思想。它解决的问题是如果一段“规则”以字符串形式输入而这段规则的组合方式是无限多的你不可能为每一种组合写一个if-else这时候就需要定义一套“文法”让程序能读懂这句话并执行。为了让你彻底理解我写一个经典到不能再经典的“加减法表达式计算器”。假设表达式是3 5 - 2用解释器模式解析它至少要定义两个角色终结符表达式数字和非终结符表达式加法、减法。这里的“终结符”你可以理解成表达式中不能再拆分的原子单位数字3就是一个终结符“非终结符”则由多个子表达式组合而成比如3 5整体是一个加法表达式。public interface Expression { int interpret(); } public class NumberExpression implements Expression { private final int number; public NumberExpression(int number) { this.number number; } Override public int interpret() { return number; } } public class AddExpression implements Expression { private final Expression left; private final Expression right; public AddExpression(Expression left, Expression right) { this.left left; this.right right; } Override public int interpret() { return left.interpret() right.interpret(); } }定义好之后客户端要做的就是用这些表达式对象拼成一棵“语法树”然后调用根节点的interpret()。你会不会觉得这套东西又麻烦又绕确实。如果只是做计算器直接写个栈扫描字符串比这简单得多。解释器模式真正的适用边界是当规则数量多且频繁变化并且规则可以递归组合时它的优势才体现出来。我很负责任地告诉你绝大多数业务项目根本不需要自己实现解释器。因为实现一个文法需要考虑优先级、括号、词法分析、语法错误处理代码量会迅速膨胀类数量成倍增加。真正到了需要解析复杂语言的程度正确做法往往是引入成熟的解析器库比如Java里的ANTLR或者干脆用现成的表达式引擎如Aviator、SpEL。解释器模式更像你考试要懂、面试要能讲但实际工程中必须克制使用的模式。过度设计的第一标志就是有人拿解释器模式去写一个只需要三五个分支判断的规则系统。3.3 常见误区责任链不是状态机解释器不是正则的替代品把这两类模式放在一起后我发现很多新手会把它们跟相邻概念搞混。职责链常被误当成状态机其实职责链的核心是“请求在节点间传递每个节点决定自己要不要处理”状态机的核心是“对象内部状态变化驱动行为切换”前者关注请求分配后者关注状态转移。举个对比购物订单“待支付→已支付→已发货→已完成”是状态机而“先风控校验→再库存校验→最后支付校验”是职责链两者不该混用。解释器模式则常被误解成“正则表达式的一种Java写法”甚至有人试图用它替代正则去解析一切文本。实际上解释器适合的是“结构化、可递归组合的规则”正则适合的是“按模式匹配的字符串”两者的抽象层级完全不同。比如你要解析IF age 18 THEN allow这种业务规则正则能匹配出关键字但没法优雅地构建规则语义解释器却可以从文法层面直接定义出条件表达式和动作表达式。在软考和面试中这两种模式最容易出现在“给出场景让你选设计模式”的题目里。只要看到“多个处理器都可以处理请求按顺序尝试直到有人处理”基本就是职责链只要看到“自定义规则语言、表达式解析、语法树”基本就是解释器。这种判断速度需要练而且要在理解了模式本质之后再练单纯背场景关键词很容易翻车。4. 访问者模式让数据结构与操作互相松绑4.1 双重分派的核心机制访问者模式是GoF 23种设计模式里公认最难讲清楚、也最容易被忽略的一个。它的意图很特别表示一个作用于某对象结构中的各元素的操作它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作。翻译成人话我们有一堆对象它们稳定不变但我们想给这堆对象不断添加新操作。访问者模式就是把这些新操作集中放到一个“访问者”类里让对象结构答应接待这个访问者。做到这一点的关键在于“双重分派”这也是访问者模式唯一真正的技术门槛。为了理解双重分派你可以先想普通的方法调用。Java里调用obj.method(arg)实际执行哪个方法取决于两点obj的运行时类型以及arg的编译时类型。大多数场景只有一重分派也就是对象的动态类型决定行为。访问者模式则要正向调用和反向回调配合客户端调用元素的accept(visitor)方法元素再反过来调用visitor.visit(this)this的动态类型在这一步被精确传递最终触达的是“访问者中针对该具体元素类型的重载方法”。这两个调用叠加在一起就是双重分派。我还是用一个具体例子来兜底。假设你有一个文件系统里面有三种文件文本文件、图片文件、视频文件。这些文件的类相对稳定不会频繁改动。但针对它们的操作很多比如统计大小、扫描敏感词、生成缩略图、转码。如果每个操作都往文件类里加方法文件类会越来越臃肿而且每次新操作都要改已有的类违反开闭原则。访问者模式的解法是让文件类只提供一个accept(FileVisitor visitor)方法把所有对外操作外包给访问者实现。文本文件收到访问者后调用visitor.visit(TextFile this)图片文件调用visitor.visit(ImageFile this)视频文件调用visitor.visit(VideoFile this)。每个访问者可以自己实现三套不同的处理逻辑互不干扰。4.2 Java示例为多种文件类型设计统计报表下面我用一段精简代码展示访问者模式的标准形态。首先是元素接口和具体元素public interface FileElement { void accept(FileVisitor visitor); } public class TextFile implements FileElement { private String name; private long size; public TextFile(String name, long size) { this.name name; this.size size; } public String getName() { return name; } public long getSize() { return size; } Override public void accept(FileVisitor visitor) { visitor.visit(this); } } public class ImageFile implements FileElement { private String name; private long size; // 构造方法、getter... Override public void accept(FileVisitor visitor) { visitor.visit(this); } }然后是访问者接口和具体访问者。注意访问者接口必须为每一种具体元素准备一个visit重载方法public interface FileVisitor { void visit(TextFile file); void visit(ImageFile file); } public class SizeReportVisitor implements FileVisitor { private long totalSize 0; Override public void visit(TextFile file) { totalSize file.getSize(); } Override public void visit(ImageFile file) { totalSize file.getSize(); } public long getTotalSize() { return totalSize; } }客户端就可以这样使用ListFileElement files Arrays.asList( new TextFile(readme.txt, 100), new ImageFile(logo.png, 2048) ); SizeReportVisitor visitor new SizeReportVisitor(); for (FileElement file : files) { file.accept(visitor); } System.out.println(总大小: visitor.getTotalSize());这个例子做完你会直观感受到访问者模式的强大和麻烦新增一种操作时只需要新建一个访问者实现类文件类一行不用改但新增一种文件类型时所有访问者接口和实现类都要跟着改。说白了访问者模式把“易变”从数据结构挪到了操作上适合操作频繁增加、数据结构相对固定的系统比如编译器中的AST遍历、报表系统、文档导出工具。4.3 什么时候该放弃访问者访问者模式是典型的“高收益高风险”模式我自己在代码评审里看到它时第一反应通常是警惕。因为它带来的抽象成本很高如果我看到有人只为了一两个操作就引入访问者我会直接建议改成简单的instanceof判断或者策略模式。放弃访问者的信号也很清晰第一你的对象结构本身经常变化比如每半年加一种新文件类型这时访问者模式会让所有访问者接口跟着改比直接改元素类还痛苦第二对象类型数量很少两三种而已instanceof分支完全能搞定没必要为双重分派引入额外层级第三你想要的操作本质上只是“对同一组数据做不同算法”这更适合策略模式而不是访问者模式。策略模式是针对同一个对象或同一组对象选择算法访问者模式是针对不同类型对象执行不同逻辑两者的出发点完全不同。还有一个实际业务里容易被忽视的问题访问者模式会让代码的调用链路变得隐晦。你点开TextFile.accept()发现它只是调了visitor.visit(this)真正的业务逻辑散落在各个访问者类中。对不熟悉这套模式的同事来说阅读成本很高。如果团队里有人不太理解双重分派我宁愿不用访问者也不愿给团队埋下认知负担。5. 后五种模式的横向对比与软考/面试记忆法5.1 一张表对齐“痛点、场景、记忆抓手”把迭代器、备忘录、职责链、解释器、访问者这5种模式放一起我用下面这张表把它们各自的痛点、典型场景和一句话记忆抓手对齐备考和面试前扫一眼很有用。模式解决的核心痛点典型场景一句话记忆抓手迭代器遍历方式与聚合对象内部结构耦合集合遍历、数据库游标、树结构前序/中序访问不暴露内部结构也能按顺序访问备忘录对象状态无法在不破封装的前提下保存恢复编辑器撤销、游戏存档、事务回滚存档、读档、后悔药职责链请求发送者与处理者之间耦合严重审批流、日志过滤器、中间件拦截器层层上报谁能处理谁处理解释器自定义规则语言难以用if-else穷举表达式计算、SQL解析、规则引擎定义文法构建语法树解释执行访问者数据结构稳定但操作频繁扩展编译器AST、报表统计、文件批量处理操作上门拜访双分派定类型这张表的妙处在于它把5种模式全部压缩成了“痛点”和“记忆抓手”。我在软考复习时也经常和学生说不要死记UML图里的类名先记住“每个模式要解决的那个不舒服”再反推结构正确率会高很多。比如你看到“多个对象都可以处理同一个请求”直接锁定职责链看到“需要保存对象历史状态以便撤销”直接锁定备忘录看到“遍历某种集合时不想暴露内部实现”直接锁定迭代器。5.2 容易混淆的三对模式别栽在选择题里软考和面试里比“不认识模式”更可惜的是把几个相近模式搞混。我总结了三对最容易错的组合第一对职责链和状态模式。状态模式的触发条件是“内部状态变化”对象自己改变自己的行为职责链的触发条件是“外部请求进入”链上节点各自判断要不要拦截。比如订单状态流转属于前者而请求经过Token校验、参数校验、权限校验属于后者。第二对策略模式和访问者模式。初学者经常搞混因为它们都涉及“对同一种输入做多种处理”。但策略模式是“同一对象、同类数据、互换算法”比如排序算法换成快排或归并访问者是“不同对象、不同数据、执行各自不同的逻辑”比如文本文件计字数、图片文件算尺寸。记忆方法是策略是“换套路”访问者是“分人接待”。第三对解释器模式和命令模式。命令模式是把一个请求封装成对象强调“请求的延迟执行、撤销、记录”解释器模式强调“按文法解析一条语言句子并计算”。你写一个SaveCommand用于编辑器保存那是命令你写一个AddExpression用于计算12那是解释器。这三对如果再配上一道典型题如果题目中出现“一个系统支持多种文件格式导出并且希望以后不断添加新的导出格式而不修改原有文件类”答案通常是访问者如果出现“系统要支持多种不同的压缩算法可以动态切换”答案通常是策略。这种题型每年软考都会出练熟之后基本是送分。5.3 针对软考选择题的速记口诀和判断技巧网络上有不少23种设计模式的记忆口诀但很多口诀只把模式名串起来没有告诉你怎么对应场景。我自己使用下来觉得更可靠的是把这一篇的5种模式串成一句话“遍录职解访”——遍历靠迭代器撤销录档靠备忘录请求流转靠职责链规则解析靠解释器结构操作靠访问者。每次做题前先念一遍这5个字然后对照题目里的“关键词场景”去匹配。判断题感也非常重要。我看到很多学生在软考上午题里能做对真到了下午案例分析题反而卡住。原因是选择题只需要你“认出模式”案例题需要你“说出为什么选它”。我的建议是在识别出模式后强迫自己多说三句话——它能解决什么耦合问题它涉及哪几个角色它违背了哪条设计原则会怎样比如你判断出该用迭代器就说它隔离了聚合对象与遍历逻辑如果不用它调用方就得自己拆解内部List结构。这样的答题深度远比堆模式名词更容易拿分。另外一个很实用的技巧是拿到场景题后先找“不变量”和“变化量”。不变量适合放进被稳定的类里变化量适合放进被扩展的类或接口里。访问者模式的不变量是文件元素类型变化量是操作所以操作放进访问者策略模式的不变量是业务目标变化量是算法所以算法放进策略接口。这个判断逻辑比背场景关键词更本质对真实项目设计同样适用。6. 写在最后第三篇之后行为型模式怎么收官如果你能跟着文章把迭代器、备忘录、职责链、解释器、访问者这5种模式挨个过完行为型模式这条线基本就闭环了。但我个人还是想说一句别满足于“认识”这5种模式要努力做到“在代码里认出来”。我自己当年学访问者模式时总觉得它是理论玩具直到后来看编译器源码里的AST遍历部分看到accept和visit成对出现才真正把它们对上号。职责链也是我第一次用Spring Security时看到过滤链上一堆Filter才意识到这不就是职责链在框架里的量产实践吗。设计模式的学习路径永远是先抽象理解再代码实现最后在成熟框架里反向印证。如果你正在赶软考或者期末我的建议是把这一篇和本系列的前两篇合并成一张模式总表按“创建型5种、结构型7种、行为型11种”的维度复习。对于行为型的11种重点记住策略、观察者、状态、命令、职责链这5个高频率考点其余模式理解到“能识别、能画出结构、能说出使用场景”的程度就够了。很多大作业和面试项目用不上把23种模式都堆进去能用对三五个关键模式反而比强行使用十几个模式要高得多。回到博客本身行为型模式三里的5种模式其实是整套设计模式里“思维含量”偏高的一组。迭代器和备忘录像一对时间方向相反的兄弟职责链和解释器一个管请求路由、一个管规则解析访问者则用双重分派把“开闭原则”玩到极致。把这些逻辑揉进自己的知识体系以后再看到复杂代码你就不会只看到一堆类在互相调来调去而是会自然地问一句这背后是不是藏了某个模式这种看代码的“透视感”才是设计模式真正回馈给你的东西。