15. 解释器模式(Interpreter):数学表达式计算器
解释器模式,说实话,在23种设计模式里属于那种「用得不多,但用对了就很爽」的类型。它解决的核心问题是:如何设计一个能解析并执行某种「语言」的引擎。
我最早接触这个模式,是在做一个报表系统的时候。业务方要求支持自定义公式,比如 SUM(A1:B10) * 0.8 + 100。当时第一反应是「用正则硬拆」,结果越写越崩溃。后来翻到GoF的书,才发现解释器模式就是干这个的。
什么是解释器模式?
官方定义是:给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
说白了,就是三步:
- 定义文法:把「语言」的规则抽象出来
- 构建抽象语法树:把输入的表达式变成树形结构
- 遍历执行:从叶子节点往上算,得到最终结果
你想想看,SQL解析器、正则表达式引擎,底层都是这个套路。只不过它们更复杂,而我们今天只做一个简单的数学公式计算器。
核心角色
| 角色 | 职责 | 类比 |
|---|---|---|
| AbstractExpression(抽象表达式) | 声明一个 interpret 方法 | 所有节点的基类 |
| TerminalExpression(终结符表达式) | 解释叶子节点(数字) | 数字 3、5 |
| NonterminalExpression(非终结符表达式) | 解释组合节点(运算符) | +、-、*、/ |
| Context(上下文) | 存储全局信息(比如变量值) | 计算时的环境 |
| Client(客户端) | 构建语法树并调用 interpret | 我们写的测试代码 |
一个真实的例子:数学表达式计算器
假设我们要解析 "3 + 5 * 2"。按照四则运算规则,先乘后加,结果应该是 13。
如果用解释器模式,我们会先把它解析成一棵语法树:
(+)
/ \
(3) (*)
/ \
(5) (2)
然后从叶子节点开始,递归计算。3 就是 3,5 就是 5,2 就是 2。然后 5 * 2 = 10,最后 3 + 10 = 13。
嗯,这里要注意:语法树的构建顺序决定了运算优先级。乘除必须比加减先「挂」到树上。
代码实现
先定义抽象表达式接口:
// 抽象表达式
public interface Expression {
int interpret();
}
然后是数字节点(终结符):
// 终结符表达式:数字
public class NumberExpression implements Expression {
private int number;
public NumberExpression(int number) {
this.number = number;
}
@Override
public int interpret() {
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() {
return left.interpret() + right.interpret();
}
}
// 非终结符表达式:乘法
public class MultiplyExpression implements Expression {
private Expression left;
private Expression right;
public MultiplyExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret() {
return left.interpret() * right.interpret();
}
}
最后是客户端,负责解析并构建语法树:
public class Client {
public static void main(String[] args) {
// 手动构建 "3 + 5 * 2" 的语法树
Expression three = new NumberExpression(3);
Expression five = new NumberExpression(5);
Expression two = new NumberExpression(2);
Expression multiply = new MultiplyExpression(five, two);
Expression add = new AddExpression(three, multiply);
int result = add.interpret();
System.out.println("3 + 5 * 2 = " + result); // 输出 13
}
}
你看,代码很清晰。每个节点只关心自己的计算,组合起来就能处理复杂的表达式。
SVG 结构图:解释器模式核心流程
避坑指南
我曾经在一个项目中,直接用解释器模式去解析复杂的业务规则。结果语法树越建越深,性能直接崩了。后来才意识到:解释器模式不适合处理超大规模的文法。
几个关键点:
- 文法不要太复杂:如果规则超过几十条,建议用专门的解析器生成器(比如 ANTLR)
- 注意递归深度:Java 默认栈深度有限,超长表达式可能 StackOverflow
- 性能敏感场景慎用:每个节点都要创建对象,GC 压力不小
核心总结
解释器模式的核心思想就一句话:把「语言」变成「树」,然后递归算。
它最适合的场景是:
✅ 文法简单且稳定
✅ 执行效率不是第一优先级
✅ 需要频繁修改或扩展规则
个人小技巧
我习惯在写解释器之前,先用「中缀转后缀」算法(调度场算法)把表达式转成逆波兰式。这样构建语法树会简单很多,而且天然支持优先级。
注意
不要试图用解释器模式去解析完整的 SQL 或编程语言。那工作量太大了,直接用现成的解析库更靠谱。解释器模式最适合的是「领域特定语言(DSL)」,比如公式、规则、配置表达式。
好了,关于解释器模式就聊这么多。记住它的核心:文法 + 语法树 + 递归解释。下次遇到需要解析自定义表达式的时候,不妨试试这个模式。
公众号:蓝海资料掘金营,微信deep3321