一、解释器模式:语法解析、表达式求值

解释器模式,说实话,在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」这个表达式对应的语法树结构:

减法 (-) 加法 (+) 数字 (2) 数字 (3) 数字 (5) 语法树结构:非终结符节点(运算符)包含子节点 终结符节点(数字)是叶子节点,没有子节点 解释过程:从叶子到根,递归求值

解释过程就是后序遍历这棵树:先算3+5=8,再算8-2=6。每个节点只关心自己的子节点返回什么值,不关心子节点内部怎么算的——这就是组合模式+递归的魅力。

4. 我在项目中踩过的坑

我曾经在一个规则引擎项目里用过解释器模式,当时犯了个低级错误——把上下文设计成了全局可变状态。结果多个表达式并发解释时,变量值互相覆盖,查了半天才发现。

避坑指南:

  • 上下文对象尽量设计成不可变的,或者每个解释器实例持有自己的上下文副本
  • 如果表达式树很大,递归深度可能引发栈溢出,考虑用迭代代替递归
  • 终结符和非终结符的区分要清晰,别把能拆分的节点设计成终结符

还有一个容易忽略的点:性能问题。解释器模式本质上是运行时解释,效率肯定不如编译执行。如果你要解析的表达式数量巨大(比如每秒几万次),建议提前把表达式编译成某种中间表示,或者直接用现成的表达式引擎(比如Aviator、QLExpress)。

5. 什么时候该用?什么时候不该用?

我个人习惯是:当语法规则简单且稳定,但需要频繁扩展时,考虑解释器模式。比如:

  • 简单的数学表达式计算
  • 自定义的脚本或规则语言
  • SQL解析、正则表达式引擎(虽然这些通常用更专业的工具)

但如果语法规则特别复杂(比如完整的编程语言),或者性能要求极高,就别自己造轮子了。用ANTLR、JavaCC这类解析器生成器,或者直接用现成的脚本引擎,它不香吗?

我的建议:解释器模式最适合「语法规则数量有限且变化不频繁」的场景。如果规则经常变,每次都要新增类,维护成本会直线上升。这时候可以考虑用访问者模式来解耦,或者干脆用策略模式+配置化。

6. 和类似模式的对比

模式 核心区别
解释器模式 关注「语法规则」的解释执行,每个规则一个类
策略模式 关注「算法」的替换,每个算法一个类
组合模式 关注「部分-整体」的层次结构,树形管理
访问者模式 关注「在不改变元素类的前提下增加操作」

其实解释器模式和组合模式经常一起出现——语法树本身就是一种组合结构。而访问者模式可以用来遍历语法树,在不修改表达式类的前提下增加新的操作(比如打印、优化、类型检查)。

好了,关于解释器模式就聊这么多。它不像单例、工厂那样天天用,但一旦遇到合适的场景,用起来是真的顺手。下次你遇到需要解析自定义语法的需求时,不妨想想这个模式。


公众号:蓝海资料掘金营,微信deep3321