走进 Gang of Four 设计模式:解释器模式
说明:不仅告诉你"怎么用",更告诉你"为什么这样设计"
前置知识:Java 面向对象基础(接口、多态、递归思想)、组合模式基本概念
📊 设计模式分类总览
GoF 23 种设计模式可按两个维度交叉分类:类/对象 (处理方式) × 创建型/结构型/行为型(目的)。
| 维度 | 创建型(Creational) | 结构型(Structural) | 行为型(Behavioral) |
|---|---|---|---|
| 类(Class) (通过继承复用) | Factory Method 工厂方法 | Adapter(类适配器) | Interpreter(解释器) Template Method(模板方法) |
| 对象(Object) (通过组合/聚合复用) | Abstract Factory(抽象工厂) Builder(建造者) Prototype(原型) Singleton(单例) | Adapter(对象适配器) Bridge(桥接) Composite(组合) Decorator(装饰) Facade(外观) Flyweight(享元) Proxy(代理) | Chain of Resp.(责任链) Command(命令) Iterator(迭代器) Mediator(中介者) Memento(备忘录) Observer(观察者) State(状态) Strategy(策略) Visitor(访问者) |
📑 目录
- [模式概述 · 15 问深度分析](#模式概述 · 15 问深度分析)
- 框架源码实战分析
- [JDK 正则表达式 Pattern.Node](#JDK 正则表达式 Pattern.Node)
- [Spring SpEL 表达式引擎](#Spring SpEL 表达式引擎)
- [MyBatis 动态 SQL SqlNode](#MyBatis 动态 SQL SqlNode)
- [深度追问:Why / How / Trade-off / Evolution / Modern Practice](#深度追问:Why / How / Trade-off / Evolution / Modern Practice)
- 总结
- 参考文献
1. 模式概述 · 15 问深度分析
Q1: 为什么需要这个模式?它解决了什么问题?
解释器模式是行为型设计模式中相对冷门、但特定场景下威力巨大的一种模式。它本质上是为一种特定语言构建编译器/解释器的面向对象实现。
在软件开发中,有时我们会遇到一类重复发生的问题,这些问题可以表达为一个特定的规则、表达式或命令序列。例如:自研的规则引擎、复杂的财务公式计算、自定义的 SQL 解析器。
它解决的核心问题 :提供了一种将领域内的特定语法规则映射为面向对象类结构的方法。通过这种方式,我们可以通过组合不同的类对象,来动态地表达和执行无穷无尽的语法逻辑,而无需修改底层解析代码。
Q2: 如果不用这个模式,会有什么缺陷?
如果不使用解释器模式,通常的做法是编写一个巨大的解析函数(往往充斥着 if-else、switch-case 以及正则匹配):
java
// ❌ 大量 if-else 硬编码,新增运算符需要改源码
public int eval(String expr, Map<String, Integer> ctx) {
if (expr.contains("+")) {
String[] parts = expr.split("\\+");
return eval(parts[0], ctx) + eval(parts[1], ctx);
} else if (expr.contains("-")) {
// ...
}
// 新增乘法运算符需要在这里加 else-if
}
- 违反开闭原则:一旦需要增加一种新的语法规则或运算符,必须深入到那个庞大的解析函数内部修改代码。
- 难以扩展与维护:随着语法越来越复杂,条件分支会呈指数级嵌套。
- 无法动态组合:硬编码的逻辑在编译期就确定了,无法在运行时由用户自由组合出新的复杂表达式。
Q3: 核心思想是什么?一句话如何概括?
- 核心思想 :将语言的语法规则定义为类层次结构 ,通过将这些类的实例组合成一棵抽象语法树(AST),并让这棵树的每个节点自行负责其对应语法规则的评估与执行。
- 一句话概括 :用"对象树"来表达语法,用"树遍历"来实现执行。
Q4: 包含哪些角色?每个角色的职责是什么?
| 角色 | 职责 |
|---|---|
| AbstractExpression(抽象表达式) | 定义一个抽象的解释操作接口(通常是 interpret(Context ctx))。 |
| TerminalExpression(终结符表达式) | 语法树的叶子节点,不再包含其他表达式(如变量、数字、常量)。 |
| NonterminalExpression(非终结符表达式) | 语法树的分支节点 ,包含一个或多个子表达式(如加、减法),通过递归调用子表达式的 interpret 组合结果。 |
| Context(上下文) | 包含解释器之外的全局信息,存储变量名与值的映射关系。 |
Q5: 它们之间如何协作?调用流程是怎样的?
构建期(编译期): 客户端或解析器接收字符串表达式(如 a + b - 3),根据语法规则将其解析并组装成一棵抽象语法树。根节点通常是非终结符,叶子节点是终结符。
执行期(解释期):
- 创建
Context(填充变量a=10, b=5)。 - 调用 AST 根节点的
interpret(Context)方法。 - 根节点(非终结符)递归调用子节点的
interpret方法。 - 叶子节点(终结符)从 Context 中获取具体数值并返回。
- 结果一路向上回溯,根节点计算最终结果。
Q6: 为什么要这样设计?每个角色存在的意义是什么?
- AbstractExpression:提供统一契约,使得解释器可以使用多态遍历整棵树。
- TerminalExpression 和 NonterminalExpression 的分离:利用组合模式,终结符负责算值终点,非终结符负责流程控制与路由。
- Context :保持表达式对象的无状态性,同一个语法树可以在不同上下文数据下重复使用。
Q7: 为什么使用接口、抽象类、组合,而不是其他方式?
- 使用接口/抽象类 :为了利用多态 。语法树的节点类型千差万别,只有通过抽象出统一的
Expression接口,非终结符节点才能以统一的方式去持有和调用子节点。 - 使用组合(Composite) :因为语法天然具有嵌套和层次结构 。例如
(a + b) * c,"乘法"非终结符组合了"加法"和"变量"。组合模式是表达这种"部分-整体"层级结构最完美的手段。
Q8: 这种设计符合哪些面向对象原则?
- 开闭原则(OCP) :增加新运算符只需新增一个类实现
Expression接口,不需修改现有的类。 - 单一职责原则(SRP):每个表达式类只负责自己那一部分语法的解释逻辑。
- 依赖倒置原则(DIP) :非终结符节点持有的是抽象的
Expression接口,而不是具体的子类。
Q9: 它有哪些优点和局限性?
优点:
- 易于扩展文法:增加新规则只需加类。
- 实现文法较容易:每个类小而直观,易于编写和单元测试。
局限性与额外成本:
- 类爆炸:如果文法极其复杂,每一个小语法都需要一个类。
- 执行效率低下:大量递归调用和对象组合,高频时性能差。
- 未解决解析问题:模式本身不包含如何把字符串变成语法树的代码,仍需自己写 Lexer 和 Parser。
Q10: 最适合/不该用的场景
最适合:
- 领域特定语言(DSL):如简单的自定义工作流规则引擎、财务公式配置。
- 数据验证器 :需要动态组合各种条件(如
Age > 18 AND Region == 'CN')。 - 简单的命令/机器人指令解析。
不应该使用:
- 复杂的、通用的高级语言:会导致类爆炸且效率低下,应使用 ANTLR、JavaCC 等专业工具链。
- 对性能要求极高的场景。
Q11: 与其他模式的区别
与组合模式(Composite)的联系:
- 解释器模式的语法树结构本质上就是组合模式。区别在于意图:组合模式专注于数据结构管理,解释器模式专注于利用这棵树去解释执行。
与策略模式(Strategy)的区别:
- 策略模式是互替的(选一个执行);解释器模式中的各类表达式是组合在一起共同工作的。
与访问者模式(Visitor)的配合:
- 如果执行逻辑经常变动,通常引入访问者模式将具体的
解释执行逻辑从 Expression 类中剥离到 Visitor 中,实现语法结构与求值逻辑的进一步解耦。
Q12: 框架中有哪些典型应用?
- JDK :
java.util.regex.Pattern,正则字符串被解析为内部Node节点链/树,通过匹配引擎解释执行。 - Spring Framework :SpEL(Spring Expression Language) ,形如
#{@bean.property ?: 'default'}的表达式被解析为SpelNode树解释求值。 - MyBatis :动态 SQL 解析 ,
<if>、<where>等标签被解析为SqlNode树,在运行时动态组装 SQL。
Q13: 最小可运行版本(数学加减法解释器)
java
import java.util.HashMap;
import java.util.Map;
// 1. AbstractExpression
interface Expression {
int interpret(Context context);
}
// Context
class Context {
private final Map<String, Integer> valueMap = new HashMap<>();
public void assign(String variable, int value) { valueMap.put(variable, value); }
public int getValue(String variable) { return valueMap.getOrDefault(variable, 0); }
}
// 2. TerminalExpression:变量
class VariableExpression implements Expression {
private final String name;
public VariableExpression(String name) { this.name = name; }
@Override
public int interpret(Context context) { return context.getValue(name); }
}
// 2. TerminalExpression:常数
class NumberExpression implements Expression {
private final int number;
public NumberExpression(int number) { this.number = number; }
@Override
public int interpret(Context context) { return this.number; }
}
// 3. NonterminalExpression:加法
class AddExpression implements Expression {
private final Expression left, right;
public AddExpression(Expression left, Expression right) { this.left = left; this.right = right; }
@Override
public int interpret(Context context) {
return left.interpret(context) + right.interpret(context);
}
}
// 3. NonterminalExpression:减法
class SubExpression implements Expression {
private final Expression left, right;
public SubExpression(Expression left, Expression right) { this.left = left; this.right = right; }
@Override
public int interpret(Context context) {
return left.interpret(context) - right.interpret(context);
}
}
// 客户端
public class InterpreterDemo {
public static void main(String[] args) {
// AST: (a + b) - 3
Expression ast = new SubExpression(
new AddExpression(new VariableExpression("a"), new VariableExpression("b")),
new NumberExpression(3)
);
Context context = new Context();
context.assign("a", 10);
context.assign("b", 5);
System.out.println("Result: " + ast.interpret(context)); // 12
}
}
Q14: 识别重构信号
- 配置驱动的复杂嵌套逻辑 :数据库里存大量规则字符串(如
if A and (B or C)),代码里有复杂的解析机制。 - 频繁变化的计算公式:业务人员经常要求修改或组合计算公式(如优惠券叠加规则)。
- 大面积的条件分支解析:入参是复杂的 JSON 或 String,方法内部通过几十层循环或 if 块逐句判断。
Q15: 现代改进(Java 16+ sealed + record + 模式匹配)
java
import java.util.Map;
@FunctionalInterface
sealed interface ModernExpression permits Variable, Constant, Op {
int interpret(Map<String, Integer> ctx);
}
record Variable(String name) implements ModernExpression {
public int interpret(Map<String, Integer> ctx) { return ctx.getOrDefault(name, 0); }
}
record Constant(int value) implements ModernExpression {
public int interpret(Map<String, Integer> ctx) { return value; }
}
record Op(ModernExpression left, ModernExpression right, char symbol) implements ModernExpression {
public int interpret(Map<String, Integer> ctx) {
return switch (symbol) {
case '+' -> left.interpret(ctx) + right.interpret(ctx);
case '-' -> left.interpret(ctx) - right.interpret(ctx);
default -> throw new IllegalArgumentException("Unknown op: " + symbol);
};
}
}
2. 框架源码实战分析
2.1 JDK 正则表达式 java.util.regex.Pattern.Node
JDK 的正则引擎将正则字符串解析为由 Node 节点组成的链表/树状结构,运行时通过从头节点开始"解释执行"这些节点来完成匹配。
角色映射
| 角色 | JDK 正则对应 |
|---|---|
| AbstractExpression | Pattern$Node(内部静态抽象类) |
| TerminalExpression | Slice(匹配固定字符串)、Ctype(匹配预定义字符集 \d) |
| NonterminalExpression | Start(起始匹配)、Loop(处理 */+ 闭包)、Branch(处理 ` |
| Context | Matcher(保存当前匹配的输入字符串、指针位置、捕获组状态) |
核心源码
java
// Pattern 内部静态类,充当 AbstractExpression
static class Node extends Object {
Node next; // 链表指针,用于组合下一个表达式节点
Node() { next = Pattern.accept; }
// 核心解释执行方法
boolean match(Matcher matcher, int i, CharSequence seq) {
return i == matcher.to && next == null;
}
}
// 终结符节点:Ctype(字符类型匹配,如 \d)
static final class Ctype extends Node {
final int ctype;
Ctype(int type) { this.ctype = type; }
@Override
boolean match(Matcher matcher, int i, CharSequence seq) {
if (i < matcher.to && isType(seq.codePointAt(i), ctype)) {
return next.match(matcher, i + countChars(seq, i, 1), seq);
}
return false; // 不匹配,回溯
}
}
// 非终结符节点:Curly(处理 {m,n} 重复量词)
static final class Curly extends Node {
Node atom; // 组合模式:被量词修饰的子表达式
int type, cmin, cmax;
Curly(Node node, int min, int max, int type) {
this.atom = node;
this.cmin = min;
this.cmax = max;
this.type = type;
}
@Override
boolean match(Matcher matcher, int i, CharSequence seq) {
// 递归调用 atom.match(),控制指针回溯
return java.util.regex.Pattern.Loop.match(matcher, i, seq, atom, cmin, cmax, next);
}
}
深度分析 :JDK 正则没有采用标准的二叉树 AST,而是由于正则的线性特性将其拉平为链表+局部循环分支树 (通过 next 指针和 atom 组合)。通过 Node.match() 的递归调用与回溯,完美实现了正则语法的解释执行。
2.2 Spring SpEL (Spring Expression Language)
SpEL 允许在运行时查询和操作对象图,是解释器模式的高级落地。
角色映射
| 角色 | Spring SpEL 对应 |
|---|---|
| AbstractExpression | SpelNode 接口 / SpelNodeImpl 抽象类 |
| TerminalExpression | Literal(字面量)、VariableReference(变量引用 #user) |
| NonterminalExpression | OpPlus(加法)、OpMatches(正则匹配)、Ternary(三元) |
| Context | EvaluationContext(存储变量、函数、Root 对象) |
核心源码
java
// SpelNode 接口
public interface SpelNode {
TypedValue getValue(ExpressionState state) throws EvaluationException;
int getChildCount();
SpelNode getChild(int index);
boolean isCompilable();
void generateCode(MethodVisitor mv, CodeFlow cf);
}
// 非终结符:OpPlus(加法)
public class OpPlus extends Operator {
public OpPlus(int startPos, int endPos, SpelNodeImpl... operands) {
super("+", startPos, endPos, operands);
}
@Override
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
Object leftOperand = getLeftOperand().getValueInternal(state).getValue();
Object rightOperand = getRightOperand().getValueInternal(state).getValue();
if (leftOperand instanceof Number && rightOperand instanceof Number) {
return new TypedValue(NumberUtils.add((Number) leftOperand, (Number) rightOperand));
}
if (leftOperand instanceof String || rightOperand instanceof String) {
return new TypedValue(String.valueOf(leftOperand) + rightOperand);
}
}
}
// 终结符:VariableReference(变量引用)
public class VariableReference extends SpelNodeImpl {
private final String name;
@Override
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
return state.lookupVariable(this.name); // 直接从 Context 中获取变量值
}
}
深度分析 :SpEL 是解释器模式的教科书级实现。Spring 自 4.1 之后引入了 generateCode 机制,当某个表达式被高频执行时,会利用 ASM 框架直接将 SpelNode 树编译为原生 Java 字节码,从"解释器模式"动态优化为"直接指令执行"。
2.3 MyBatis 动态 SQL SqlNode
MyBatis 的强大之处在于通过 XML 标签动态拼接 SQL。在启动加载 XML 时,标签被解析为一棵 SqlNode 树,在运行时通过传入的参数去解释执行这棵树。
角色映射
| 角色 | MyBatis 对应 |
|---|---|
| AbstractExpression | SqlNode 接口 |
| TerminalExpression | StaticTextSqlNode(纯 SQL 文本) |
| NonterminalExpression | IfSqlNode(<if>)、WhereSqlNode(<where>)、MixedSqlNode(组合节点) |
| Context | DynamicContext(持有参数 Map 和 StringBuilder 结果缓冲) |
核心源码
java
public interface SqlNode {
boolean apply(DynamicContext context);
}
// 非终结符:MixedSqlNode(容器节点)
public class MixedSqlNode implements SqlNode {
private final List<SqlNode> contents; // 组合多个子表达式节点
@Override
public boolean apply(DynamicContext context) {
for (SqlNode sqlNode : contents) {
sqlNode.apply(context); // 递归调用子节点
}
return true;
}
}
// 非终结符:IfSqlNode(条件判断)
public class IfSqlNode implements SqlNode {
private final ExpressionEvaluator evaluator;
private final String test; // "username != null"
private final SqlNode contents; // 标签内部的 SQL 片段
@Override
public boolean apply(DynamicContext context) {
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context); // 条件满足,拼接 SQL
return true;
}
return false; // 条件不满足,被过滤掉
}
}
// 终结符:StaticTextSqlNode(纯文本)
public class StaticTextSqlNode implements SqlNode {
private final String text;
@Override
public boolean apply(DynamicContext context) {
context.appendSql(text); // 直接追加到 StringBuilder 中
return true;
}
}
深度分析 :MyBatis 将动态 SQL 拼接变成了语法树的深度优先遍历(DFS) 。调用 Mapper 时,找根 SqlNode → 创建 DynamicContext → 调用 rootSqlNode.apply(context) → 树遍历完成后从 context.getSql() 取出最终拼好的 SQL。
三者对比总结:
| 框架 | AbstractExpression | TerminalExpression | NonterminalExpression | Context | 核心解释动作 |
|---|---|---|---|---|---|
| JDK Pattern | Pattern$Node |
Slice, Ctype |
Curly, Branch |
Matcher |
match(matcher, idx, seq) |
| Spring SpEL | SpelNode |
Literal, VariableReference |
OpPlus, Ternary |
ExpressionState |
getValue(state) |
| MyBatis | SqlNode |
StaticTextSqlNode |
IfSqlNode, MixedSqlNode |
DynamicContext |
apply(context) |
3. 深度追问
3.1 Why(为什么):解决哪个根本矛盾?
解释器模式解决的根本矛盾是:"业务规则的无限多变性"与"硬编码系统编译期确定性"之间的矛盾。
在常规开发中,逻辑是写死在类和方法中的。一旦规则改变,就必须修改代码、重新编译、部署。然而在某些特定领域,规则是由业务人员在运行时高频、随机组合 出来的。解释器模式通过引入一门 DSL,将变化的业务逻辑从不变的执行引擎中彻底剥离。系统不再硬编码业务逻辑,而是硬编码一个"解释器引擎"。其本质是用数据的组合(语法树)代替了代码的逻辑分支(if-else)。
3.2 How(怎么做):通过哪些对象关系和交互机制?
解释器模式的核心机制是将语法的"递归定义"直接映射为对象的"递归组合"。
- 对象关系:利用组合关系,非终结符节点内部持有其他抽象表达式的引用。这种"自己包含自己"的树状结构,完美对应了语法的嵌套。
- 交互机制 :调用根节点的
interpret(Context)触发整棵树的**深度优先搜索(DFS)**递归遍历。表达式节点是无状态的,所有的状态、变量映射都封装在Context中,确保同一棵语法树可以在不同数据集下复用。
3.3 Trade-off(代价):牺牲了什么换取什么?
| 换取(收益) | 牺牲(代价) |
|---|---|
| 极致的动态扩展性:拼装不同语法树就能组合出无限业务逻辑 | 性能下降:大量递归调用导致方法栈极深 |
| 极高的维护边界:每个语法节点职责单一,易测试 | 调试困难:Call Stack 极深,难以追踪 |
| 运行时动态组合 | 类爆炸:复杂文法需要大量类 |
| 前端解析成本:需额外编写词法/语法解析器 |
3.4 Evolution(演化):如何逐步演化而来?
- 硬编码 :一个巨大的
eval()方法嵌套无数switch-case。 - 策略模式组合:把加法、减法抽成策略类,但无法解决嵌套问题。
- 标准解释器模式:引入组合模式,形成标准 AST。
- 访问者模式变体:将语法树结构与求值行为彻底分离。
- 虚拟机/字节码变体:将树压平为线性指令集(Bytecode),由高效率循环执行(如 Lua 虚拟机、Spring SpEL 字节码生成)。
3.5 Modern Practice(现代实践):是否被取代?
大部分被专业工具取代: 现在通常使用 ANTLR、JavaCC 等工具,或 Drools、Groovy、Aviator 等开箱即用引擎。
但在现代技术栈中融合新生:
- 函数式编程 :
Expression接口变成Function<Context, Object>,非终结符变成高阶函数,直接消灭类爆炸。 - JIT 编译变体:Spring SpEL 在 4.1+ 引入运行时编译,高频表达式直接编译为原生字节码,性能提升百倍。
- 微服务 + 配置中心:规则文本从 Nacos/Apollo 动态下发,在服务内部通过解释器模式构建 AST,结合 IoC 无缝调用 Spring 容器中的基础服务。
结论:不需要刻意模仿 90 年代繁琐的类结构,但"用对象树/函数组合来抽象、分离多变文法"的核心思想,依然是指导现代动态规则引擎设计的不二法门。
4. 总结
核心要点
- 解释器模式将语言的文法映射为类结构,核心是组合模式 + 递归调用
- 适用范���有限:仅适用于文法简单的场景
- 三个工业级应用:JDK Pattern(正则链表 AST)、Spring SpEL(字节码编译 JIT)、MyBatis SqlNode(SQL 树状过滤)
- 与组合模式的关系:语法树结构本质上就是组合模式
- 现代演进:Java 16+
sealed+record+ 模式匹配、函数式 Combinator、JIT 编译
一句话
解释器模式就是"让程序读懂自定义语言"------定义一套语法规则,把"规则"映射为"对象树",让
interpret()沿着树递归执行。
5. 参考文献
1 GAMMA E, HELM R, JOHNSON R, et al. Design Patterns: Elements of Reusable Object-Oriented SoftwareM. Boston: Addison-Wesley, 1994.
2 Oracle. Java Pattern Source CodeEB/OL. https://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html, 2024.
3 SPRING. Spring Expression Language (SpEL)EB/OL. https://docs.spring.io/spring-framework/reference/core/expressions.html, 2024.
4 MyBatis. MyBatis 3 Dynamic SQLEB/OL. https://mybatis.org/mybatis-3/dynamic-sql.html, 2024.