一、解释器模式:语法解析、表达式求值
解释器模式,说实话,在23种经典设计模式里属于「用得少但很巧妙」的那一类。我早年刚接触时也觉得它有点玄乎——不就是写个解析器吗?后来在项目中真正用了一回,才体会到它的设计精髓。
说白了,解释器模式就是给某种「语言」定义一套语法规则,然后用代码去解释执行它。你想想看,我们平时写的正则表达式、SQL语句、数学表达式,背后都有解释器在干活。
1. 模式定义与核心思想
解释器模式的定义是这样的:给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
嗯,这句话有点绕。我换个说法:
- 你定义一套「语法规则」
- 然后写一个「解释器」去解析符合这些规则的表达式
- 最终得到你想要的结果
它的核心结构包含这几个角色:
| 角色 | 职责 | 举个栗子 |
|---|---|---|
| 抽象表达式(AbstractExpression) | 定义解释操作的接口 | 所有节点都要实现 interpret() |
| 终结符表达式(TerminalExpression) | 语法中的最小单元,不可再分 | 数字、变量 |
| 非终结符表达式(NonterminalExpression) | 由多个子表达式组合而成 | 加法、减法、乘法 |
| 上下文(Context) | 存放全局信息,比如变量值 | Map<String, Integer> |
核心要点:解释器模式把「语法规则」映射成「类层次结构」。每个语法规则对应一个类,解释过程就是递归遍历这棵树。
2. 一个真实的例子:四则运算表达式求值
我记得有一次,项目里需要让用户自定义一些计算规则,比如「(price * quantity + shipping) * taxRate」。如果硬编码,每次改规则都要发版,太痛苦了。于是我用解释器模式写了个简单的表达式引擎。
先看抽象表达式接口:
public interface Expression {
int interpret(Context ctx);
}
终结符表达式——数字:
public class NumberExpression implements Expression {
private int number;
public NumberExpression(int number) {
this.number = number;
}
@Override
public int interpret(Context ctx) {
return number;
}
}
非终结符表达式——加法:
public class AddExpression implements Expression {
private Expression left;
private Expression right;
public AddExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret(Context ctx) {
return left.interpret(ctx) + right.interpret(ctx);
}
}
减法、乘法、除法也是类似的写法,我就不全贴了。关键是看这个结构——每个运算符都是一个类,它们组合成一棵语法树。
上下文类,用来存放变量值:
public class Context {
private Map<String, Integer> variables = new HashMap<>();
public void setVariable(String name, int value) {
variables.put(name, value);
}
public int getVariable(String name) {
return variables.getOrDefault(name, 0);
}
}
好了,现在我们来解析一个表达式「3 + 5 - 2」:
// 手动构建语法树
Expression three = new NumberExpression(3);
Expression five = new NumberExpression(5);
Expression two = new NumberExpression(2);
Expression add = new AddExpression(three, five);
Expression sub = new SubtractExpression(add, two);
Context ctx = new Context();
int result = sub.interpret(ctx);
System.out.println(result); // 输出 6
你可能会说:「手动建树也太麻烦了吧?」没错,实际项目中我们不会手写这棵树,而是用词法分析器+语法分析器自动生成。但模式本身只关心「如何解释」,不关心「如何解析」。
3. 用SVG画一下核心结构
下面这张图展示了「3 + 5 - 2」这个表达式对应的语法树结构:
解释过程就是后序遍历这棵树:先算3+5=8,再算8-2=6。每个节点只关心自己的子节点返回什么值,不关心子节点内部怎么算的——这就是组合模式+递归的魅力。
4. 我在项目中踩过的坑
我曾经在一个规则引擎项目里用过解释器模式,当时犯了个低级错误——把上下文设计成了全局可变状态。结果多个表达式并发解释时,变量值互相覆盖,查了半天才发现。
避坑指南:
- 上下文对象尽量设计成不可变的,或者每个解释器实例持有自己的上下文副本
- 如果表达式树很大,递归深度可能引发栈溢出,考虑用迭代代替递归
- 终结符和非终结符的区分要清晰,别把能拆分的节点设计成终结符
还有一个容易忽略的点:性能问题。解释器模式本质上是运行时解释,效率肯定不如编译执行。如果你要解析的表达式数量巨大(比如每秒几万次),建议提前把表达式编译成某种中间表示,或者直接用现成的表达式引擎(比如Aviator、QLExpress)。
5. 什么时候该用?什么时候不该用?
我个人习惯是:当语法规则简单且稳定,但需要频繁扩展时,考虑解释器模式。比如:
- 简单的数学表达式计算
- 自定义的脚本或规则语言
- SQL解析、正则表达式引擎(虽然这些通常用更专业的工具)
但如果语法规则特别复杂(比如完整的编程语言),或者性能要求极高,就别自己造轮子了。用ANTLR、JavaCC这类解析器生成器,或者直接用现成的脚本引擎,它不香吗?
我的建议:解释器模式最适合「语法规则数量有限且变化不频繁」的场景。如果规则经常变,每次都要新增类,维护成本会直线上升。这时候可以考虑用访问者模式来解耦,或者干脆用策略模式+配置化。
6. 和类似模式的对比
| 模式 | 核心区别 |
|---|---|
| 解释器模式 | 关注「语法规则」的解释执行,每个规则一个类 |
| 策略模式 | 关注「算法」的替换,每个算法一个类 |
| 组合模式 | 关注「部分-整体」的层次结构,树形管理 |
| 访问者模式 | 关注「在不改变元素类的前提下增加操作」 |
其实解释器模式和组合模式经常一起出现——语法树本身就是一种组合结构。而访问者模式可以用来遍历语法树,在不修改表达式类的前提下增加新的操作(比如打印、优化、类型检查)。
好了,关于解释器模式就聊这么多。它不像单例、工厂那样天天用,但一旦遇到合适的场景,用起来是真的顺手。下次你遇到需要解析自定义语法的需求时,不妨想想这个模式。
公众号:蓝海资料掘金营,微信deep3321