解释器模式:模式动机与定义

解释器模式,说白了就是给语言造一个“翻译官”。

我记得刚入行那会儿,接手过一个老项目——里面全是各种自定义的配置脚本。什么“IF A > 100 THEN B = C * 2”这种语法,看着像SQL又不是SQL。每次要加新规则,就得改一大坨if-else。改到后面,我自己都看不懂了。

后来我翻《GoF设计模式》,看到解释器模式,一拍大腿——早该用这个!

解释器模式的动机其实很朴素:当你的系统需要频繁解析和执行某种“语言”时,别硬编码,给它定义一个语法规则,然后用解释器去处理。这里的“语言”不一定是编程语言,可以是数学表达式、SQL子集、配置文件、甚至游戏中的AI指令。

核心定义:给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。

嗯,这句话有点绕。我换个说法:你定义一套规则,解释器负责按规则干活

角色与结构

解释器模式的结构其实不复杂,就四个角色:

角色 职责 我见过的坑
AbstractExpression(抽象表达式) 声明一个解释操作,所有节点都要实现它 很多人忘了定义纯虚析构函数
TerminalExpression(终结符表达式) 处理语法中的最小单元,比如数字、变量 终结符往往是最容易被忽略的
NonterminalExpression(非终结符表达式) 处理组合规则,比如加法、乘法、if条件 递归调用时容易栈溢出
Context(上下文) 存储全局信息,比如变量值、环境参数 我习惯用shared_ptr管理上下文生命周期

结构上,它其实就是一个组合模式 + 递归调用。每个非终结符节点都包含子节点,解释时递归往下走。

我的习惯:画类图时,先把终结符和非终结符分开。终结符是叶子节点,非终结符是树枝节点。这样结构一目了然。
解释器模式结构图 AbstractExpression + interpret(Context) TerminalExpression + interpret(Context) NonterminalExpression + interpret(Context) 子表达式(递归) 子表达式(递归) Context

C++代码实现

咱们直接写一个简单的算术表达式解释器。支持加减法,比如“3 + 5 - 2”。

先定义抽象基类:

#include <iostream>
#include <memory>
#include <unordered_map>
#include <string>

// 抽象表达式
class Expression {
public:
    virtual ~Expression() = default;
    virtual int interpret(const std::unordered_map<std::string, int>& ctx) = 0;
};

终结符表达式——处理数字:

// 数字表达式(终结符)
class NumberExpression : public Expression {
    int value;
public:
    explicit NumberExpression(int v) : value(v) {}
    int interpret(const std::unordered_map<std::string, int>& ctx) override {
        return value;
    }
};

非终结符表达式——处理加法:

// 加法表达式(非终结符)
class AddExpression : public Expression {
    std::shared_ptr<Expression> left;
    std::shared_ptr<Expression> right;
public:
    AddExpression(std::shared_ptr<Expression> l, std::shared_ptr<Expression> r)
        : left(std::move(l)), right(std::move(r)) {}
    
    int interpret(const std::unordered_map<std::string, int>& ctx) override {
        return left->interpret(ctx) + right->interpret(ctx);
    }
};

减法同理:

class SubExpression : public Expression {
    std::shared_ptr<Expression> left;
    std::shared_ptr<Expression> right;
public:
    SubExpression(std::shared_ptr<Expression> l, std::shared_ptr<Expression> r)
        : left(std::move(l)), right(std::move(r)) {}
    
    int interpret(const std::unordered_map<std::string, int>& ctx) override {
        return left->interpret(ctx) - right->interpret(ctx);
    }
};
我曾经踩过的坑:一开始我直接用裸指针管理表达式树,结果内存泄漏得一塌糊涂。后来全部换成 shared_ptr,世界清净了。记住:解释器模式生成的树结构,生命周期管理一定要用智能指针

写个简单的解析器,把“3+5-2”这种字符串转成表达式树:

// 简易解析器(只支持加减法,数字为个位数)
std::shared_ptr<Expression> parse(const std::string& input) {
    auto left = std::make_shared<NumberExpression>(input[0] - '0');
    
    for (size_t i = 1; i < input.size(); i += 2) {
        char op = input[i];
        auto right = std::make_shared<NumberExpression>(input[i+1] - '0');
        
        if (op == '+') {
            left = std::make_shared<AddExpression>(left, right);
        } else if (op == '-') {
            left = std::make_shared<SubExpression>(left, right);
        }
    }
    return left;
}

int main() {
    std::unordered_map<std::string, int> ctx;  // 本例中未使用
    auto expr = parse("3+5-2");
    std::cout << "3+5-2 = " << expr->interpret(ctx) << std::endl;
    return 0;
}

输出:3+5-2 = 6

语法解析示例

上面那个例子太简单了?咱们来点实际的。

我在项目中遇到过需要解析自定义查询语言的情况。比如用户输入:SELECT name WHERE age > 18。用解释器模式,我们可以这样设计:

  • 终结符:字段名(name、age)、数值(18)、操作符(>)
  • 非终结符:WHERE条件、SELECT语句、比较表达式

你想想看,如果不用解释器模式,你会怎么写?大概率是一堆if-else判断字符串,然后硬编码处理逻辑。每次加新语法,就得改核心代码,风险极高。

用解释器模式,加新语法就是加一个新的表达式子类,完全符合开闭原则。

我的建议:解释器模式最适合语法规则相对固定、但组合方式千变万化的场景。如果你的语法规则本身也在频繁变动,那解释器模式反而会成为负担。这时候可以考虑用ANTLR之类的解析器生成器。

再举一个真实案例。我曾经给一个游戏引擎写过AI行为树解析器。行为树节点包括:Sequence(顺序执行)、Selector(选择执行)、Condition(条件判断)、Action(动作)。每个节点就是一个表达式,组合起来就是一棵巨大的行为树。解释器模式在这里简直是天作之合。

嗯,说到这儿,我想强调一点:解释器模式的核心价值不在于“解析”,而在于“可扩展”。解析字符串只是第一步,真正厉害的是你可以随时组合出新的行为,而不需要改动已有代码。

最后,给个实用建议:别试图用解释器模式解析完整的编程语言。那工作量太大了。它最适合的是领域特定语言(DSL),小而精,刚好够用。


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