1. 行为型模式里最容易被人放在一起比较的两兄弟
事情得从一次代码评审说起。当时组里有个同事在处理一份配置文件的解析逻辑,代码里写了一大堆 if-else 去判断不同的命令关键词,然后在另一个模块里又写了一堆游标逻辑去逐个遍历结果集。评到一半,另一个同事随口说了句:"这不就是解释器模式吗?下面那个是迭代器模式吧?"大家都点头,但又都说不清楚这两个东西到底差在哪——除了都是行为型设计模式之外。
这个场景我见过太多次了。设计模式的书里,解释器模式和迭代器模式常常被安排在同一章,因为它们的名字都有点"动作感",感觉都是"在处理什么东西"。但很多人学着学着就会发现一个问题:解释器模式好像很难用到,迭代器模式又好像天天在写但自己没意识到。实际上,这两者虽然同属于行为型设计模式,但一个是在制造语言,一个是在规划路径,解决问题的维度完全不同。搞混了,轻则设计出来的代码结构别扭,重则在错误的方向上加抽象,把本来能解决的问题搞成一堆永远不会被调用的类。
这篇文章我想把这两个模式从头到尾拆开讲一遍,包括各自的本质问题、典型结构、代码示例、适合的场景,以及一套我认为比较实用的选型判断框架。如果你在学设计模式、准备面试,或者正在纠结"我这里到底该用哪个模式",这篇文章可以给你一个相对清晰的参考。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 解释器模式:它解决的是"如何定义一个语言并解释执行"的问题
2.1 从一份简单的规则表达式说起
先说解释器模式。它的 GoF 定义是:给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
这个定义太绕了。我换个说法:解释器模式就是让你能自己定义一套"小语言",然后写一套程序来读懂并执行这套语言写的"句子"。
举个例子。假设你在做一个日志分析系统,运营人员经常会问:"把昨天访问量超过 1000 且来自移动端的请求都筛出来"。如果每个这样的需求都要改代码,你迟早会被运营的 50 个类似需求逼疯。这时候你可能会想到,我能不能定义一种简单的查询语法?
code复制type=request AND count>1000 AND source=mobile
然后让运营自己在输入框里填这种表达式,你的系统去解析它、执行它,把结果返回。这时候问题就来了:怎么把一个类似 count>1000 的字符串转换成程序里的逻辑判断?怎么处理 AND 和 OR 的组合?怎么嵌套括号?
这种场景就是解释器模式的主场。你定义一套语法规则(文法),再为每条规则写一个解释器类。每个类代表语法树上的一个节点,节点之间互相组合,最终形成一棵能表达任意复杂表达式的树。
2.2 解释器模式的核心结构:语法树就是一张类图
解释器模式的类结构非常有特点,它把文法规则直接映射成了类结构。最基本的是这四个角色:
- 抽象表达式(AbstractExpression):声明一个 interpret(Context) 方法,是所有节点的父类。
- 终结符表达式(TerminalExpression):语法树中的叶子节点,不可再分。比如
count>1000里的1000、mobile,或者一个表示"数字"的节点。 - 非终结符表达式(NonterminalExpression):语法树中的分支节点,包含子表达式。比如
AND、OR、>, 这类节点需要递归调用子节点的 interpret 方法。 - 上下文(Context):存放语法解析过程中的全局信息,比如变量值、中间结果等。
写个最简化的 Java 示例。先定义一个表达式接口:
java复制public interface Expression {
boolean interpret(Map<String, String> context);
}
然后定义终结符表达式,比如一个"判断字段值等于什么"的节点:
java复制public class EqualsExpression implements Expression {
private String field;
private String value;
public EqualsExpression(String field, String value) {
this.field = field;
this.value = value;
}
@Override
public boolean interpret(Map<String, String> context) {
return value.equals(context.get(field));
}
}
再定义非终结符表达式,比如 AND 组合节点:
java复制public class AndExpression implements Expression {
private Expression left;
private Expression right;
public AndExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
@Override
public boolean interpret(Map<String, String> context) {
return left.interpret(context) && right.interpret(context);
}
}
再加一个 OR 组合节点:
java复制public class OrExpression implements Expression {
private Expression left;
private Expression right;
public OrExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
@Override
public boolean interpret(Map<String, String> context) {
return left.interpret(context) || right.interpret(context);
}
}
然后你需要一个解析器(Parser),负责把类似 type=request AND source=mobile 这样的字符串拆解成上面这棵由 Expression 节点组成的语法树。解析的过程通常是递归下降:读到 AND 就创建 AndExpression,把左边和右边的子表达式挂上去;读到 = 就创建 EqualsExpression。
测试的时候是这样:
java复制public class InterpreterDemo {
public static void main(String[] args) {
// 模拟一条日志记录
Map<String, String> logEntry = new HashMap<>();
logEntry.put("type", "request");
logEntry.put("count", "1500");
logEntry.put("source", "mobile");
// 构造表达式: type=request AND source=mobile
Expression typeEquals = new EqualsExpression("type", "request");
Expression sourceEquals = new EqualsExpression("source", "mobile");
Expression query = new AndExpression(typeEquals, sourceEquals);
boolean result = query.interpret(logEntry);
System.out.println("匹配结果: " + result); // true
}
}
2.3 什么时候真的需要解释器模式
必须承认:解释器模式是 GoF 23 种模式里实际使用频率最低的之一。原因很简单——如果真的需要一门完整的语言,你通常应该直接用现成的表达式引擎(比如 ANTLR、GraalJS、SpEL、Aviator),而不是从零手写一套。自己实现解释器的成本很高,而且容易出 bug。
但它有两个非常典型且无可替代的应用场景:
第一个是规则引擎和动态配置。如果规则语的规模非常小且固定,语法规则就几种(AND、OR、NOT、比较符),手写一个解释器反而比引一个大而全的外部引擎更轻量。规则数量少、变化不频繁、性能要求可控——这时候解释器模式的性价比就出来了。
第二个是复杂的字符串/文本解析。比如你内部定义了一个模板渲染语法 {{user.name}}、{% if age > 18 %},其中格式变化有限且可控,自己写个解释器去解析是常见做法。很多代码生成器、ORM 的条件构造器(比如 MyBatis 的动态 SQL)本质上也是一个小型解释器。
解释器模式最大的特点在于:它为每一种语法规则建立一个类,所以新增一种语法 = 新增一个类 + 在 Parser 里注册一条规则。好处是每条规则独立、好测试;坏处是一旦语法规则过多(几十上百种),类数量会爆炸,维护成本直线上升。所以我的经验是:语法种类在 10 个以内,解释器模式是个好选择;超过这个量级,考虑引入现成的语法分析器生成工具。
3. 迭代器模式:它解决的是"如何不暴露内部结构地遍历元素"的问题
3.1 你以为你在用 for 循环,其实背后就是迭代器
迭代器模式的定义:提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。
这个概念大家其实天天在用。Java 里的 Iterator、C++ STL 里的迭代器、Python 里的 for...in、JavaScript 里的 Array.prototype[Symbol.iterator],全都是迭代器模式的具体实现。甚至你在代码里写一个 for (String s : list),编译器在背后也会帮你翻译成迭代器调用。
但正因为它太常见了,反而容易被忽略。很多人在设计自己的容器类/集合类时,第一反应是"加一个 get(int index) 方法,外面的人自己循环取不就行了"。这就是典型的"没意识到迭代器模式解决的问题"。
设想一下:你实现了一个自定义的数据结构,比如一个内部是数组加链表的混合结构,或者一个从外部数据源懒加载数据的游标。你如果暴露 get(int index),调用方就必须了解你的索引规则;如果暴露底层数组,调用方就能直接修改你的内部状态。不管是哪种,你都把内部实现暴露给了外部,将来一旦你想改存储结构,所有调用方的代码都会跟着崩。
迭代器模式的思路是:你内部存储结构随便折腾,只要给我一个"下一个是什么"的方法,遍历这件事就与你无关了。
3.2 一个手写迭代器的完整例子
假设你在维护一个"页面列表"对象,内部用数组存储当前页的数据,但同时保留一个"上一页/下一页"的指针。你希望外部可以像遍历普通 List 一样去遍历它,但完全不用知道内部维护了两个数组和三个指针。
定义一个迭代器接口:
java复制public interface Iterator<T> {
boolean hasNext();
T next();
}
再定义聚合接口:
java复制public interface Aggregate<T> {
Iterator<T> createIterator();
}
然后是具体的聚合类,内部结构随便设计:
java复制public class PageList<T> implements Aggregate<T> {
private T[] items;
private int size;
private int currentPage;
private int pageSize;
public PageList(T[] items, int pageSize) {
this.items = items;
this.size = items.length;
this.currentPage = 0;
this.pageSize = pageSize;
}
@Override
public Iterator<T> createIterator() {
return new PageIterator();
}
private class PageIterator implements Iterator<T> {
private int cursor = 0;
@Override
public boolean hasNext() {
return cursor < size;
}
@Override
public T next() {
return items[cursor++];
}
}
}
这里最关键的是:PageList 的 createIterator() 返回了一个 PageIterator,而这个迭代器是一个内部类,它可以直接访问 PageList 的私有字段 items 和 size,但外部完全拿不到这些字段。这就是迭代器模式的核心价值——访问权限的隔离。
3.3 现代语言里的迭代器已经是"语法糖"级别的存在
写 Java 的人可能会觉得,上面的代码好麻烦,Java 的 ArrayList 不是已经提供了 iterator() 方法吗?确实,但正因为 JDK 替你写好了,你在真正需要自定义遍历时反而不知道怎么写。
Python 和 JavaScript 更进一步,直接把这个模式融入了语法层。Python 里你只需要实现 __iter__ 和 __next__ 方法:
python复制class PageList:
def __init__(self, items):
self.items = items
def __iter__(self):
self.index = 0
return self
def __next__(self):
if self.index < len(self.items):
result = self.items[self.index]
self.index += 1
return result
raise StopIteration
之后 for item in page_list 就能直接遍历。这个 __iter__ 的协议就是迭代器模式的语言级实现。JavaScript 则定义了 Symbol.iterator 协议,你能在对象上挂一个自定义迭代器,让 for...of 和展开运算符 ... 都支持你的自定义结构。
所以你看,迭代器模式并不是一个"要不要用"的问题,而是现代编程语言里已经深度内置的一种基础设施。你只要在写自定义集合类,就一定绕不开它。
4. 两个模式的全面对比:虽然都是行为型,但差在骨子里
解释器模式和迭代器模式,表面上都是行为型设计模式,但它们的目标、结构、复杂度变化方向和应用场景几乎完全不同。下面这张表能把差异看得比较清楚:
| 维度 | 解释器模式 | 迭代器模式 |
|---|---|---|
| 核心问题 | 定义一门语言,并解释执行它的句子 | 在不暴露内部结构的前提下,顺序访问聚合元素 |
| 关注对象 | 语法规则、表达式、层级结构 | 遍历方式、游标状态、访问协议 |
| 数据结构 | 语法树(树形结构) | 聚合对象(数组、链表、树、图等) |
| 复杂度变化 | 随语法规则数量线性甚至指数增长 | 相对稳定,与容器结构复杂度相关 |
| 典型实现 | 表达式节点类 + Parser | Iterator 接口 + 具体容器类的内部类 |
| 使用频率 | 低(规则引擎、模板解析等特定场景) | 极高(几乎所有集合类) |
| 是否被语言内置 | 基本没有,需自行实现 | 太多语言内置为语法级协议 |
光看表格还不够,我想把最核心的"行为型"这个共同点展开说说。在 GoF 的划分里,行为型模式关注的是"对象之间的职责分配"和"算法与对象的耦合关系"。解释器模式的职责分配是:把"解释"这个责任拆分到每一个表达式节点上,每个节点知道自己该怎么解释自己,父节点递归调用子节点的解释方法。迭代器模式的职责分配是:把"遍历"这个责任从聚合类中剥离出来,单独放到迭代器类中,聚合类只负责提供迭代器实例。
这俩都是在做"职责分配",但分配出去的职责完全不同。一个分配出去的是"对语言的解释能力",一个分配出去的是"对元素的访问能力"。
再补一个结构层面的差异:解释器模式有明确的层级递归特征——一个非终结符表达式内部持有多个子表达式;迭代器模式则是平铺的线性推进特征——迭代器内部只维护一个游标,一步步往前走,没有递归也没有层级。
这个差异还有一个很实际的表现:解释器模式的测试通常比迭代器模式麻烦得多。迭代器的测试就是喂几个元素、走一遍、断言遍历结果;解释器的测试需要你为每条语法规则写用例,还要测试规则的嵌套组合,组合数是爆炸的。我见过很多解释器实现,单测覆盖率看着挺高,但一到嵌套括号的 case 就挂——这就是层级递归结构带来的测试复杂度。
5. 容易混淆的几个点:命名相似和"语法糖"错觉
5.1 "Interpreter"和"Iterator"的名字陷阱
两个模式英文名真的很像:Interpreter vs. Iterator,都是 I 开头,都是 -er 结尾,听起来都像"做某件事的人"。但一个词根是 interpret(解释),一个词根是 iterate(迭代)。这不仅是拼写问题,更是理解上的陷阱。
我见过有人在简历里写"熟练运用解释器模式优化了列表遍历性能"——这句话本身就是矛盾的。优化列表遍历性能,跟解释器模式没关系;如果你真想优化遍历,你关注的是迭代器模式的变体,比如"懒加载迭代""流式迭代""并行迭代器"。反过来说,如果你在处理"把一种文本格式解析成结构化数据"的问题,那才是解释器模式的领域。
一个简单的判断口诀:如果问题是"怎么读懂一句话",考虑解释器模式;如果问题是"怎么一个一个地访问一堆东西",考虑迭代器模式。
5.2 把 HashMap 的遍历和解释器混为一谈
还有一种常见的混淆发生在实际业务代码里。比如有人用 Map 存储了一套规则配置,然后遍历 Map 去匹配输入内容,就觉得自己"用了解释器模式"。
不对。用 Map 存规则、遍历 Map 去匹配,本质上是"查表",跟解释器模式差别很大。解释器模式的核心特征是递归组合的语法树和非终结符节点的递归调用。如果代码里没有形成树状层级结构,没有出现"某个表达式节点内部持有其他表达式节点"这种组合关系,那就不是在用解释器模式。
至于遍历 Map 这个过程,你用到的恰恰是迭代器模式(HashMap 的 entrySet().iterator())。所以一个典型的业务代码里可能同时涉及两种模式:外层用迭代器遍历规则集合,内层用解释器解析每一条规则。它们是协作关系,不是替代关系。
5.3 "复合模式"场景:两个模式如何共存
说到协作,有必要展示一个真实项目中常见的组合场景。假设你在做一个订单筛选系统,系统里有多条筛选规则,每条规则是一个表达式,比如 amount > 500 AND status = paid。你需要遍历所有规则,对每一条规则调用解释器的解析方法:
java复制List<Expression> rules = loadRules(); // 从配置加载规则并解析成表达式树
// 迭代器模式:遍历规则列表
for (Expression rule : rules) {
// 解释器模式:解释并执行单条规则
boolean matched = rule.interpret(orderContext);
if (matched) {
handleMatch(order);
}
}
这里的 for 循环是迭代器模式的语法糖形式,rule.interpret(orderContext) 是解释器模式的核心调用。两个模式各司其职:迭代器管"走到下一条规则",解释器管"这条规则的含义是什么"。这是两种模式在实际系统中共同存在的典型形态。
6. 如何选型:几个能落地的判断标准
6.1 直接判断:你手头的问题属于哪一类
选型这件事,本质上不是"我懂不懂这个模式",而是"我能准确识别出问题的类型"。
拿几个实际问题来练手:
| 实际问题 | 问题类型 | 该用哪个模式 |
|---|---|---|
运营要自己输入筛选条件,如 age>18 AND city=北京 |
需要定义并解析一个小语言 | 解释器模式 |
| 自己写了一个二叉树结构,外部要遍历所有节点 | 需要访问容器元素但不暴露内部结构 | 迭代器模式 |
需要把邮件模板中的 {{user.name}} 替换为实际值 |
解析自定义模板语法 | 解释器模式 |
| 自定义分页列表,外部想用 for-each 遍历 | 需要为自定义集合提供遍历能力 | 迭代器模式 |
| 一个配置文件有多层嵌套结构,需要递归展开 | 解析层级化配置 | 解释器模式(如果是自定义语法) |
| 一个队列在不同线程间传递数据,消费者要逐个处理 | 顺序访问元素 | 迭代器模式 |
可以看到,问题里出现"语法、解析、表达式、规则组合、模板"这些关键词,优先考虑解释器模式;出现"遍历、集合、容器、游标、逐个访问"这些关键词,优先考虑迭代器模式。
6.2 当迭代器不够用的时候:考虑语言自带的 Stream / Generator
现实中很多人会问:"既然迭代器模式已经被语言内置了,我还需要自己写吗?"答案是:大多数情况下不需要。你直接用 for-each、Iterator、Generator、Stream 就行。但有一种情况必须手写迭代器:你的遍历逻辑不是简单的线性扫描,而是有状态、有跳过、有回溯的。
比如你在遍历一棵树结构,想实现"深度优先遍历一次,跳过所有叶子节点为空的子树",这时候用语言内置的简单迭代器就不够。你需要单独写一个 DFSIterator,把栈结构放在迭代器内部,每次 next() 时往栈里压入新节点。这正是迭代器模式在设计层面的价值——把复杂的遍历算法封装成一个有着统一 next/hasNext 协议的类,调用方无需关心遍历过程的状态管理。
6.3 当解释器过于笨重的时候:考虑现成的规则引擎
前文提到过,解释器模式最大的缺点就是实现和测试成本高。所以在选型时要评估一下你的规则复杂度。这里我给出一个比较实操的判断标准:
3 条以内简单规则(只有等于、不等于):别用解释器模式,直接写策略模式或者几个 if-else 就行。解释器模式的抽象成本反而会拖累你。
3 到 20 条规则,包含 AND/OR/NOT 组合和比较运算:解释器模式的舒适区。语法数量刚好,类的数量可控,每一条规则都能单独测试。
20 条以上规则,或需要支持函数调用、变量作用域、复杂运算符优先级:别自己写,直接用成熟的表达式引擎或语法分析器生成工具。自己写的解释器在这种复杂度下,不仅开发周期长,而且容易在边界情况上翻车。
这个判断标准是我在实际踩坑中总结出来的。之前在某个项目里,一开始规则只有 5 条,手写解释器很爽,后来业务扩张到 40 多条规则,还加了函数调用,维护成本直接翻了好几倍,最后不得不推翻重构成用现成的表达式引擎。早知如此,当初直接引入外部引擎会省很多事。
6.4 边界情况:当"遍历"本身也涉及"语义解释"
最后讲一个有意思的边界情况。有些遍历场景,表面上是在访问元素,但访问的顺序本身是由"规则"决定的——这时候两个模式就开始有重叠了。
比如你要遍历一个图结构,遍历规则是"按节点权重从大到小依次访问"。你可以把"权重比较"写进迭代器里(优先队列实现),也可以把"按权重排序"理解成一种小小的领域规则。但严格来说,这仍然是迭代器的职责——**迭代器负责定义访问顺序,解释器负责定义谓词语义,两者关注的点不同。**所以哪怕功能上有一点点重叠,选型判断依然清晰:想改遍历顺序就找迭代器,想改匹配条件就找解释器。
7. 一份送给实践者的经验清单
说实话,设计模式学到后面,你会发现最难的不是"记住 23 个模式",而是在代码里识别出真实存在的问题类型。解释器模式和迭代器模式恰好是很典型的例子——一个名字相似、看起来都在"处理东西",但实际问题的本质完全不同。
根据我这些年写代码和做代码评审的经验,给你一份可以直接拿去用的实践清单:
- 接到需求先问自己:这是"读懂一种格式"的问题,还是"走完一组元素"的问题?答案直接指向两个模式。
- 如果需要定义语言、解析表达式、处理嵌套结构,那就是解释器模式的领地,但在动手前先评估规则数量,规则一多就要考虑外部引擎。
- 如果只是遍历一个自定义集合,先看语言有没有内置的迭代协议(Python 的
__iter__/__next__、Java 的Iterable、JS 的Symbol.iterator)。有就直接实现协议接口,没有才需要自己定义迭代器类。 - 迭代器内部可以持有复杂状态(栈、队列、游标、快照),这正是它的封装价值所在;不要为了"省事"把内部结构直接暴露出去。
- 解释器模式的每一个表达式节点都应该是可独立测试的,碰到嵌套组合的 case,一定要专门写边界测试,不能只在 happy path 上跑一遍。
- 代码评审时看到"循环 + 分支匹配",先分辨一下:这是查表、是策略、还是真正的语法树解析?不要看到一个 switch 就说这是解释器模式。
我个人实际使用中的体会是:迭代器模式像空气一样无处不在,你写每一段 for 循环都在不觉间依赖它;解释器模式则像一把手术刀,很少出鞘,但只要需要处理自定义语法,它的价值立刻体现出来。两个都是行为型设计模式,但一个管"按顺序走",一个管"按语义跳",它们在各自的战场上都是举足轻重的存在。
