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 结构图:解释器模式核心流程

解释器模式核心流程 输入表达式 "3 + 5 * 2" 解析器(Parser) 抽象语法树(AST) 解释器(Interpreter) 计算结果:13 原始字符串 词法/语法分析 树形结构表示 递归遍历执行 最终输出

避坑指南

我曾经在一个项目中,直接用解释器模式去解析复杂的业务规则。结果语法树越建越深,性能直接崩了。后来才意识到:解释器模式不适合处理超大规模的文法

几个关键点:

  • 文法不要太复杂:如果规则超过几十条,建议用专门的解析器生成器(比如 ANTLR)
  • 注意递归深度:Java 默认栈深度有限,超长表达式可能 StackOverflow
  • 性能敏感场景慎用:每个节点都要创建对象,GC 压力不小

核心总结

解释器模式的核心思想就一句话:把「语言」变成「树」,然后递归算

它最适合的场景是:
✅ 文法简单且稳定
✅ 执行效率不是第一优先级
✅ 需要频繁修改或扩展规则

个人小技巧

我习惯在写解释器之前,先用「中缀转后缀」算法(调度场算法)把表达式转成逆波兰式。这样构建语法树会简单很多,而且天然支持优先级。

注意

不要试图用解释器模式去解析完整的 SQL 或编程语言。那工作量太大了,直接用现成的解析库更靠谱。解释器模式最适合的是「领域特定语言(DSL)」,比如公式、规则、配置表达式。

好了,关于解释器模式就聊这么多。记住它的核心:文法 + 语法树 + 递归解释。下次遇到需要解析自定义表达式的时候,不妨试试这个模式。


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