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

解释器模式,说白了就是给语言写个解释器。你想想看,我们平时写代码,编译器是怎么理解你写的if-else、for循环的?背后就是一套解释机制。解释器模式把这个过程抽象出来,让你可以定义自己的语法规则,然后解释执行。

我在项目中遇到过这样一个场景:业务方需要动态配置一些过滤规则,比如「年龄大于18且城市为北京」。如果每次改规则都要改代码重新部署,那运维成本就太高了。这时候,解释器模式就派上了用场——我们把规则定义成一种简单的表达式语言,运行时动态解析执行。

核心动机:当某个问题频繁出现,且可以用简单的语法规则来描述时,解释器模式能让你把「语法定义」和「语法解释」分离开,从而灵活地扩展和修改语法。

模式定义

解释器模式(Interpreter Pattern)定义了一种文法表示,并提供了一个解释器,用于解释该文法中的句子。它属于行为型设计模式。

嗯,这里要注意:不是所有语法都适合用解释器模式。它只适用于那些语法规则相对简单、且执行效率不是首要考量的场景。如果你要做一个完整的编程语言解释器,那得用专门的编译器工具(如Lex/Yacc),而不是这个模式。

角色与结构

解释器模式涉及以下几个角色,我习惯把它们分成三组来理解:

角色 名称 职责
AbstractExpression 抽象表达式 声明一个抽象的interpret()操作,是所有终结符和非终结符表达式的公共父类
TerminalExpression 终结符表达式 实现与文法中的终结符相关的解释操作。一个句子中的每个终结符都需要一个实例
NonterminalExpression 非终结符表达式 对文法中的非终结符进行解释。通常递归地调用其他表达式的interpret()方法
Context 上下文 包含解释器之外的全局信息,比如变量值、环境配置等
Client 客户端 构建抽象语法树(AST),调用interpret()方法

结构其实不复杂。抽象表达式是根,终结符表达式是叶子节点,非终结符表达式是中间节点。整个结构就是一棵树——抽象语法树(AST)。

解释器模式结构图 Client AbstractExpression + interpret(Context) TerminalExpression + interpret(Context) NonterminalExpression + interpret(Context) 递归调用 Context 抽象类 终结符 非终结符 上下文

C++代码实现

我们来实现一个简单的布尔表达式解释器。支持三种操作:AND、OR、NOT,以及变量。比如表达式「A AND B OR NOT C」。

先定义抽象表达式接口:

// AbstractExpression
class Expression {
public:
    virtual ~Expression() = default;
    virtual bool interpret(const std::map<std::string, bool>& context) const = 0;
};

然后是终结符表达式——变量:

// TerminalExpression
class VariableExpression : public Expression {
private:
    std::string name_;
public:
    explicit VariableExpression(const std::string& name) : name_(name) {}
    
    bool interpret(const std::map<std::string, bool>& context) const override {
        auto it = context.find(name_);
        if (it == context.end()) {
            throw std::runtime_error("Variable not found: " + name_);
        }
        return it->second;
    }
};

非终结符表达式——AND、OR、NOT:

// NonterminalExpression - AND
class AndExpression : public Expression {
private:
    std::shared_ptr<Expression> left_;
    std::shared_ptr<Expression> right_;
public:
    AndExpression(std::shared_ptr<Expression> left, std::shared_ptr<Expression> right)
        : left_(std::move(left)), right_(std::move(right)) {}
    
    bool interpret(const std::map<std::string, bool>& context) const override {
        return left_->interpret(context) && right_->interpret(context);
    }
};

// NonterminalExpression - OR
class OrExpression : public Expression {
private:
    std::shared_ptr<Expression> left_;
    std::shared_ptr<Expression> right_;
public:
    OrExpression(std::shared_ptr<Expression> left, std::shared_ptr<Expression> right)
        : left_(std::move(left)), right_(std::move(right)) {}
    
    bool interpret(const std::map<std::string, bool>& context) const override {
        return left_->interpret(context) || right_->interpret(context);
    }
};

// NonterminalExpression - NOT
class NotExpression : public Expression {
private:
    std::shared_ptr<Expression> expr_;
public:
    explicit NotExpression(std::shared_ptr<Expression> expr) : expr_(std::move(expr)) {}
    
    bool interpret(const std::map<std::string, bool>& context) const override {
        return !expr_->interpret(context);
    }
};

个人经验:我习惯用std::shared_ptr来管理表达式树的生命周期。这样不用担心内存泄漏,而且可以安全地在多个地方共享同一个子表达式。不过要注意循环引用的问题——好在这里的树结构是单向的,不会出现循环。

客户端使用示例:

int main() {
    // 构建表达式: (A AND B) OR NOT C
    auto a = std::make_shared<VariableExpression>("A");
    auto b = std::make_shared<VariableExpression>("B");
    auto c = std::make_shared<VariableExpression>("C");
    
    auto andExpr = std::make_shared<AndExpression>(a, b);
    auto notExpr = std::make_shared<NotExpression>(c);
    auto root = std::make_shared<OrExpression>(andExpr, notExpr);
    
    // 设置上下文
    std::map<std::string, bool> context = {
        {"A", true},
        {"B", false},
        {"C", true}
    };
    
    bool result = root->interpret(context);
    // 结果: (true AND false) OR NOT true = false OR false = false
    
    return 0;
}

语法解析

上面我们手动构建了抽象语法树。但在实际项目中,通常需要从文本表达式解析出AST。这就涉及到了语法解析。

我曾经接手过一个项目,业务方用Excel表格配置了上百条过滤规则,每条规则都是类似「field1 > 10 AND field2 == 'active'」这样的字符串。手动解析?那不得累死。后来我用递归下降解析器,配合解释器模式,一天就搞定了。

一个简单的递归下降解析器示例:

class Parser {
private:
    std::vector<std::string> tokens_;
    size_t pos_ = 0;
    
    std::string peek() {
        return pos_ < tokens_.size() ? tokens_[pos_] : "";
    }
    
    std::string consume() {
        return tokens_[pos_++];
    }
    
    // 解析表达式: OR 优先级最低
    std::shared_ptr<Expression> parseOr() {
        auto left = parseAnd();
        while (peek() == "OR") {
            consume();
            auto right = parseAnd();
            left = std::make_shared<OrExpression>(left, right);
        }
        return left;
    }
    
    // 解析 AND
    std::shared_ptr<Expression> parseAnd() {
        auto left = parseNot();
        while (peek() == "AND") {
            consume();
            auto right = parseNot();
            left = std::make_shared<AndExpression>(left, right);
        }
        return left;
    }
    
    // 解析 NOT 和基本表达式
    std::shared_ptr<Expression> parseNot() {
        if (peek() == "NOT") {
            consume();
            auto expr = parseNot();  // 支持连续 NOT
            return std::make_shared<NotExpression>(expr);
        }
        if (peek() == "(") {
            consume();  // 吃掉 '('
            auto expr = parseOr();
            if (peek() != ")") throw std::runtime_error("Missing )");
            consume();  // 吃掉 ')'
            return expr;
        }
        // 变量
        std::string var = consume();
        return std::make_shared<VariableExpression>(var);
    }
    
public:
    std::shared_ptr<Expression> parse(const std::string& input) {
        // 分词:将 "A AND (B OR NOT C)" 拆成 tokens
        tokens_ = tokenize(input);
        pos_ = 0;
        auto ast = parseOr();
        if (pos_ != tokens_.size()) {
            throw std::runtime_error("Unexpected token: " + tokens_[pos_]);
        }
        return ast;
    }
};

避坑指南:我曾经在解析时忘记处理括号匹配,结果一个复杂的嵌套表达式解析出了完全错误的AST。调试了整整一个下午才发现是括号栈没清空。所以,写解析器时一定要先写单元测试,把各种边界情况都覆盖到——空表达式、单变量、多层嵌套、连续NOT等等。

分词器(tokenizer)相对简单,这里给个示意:

std::vector<std::string> tokenize(const std::string& input) {
    std::vector<std::string> tokens;
    std::string current;
    for (char ch : input) {
        if (ch == ' ') {
            if (!current.empty()) {
                tokens.push_back(current);
                current.clear();
            }
        } else if (ch == '(' || ch == ')') {
            if (!current.empty()) {
                tokens.push_back(current);
                current.clear();
            }
            tokens.push_back(std::string(1, ch));
        } else {
            current += ch;
        }
    }
    if (!current.empty()) {
        tokens.push_back(current);
    }
    return tokens;
}

使用起来就很简单了:

Parser parser;
auto ast = parser.parse("A AND (B OR NOT C)");
std::map<std::string, bool> ctx = {{"A", true}, {"B", false}, {"C", true}};
bool result = ast->interpret(ctx);  // true AND (false OR NOT true) = false

核心要点回顾:

  • 解释器模式把「语法规则」抽象成表达式树,每个节点负责自己的解释逻辑
  • 终结符表达式是叶子节点,非终结符表达式是中间节点,递归组合
  • 上下文(Context)保存解释过程中需要的全局信息
  • 递归下降解析器是构建AST的常用手段,配合解释器模式非常自然
  • 适合语法规则简单、变化不频繁的场景。复杂语法请用专门的解析器生成工具

说实话,解释器模式在实际项目中用得不算多。但一旦用上,往往能解决其他模式搞不定的问题。我个人觉得,理解这个模式最大的价值不在于直接套用,而在于培养一种「把问题抽象成语言」的思维方式。当你遇到一个需要频繁修改规则的系统时,不妨想想:能不能定义一种小语言,让规则自己解释自己?


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