解释器模式:模式动机与定义
解释器模式,说白了就是给语言写个解释器。你想想看,我们平时写代码,编译器是怎么理解你写的if-else、for循环的?背后就是一套解释机制。解释器模式把这个过程抽象出来,让你可以定义自己的语法规则,然后解释执行。
我在项目中遇到过这样一个场景:业务方需要动态配置一些过滤规则,比如「年龄大于18且城市为北京」。如果每次改规则都要改代码重新部署,那运维成本就太高了。这时候,解释器模式就派上了用场——我们把规则定义成一种简单的表达式语言,运行时动态解析执行。
核心动机:当某个问题频繁出现,且可以用简单的语法规则来描述时,解释器模式能让你把「语法定义」和「语法解释」分离开,从而灵活地扩展和修改语法。
模式定义
解释器模式(Interpreter Pattern)定义了一种文法表示,并提供了一个解释器,用于解释该文法中的句子。它属于行为型设计模式。
嗯,这里要注意:不是所有语法都适合用解释器模式。它只适用于那些语法规则相对简单、且执行效率不是首要考量的场景。如果你要做一个完整的编程语言解释器,那得用专门的编译器工具(如Lex/Yacc),而不是这个模式。
角色与结构
解释器模式涉及以下几个角色,我习惯把它们分成三组来理解:
| 角色 | 名称 | 职责 |
|---|---|---|
| AbstractExpression | 抽象表达式 | 声明一个抽象的interpret()操作,是所有终结符和非终结符表达式的公共父类 |
| TerminalExpression | 终结符表达式 | 实现与文法中的终结符相关的解释操作。一个句子中的每个终结符都需要一个实例 |
| NonterminalExpression | 非终结符表达式 | 对文法中的非终结符进行解释。通常递归地调用其他表达式的interpret()方法 |
| Context | 上下文 | 包含解释器之外的全局信息,比如变量值、环境配置等 |
| Client | 客户端 | 构建抽象语法树(AST),调用interpret()方法 |
结构其实不复杂。抽象表达式是根,终结符表达式是叶子节点,非终结符表达式是中间节点。整个结构就是一棵树——抽象语法树(AST)。
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