解释器模式:让代码自己“读懂”数学公式
说实话,解释器模式是我在《设计模式实战100例》里最想聊的一个模式。为什么?因为它的名字太容易让人误解了。很多人一听“解释器”,就想到Python解释器、Java虚拟机——其实完全不是一回事。
这个模式的核心思想很简单:给一种语言定义它的语法规则,然后写一个解释器来执行这些规则。说白了,就是让程序能“读懂”并“计算”某种特定格式的表达式。
我当年第一次接触这个模式,是在做一个金融风控系统。业务方给了一堆复杂的规则表达式,比如“A > 100 AND (B < 50 OR C == 0)”。如果硬编码去解析,代码会变得又臭又长。后来我用解释器模式重构了,代码量直接砍掉一半。嗯,从那以后我就爱上了这个模式。
模式的核心角色
解释器模式有四个核心角色,我一个个说清楚:
- 抽象表达式(AbstractExpression):定义解释操作的接口,通常是一个
interpret(Context)方法。 - 终结符表达式(TerminalExpression):语法中的最小单元,不能再分解。比如数字、变量名。
- 非终结符表达式(NonterminalExpression):由多个子表达式组合而成,比如加法、乘法、AND、OR。
- 上下文(Context):存放解释器需要的全局信息,比如变量的值映射表。
你想想看,这其实很像语法分析树。终结符是叶子节点,非终结符是内部节点。解释器从根节点开始递归执行,最终得到结果。
一个完整的数学表达式解析器
咱们直接上代码。我写一个支持加减乘除和括号的表达式解析器。先看Java版本:
// 抽象表达式
interface Expression {
int interpret(Context ctx);
}
// 上下文:存储变量值
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);
}
}
// 终结符表达式:数字
class NumberExpression implements Expression {
private int number;
public NumberExpression(int number) {
this.number = number;
}
@Override
public int interpret(Context ctx) {
return number;
}
}
// 终结符表达式:变量
class VariableExpression implements Expression {
private String name;
public VariableExpression(String name) {
this.name = name;
}
@Override
public int interpret(Context ctx) {
return ctx.getVariable(name);
}
}
// 非终结符表达式:加法
class AddExpression implements Expression {
private Expression left, 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);
}
}
// 非终结符表达式:减法
class SubtractExpression implements Expression {
private Expression left, right;
public SubtractExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret(Context ctx) {
return left.interpret(ctx) - right.interpret(ctx);
}
}
// 非终结符表达式:乘法
class MultiplyExpression implements Expression {
private Expression left, right;
public MultiplyExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret(Context ctx) {
return left.interpret(ctx) * right.interpret(ctx);
}
}
// 非终结符表达式:除法
class DivideExpression implements Expression {
private Expression left, right;
public DivideExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret(Context ctx) {
int divisor = right.interpret(ctx);
if (divisor == 0) {
throw new ArithmeticException("除数不能为0");
}
return left.interpret(ctx) / divisor;
}
}
再看C++版本,结构基本一致:
#include <iostream>
#include <map>
#include <memory>
#include <stdexcept>
// 抽象表达式
class Expression {
public:
virtual ~Expression() = default;
virtual int interpret(const std::map<std::string, int>& ctx) = 0;
};
// 终结符:数字
class NumberExpression : public Expression {
int number;
public:
NumberExpression(int n) : number(n) {}
int interpret(const std::map<std::string, int>& ctx) override {
return number;
}
};
// 终结符:变量
class VariableExpression : public Expression {
std::string name;
public:
VariableExpression(const std::string& n) : name(n) {}
int interpret(const std::map<std::string, int>& ctx) override {
auto it = ctx.find(name);
if (it != ctx.end()) return it->second;
throw std::runtime_error("变量未定义: " + name);
}
};
// 非终结符:加法
class AddExpression : public Expression {
std::unique_ptr<Expression> left, right;
public:
AddExpression(std::unique_ptr<Expression> l, std::unique_ptr<Expression> r)
: left(std::move(l)), right(std::move(r)) {}
int interpret(const std::map<std::string, int>& ctx) override {
return left->interpret(ctx) + right->interpret(ctx);
}
};
// 非终结符:减法
class SubtractExpression : public Expression {
std::unique_ptr<Expression> left, right;
public:
SubtractExpression(std::unique_ptr<Expression> l, std::unique_ptr<Expression> r)
: left(std::move(l)), right(std::move(r)) {}
int interpret(const std::map<std::string, int>& ctx) override {
return left->interpret(ctx) - right->interpret(ctx);
}
};
解释器模式的核心结构图
下面这张SVG图,把整个模式的结构和流程画清楚了。我建议你多看几遍,理解每个角色之间的关系:
如何构建语法树
有了表达式类,我们还需要一个解析器来把字符串“3 + 5 * (2 - 1)”变成一棵语法树。这里我展示一个简化版的递归下降解析器:
// Java 简化版解析器
class Parser {
private StringTokenizer tokenizer;
private String currentToken;
public Parser(String expression) {
tokenizer = new StringTokenizer(expression, "+-*/()", true);
nextToken();
}
private void nextToken() {
while (tokenizer.hasMoreTokens()) {
currentToken = tokenizer.nextToken().trim();
if (!currentToken.isEmpty()) return;
}
currentToken = null;
}
// 解析表达式:处理加减
public Expression parseExpression() {
Expression expr = parseTerm();
while (currentToken != null &&
(currentToken.equals("+") || currentToken.equals("-"))) {
String op = currentToken;
nextToken();
Expression right = parseTerm();
if (op.equals("+")) {
expr = new AddExpression(expr, right);
} else {
expr = new SubtractExpression(expr, right);
}
}
return expr;
}
// 解析项:处理乘除
private Expression parseTerm() {
Expression expr = parseFactor();
while (currentToken != null &&
(currentToken.equals("*") || currentToken.equals("/"))) {
String op = currentToken;
nextToken();
Expression right = parseFactor();
if (op.equals("*")) {
expr = new MultiplyExpression(expr, right);
} else {
expr = new DivideExpression(expr, right);
}
}
return expr;
}
// 解析因子:数字、变量、括号
private Expression parseFactor() {
if (currentToken.equals("(")) {
nextToken();
Expression expr = parseExpression();
if (!currentToken.equals(")")) {
throw new RuntimeException("缺少右括号");
}
nextToken();
return expr;
}
// 尝试解析数字
try {
int num = Integer.parseInt(currentToken);
nextToken();
return new NumberExpression(num);
} catch (NumberFormatException e) {
// 当作变量处理
String varName = currentToken;
nextToken();
return new VariableExpression(varName);
}
}
}
使用起来很简单:
// 客户端代码
Context ctx = new Context();
ctx.setVariable("x", 10);
ctx.setVariable("y", 5);
Parser parser = new Parser("x + y * 2");
Expression expr = parser.parseExpression();
int result = expr.interpret(ctx);
System.out.println("结果: " + result); // 输出: 20
核心要点:解释器模式把“语法规则”和“执行逻辑”分离了。每个表达式类只负责自己的计算,组合起来就能处理任意复杂的表达式。这就是组合模式+策略模式的完美结合。
我在项目中踩过的坑
我曾经在一个规则引擎项目里,用解释器模式处理了上百条业务规则。一开始很爽,后来发现性能越来越差。为什么?因为每次解析都要重新构建整棵语法树。
后来我做了两件事:
- 缓存语法树:对于相同的表达式字符串,只解析一次,把语法树缓存起来。
- 预编译:在系统启动时就把所有规则解析好,运行时只做interpret。
这两招下去,性能提升了10倍不止。你想想看,如果每次请求都重新解析,那CPU全花在字符串处理上了。
避坑指南:我曾经犯过一个低级错误——在非终结符表达式里直接修改上下文。这会导致严重的并发问题。记住:上下文应该是只读的,或者至少是线程安全的。
什么时候该用解释器模式?
我个人习惯用三个条件来判断:
| 条件 | 说明 |
|---|---|
| 语法简单 | 语法规则数量有限,且不会频繁变化。如果语法太复杂,建议用ANTLR等工具生成解析器。 |
| 执行效率要求不高 | 解释器模式本质上是递归遍历,性能不如编译型方案。高频调用的场景要慎重。 |
| 需要灵活扩展 | 如果经常要新增语法规则,解释器模式比硬编码好维护得多。 |
我的建议:如果你只是解析简单的数学公式、布尔表达式、或者配置文件里的条件判断,解释器模式是绝佳选择。但如果你要做一门完整的编程语言,还是老老实实用Lex/Yacc或者ANTLR吧。
优缺点一览
最后总结一下,方便你快速决策:
- 优点:
- 语法规则和实现逻辑分离,易于扩展
- 每个表达式类职责单一,符合单一职责原则
- 新增一种操作只需要新增一个类,符合开闭原则
- 缺点:
- 语法规则多的时候,类数量爆炸
- 递归调用深,调试起来比较头疼
- 性能不如编译型方案
好了,解释器模式就聊到这里。记住它的核心思想:把语法规则变成对象,让对象自己解释自己。下次你遇到需要解析表达式或者规则的时候,不妨试试这个模式。
公众号:蓝海资料掘金营,微信deep3321