手写一个表达式计算器,你就理解解释器模式了
写过计算器的都知道,解析一个数学表达式有多麻烦。加减乘除、括号优先级、变量替换......如果你用一堆 if-else 来硬刚,代码会膨胀到让你自己都看不懂。
解释器模式(Interpreter Pattern)就是干这个活的:给一种"语言"定义语法规则,然后用代码来解释执行它。
解释器模式到底解决什么问题
先说一个真实场景:你做了一个电商促销系统,运营想自己配促销规则,比如"满200减30,SKU 为 A001 的商品打8折,VIP 用户再减10"。你不可能每次运营改规则都去发版上线对吧?
你需要一种方式,让运营配置的规则能被系统直接"读懂"并执行。这就是解释器模式的核心------把业务规则变成一种小语言,然后写一个解释器去执行它。
再举几个你一定见过的例子:
- SQL 查询语句,数据库引擎就是 SQL 的解释器
- 正则表达式,regex 引擎在解释你写的模式串
- 模板引擎(FreeMarker、Thymeleaf),解释模板里的
${}占位符 - Spring 的 SpEL(
#{#user.age > 18}),在运行时解析表达式
这些本质上都是解释器模式的应用。
解释器模式的核心结构
解释器模式只有两个关键角色:
- AbstractExpression(抽象表达式) :定义统一的
interpret()方法 - TerminalExpression(终结符表达式):处理语法中最小的单位,比如数字、变量名
- NonTerminalExpression(非终结符表达式):处理语法中的组合规则,比如加法、减法
再加一个 Context(上下文),用来存运行时的变量和状态。
画个图就是:
Context(存变量)
|
AbstractExpression(interpret 方法)
├── TerminalExpression(数字/变量,直接返回值)
└── NonTerminalExpression(运算符,递归调用子表达式的 interpret)
动手写一个:四则运算解释器
光看概念太虚了,直接上代码。我们实现一个支持加减乘除和括号的表达式解释器。
先定义抽象表达式:
java
public interface Expression {
int interpret(Map<String, Integer> context);
}
然后是终结符表达式------处理数字和变量:
java
// 数字
public class NumberExpression implements Expression {
private final int number;
public NumberExpression(int number) {
this.number = number;
}
@Override
public int interpret(Map<String, Integer> context) {
return number;
}
}
// 变量
public class VariableExpression implements Expression {
private final String name;
public VariableExpression(String name) {
this.name = name;
}
@Override
public int interpret(Map<String, Integer> context) {
return context.getOrDefault(name, 0);
}
}
非终结符表达式------加减乘除:
java
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(Map<String, Integer> context) {
return left.interpret(context) + right.interpret(context);
}
}
减法、乘法、除法结构完全一样,只是把 + 换成对应的运算符,代码就不贴了。
把这几个组装起来,解析 "a + b * 3 - (c - 2)" 这样的表达式:
java
Map<String, Integer> vars = new HashMap<>();
vars.put("a", 10);
vars.put("b", 4);
vars.put("c", 5);
// 对应表达式:a + b * 3 - (c - 2)
Expression expr = new SubtractExpression(
new AddExpression(
new VariableExpression("a"),
new MultiplyExpression(
new VariableExpression("b"),
new NumberExpression(3)
)
),
new SubtractExpression(
new VariableExpression("c"),
new NumberExpression(2)
)
);
int result = expr.interpret(vars);
// 10 + 4*3 - (5-2) = 10 + 12 - 3 = 19
你看,解释器把表达式变成了一个语法树 。每个节点都是一个 Expression,interpret() 递归向下计算,最后得出结果。
实际踩过的坑
第一版代码我写的很兴奋,觉得解释器模式太优雅了。然后运营配了一条规则:价格 > 100 AND (分类 = 电子产品 OR 品牌 = 苹果)。
我一看傻了------我那套只支持四则运算的解释器完全不够用。要支持 AND、OR、比较运算符,还得处理字符串类型的变量......
这里就是解释器模式最大的痛点:语法规则越复杂,解释器的类就越膨胀。每加一种语法就要加一个类,维护成本直线上升。
后来我换了个思路:不再自己写解释器,直接用了 ANTLR 。它是一个强大的语法分析工具,你只需要写语法规则文件(.g4),它自动帮你生成解析器代码。而且它生成的解析器性能比自己手写的好得多。
但这不代表解释器模式就没用了。理解了解释器模式的原理,你才知道 ANTLR 这类工具在底层帮你做了什么。而且对于简单的场景(比如只支持 AND/OR 的条件表达式),手写解释器反而是更轻量的选择。
你什么时候该用解释器模式
用一个简单的判断标准:
- 你需要解析一种"小语言"(不是完整的编程语言)
- 语法规则相对简单,用编译原理那套 yacc/lex 太重了
- 业务规则频繁变化,你不想每次都改代码重新部署
典型的应用场景:
- 规则引擎:促销规则、风控规则、审批流程条件
- SQL/NoSQL 查询构建器:把对象查询条件转成 SQL
- 模板表达式 :解析
#{user.name}之类的占位符 - 配置表达式 :比如 Nginx 配置中
$variable的解析
什么时候别用
这个我要说句实话:解释器模式是 23 种设计模式里使用频率最低的之一。
原因很简单:大多数需要"解释"的场景,都已经有了成熟的工具。
- 要解析 SQL?用 MyBatis/Hibernate
- 要解析表达式?用 Spring EL / OGNL / SpEL
- 要做规则引擎?用 Drools / LiteFlow
- 要做模板渲染?用 FreeMarker / Thymeleaf
除非你做的产品本身就是一个 DSL(领域特定语言)编辑器,或者你的规则系统有特殊需求现有工具覆盖不了,否则手写解释器大概率是过度设计。
我在实际项目中只手写过一次解释器------是一个极其简单的条件过滤器,只支持 field op value 的格式(age > 18,status == active),连括号都没有。就这么简单的东西,用 ANTLR 都嫌重,手写反而是最优解。
一句话总结
解释器模式教会你的不是"怎么手写一个完整的语言解释器",而是让你理解语法树、递归解析、上下文传递这些概念。这些概念在你看 ANTLR、看编译原理、看 SpEL 源码的时候,会让你觉得"哦原来底层是这样"。
理解原理比背实现重要得多。
我在做一个用卡皮巴拉讲设计模式的微信小程序「爪爪代码冒险记」,23 个设计模式用漫画 + 答题的方式讲,目前正在开发中。如果你觉得这类内容有意思,搜一下「爪爪代码冒险记」,或者等我后面的文章------每篇文章我都会同步对应的小程序内容。