解释器模式:给代码装上翻译引擎
解释器模式,说白了就是定义一套语言规则,然后写个程序去解释它。你想想看,正则表达式、SQL解析、模板引擎……这些底层都在用这个模式。我个人习惯把解释器模式叫做「迷你语言处理器」——它不处理复杂的编译任务,只解决特定领域的小问题。
什么时候需要解释器?
我在项目中遇到过这样一个场景:业务方频繁调整促销规则,每次改规则都要发版。后来我设计了一套简单的表达式语言,让运营人员自己写规则,系统自动解释执行。嗯,这就是解释器模式的典型应用。
适合用解释器模式的场景:
- 你需要处理一种可重复出现的语法结构
- 语法相对简单,不需要完整的编译器
- 业务规则经常变化,希望由非技术人员维护
- 性能要求不是极端苛刻
核心结构:语法树与解释器
解释器模式的核心就两个东西:抽象语法树(AST)和解释器。AST描述句子的结构,解释器遍历AST并执行对应的操作。
来看一个简单的例子:解析并计算「3 + 5 - 2」这样的表达式。
// 抽象表达式
interface Expression {
int interpret();
}
// 终结符表达式 - 数字
class NumberExpression implements Expression {
private int number;
public NumberExpression(int number) {
this.number = number;
}
@Override
public int interpret() {
return number;
}
}
// 非终结符表达式 - 加法
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();
}
}
// 非终结符表达式 - 减法
class SubtractExpression implements Expression {
private Expression left;
private Expression right;
public SubtractExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret() {
return left.interpret() - right.interpret();
}
}
你看,每个表达式节点都实现了 interpret() 方法。数字节点直接返回值,运算节点先递归解释左右子节点,再执行运算。这就是解释器模式最朴素的样子。
构建语法树
光有节点还不够,我们还需要一个解析器来构建语法树。这里我展示一个简单的递归下降解析器:
class Parser {
private String expression;
private int position;
public Parser(String expression) {
this.expression = expression.replaceAll(" ", "");
this.position = 0;
}
public Expression parse() {
Expression result = parseTerm();
while (position < expression.length()) {
char op = expression.charAt(position);
if (op == '+') {
position++;
Expression right = parseTerm();
result = new AddExpression(result, right);
} else if (op == '-') {
position++;
Expression right = parseTerm();
result = new SubtractExpression(result, right);
} else {
break;
}
}
return result;
}
private Expression parseTerm() {
int start = position;
while (position < expression.length() &&
Character.isDigit(expression.charAt(position))) {
position++;
}
int number = Integer.parseInt(expression.substring(start, position));
return new NumberExpression(number);
}
}
// 使用示例
public class InterpreterDemo {
public static void main(String[] args) {
Parser parser = new Parser("3+5-2");
Expression expression = parser.parse();
int result = expression.interpret();
System.out.println("3+5-2 = " + result); // 输出: 6
}
}
解释器模式的知识体系
下面这张图展示了解释器模式的核心脉络:
终结符 vs 非终结符
解释器模式里有两个重要概念:终结符和非终结符。说白了:
- 终结符表达式:叶子节点,不能再分解。比如数字、变量名、字符串常量
- 非终结符表达式:分支节点,由其他表达式组合而成。比如加法、减法、条件判断
我习惯这样记:终结符是「原子」,非终结符是「分子」。原子不可分割,分子由原子组成。
实际项目中的变体
纯解释器模式有个问题:每个语法规则都要定义一个类。如果语法规则多了,类数量会爆炸。我在项目中常用两种变体:
变体一:函数式解释器
用函数代替类,减少类数量:
// 用函数式接口简化
@FunctionalInterface
interface Expr {
int eval();
}
// 工厂方法创建表达式
class ExprFactory {
static Expr number(int n) {
return () -> n;
}
static Expr add(Expr left, Expr right) {
return () -> left.eval() + right.eval();
}
static Expr subtract(Expr left, Expr right) {
return () -> left.eval() - right.eval();
}
}
// 使用
Expr expr = ExprFactory.subtract(
ExprFactory.add(ExprFactory.number(3), ExprFactory.number(5)),
ExprFactory.number(2)
);
System.out.println(expr.eval()); // 输出: 6
变体二:访问者模式结合
当解释器需要支持多种操作(比如求值、格式化输出、类型检查)时,我会把解释器拆成访问者。这样新增操作不需要修改AST节点类。
性能考量
解释器模式有个明显的短板:性能。每次解释执行都要遍历AST,如果表达式很复杂,性能开销不小。
我的优化策略:
- 缓存编译结果:对于频繁执行的相同表达式,第一次解析后缓存AST,后续直接复用
- 预编译为字节码:对于性能敏感的场景,可以把AST编译成类似JVM字节码的中间表示,执行时直接运行字节码
- 限制表达式复杂度:在业务层面限制嵌套深度和节点数量,防止恶意构造复杂表达式
总结
解释器模式不是什么高深莫测的东西。它就是一套「定义语言 + 解释执行」的套路。你想想看,正则表达式、SpEL、OGNL……这些你每天都在用的东西,底层都是解释器模式。
记住三个要点:
- 文法定义要清晰,终结符和非终结符要分清楚
- AST是核心,解释器遍历AST执行操作
- 性能是短板,该缓存就缓存,该限制就限制
嗯,解释器模式就讲到这里。它不像单例模式那样随处可见,但在特定场景下,它是最优雅的解决方案。