解释器模式:让代码自己“读懂”数学公式

说实话,解释器模式是我在《设计模式实战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图,把整个模式的结构和流程画清楚了。我建议你多看几遍,理解每个角色之间的关系:

解释器模式结构图 抽象表达式 + interpret(Context) 终结符表达式 数字、变量、布尔值 语法树的叶子节点 非终结符表达式 加法、乘法、AND、OR 语法树的内部节点 递归组合 上下文 (Context) 客户端 抽象 终结符 非终结符 上下文

如何构建语法树

有了表达式类,我们还需要一个解析器来把字符串“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