解释器模式:模式动机与定义
解释器模式,说白了就是给语言造一个“翻译官”。
我记得刚入行那会儿,接手过一个老项目——里面全是各种自定义的配置脚本。什么“IF A > 100 THEN B = C * 2”这种语法,看着像SQL又不是SQL。每次要加新规则,就得改一大坨if-else。改到后面,我自己都看不懂了。
后来我翻《GoF设计模式》,看到解释器模式,一拍大腿——早该用这个!
解释器模式的动机其实很朴素:当你的系统需要频繁解析和执行某种“语言”时,别硬编码,给它定义一个语法规则,然后用解释器去处理。这里的“语言”不一定是编程语言,可以是数学表达式、SQL子集、配置文件、甚至游戏中的AI指令。
嗯,这句话有点绕。我换个说法:你定义一套规则,解释器负责按规则干活。
角色与结构
解释器模式的结构其实不复杂,就四个角色:
| 角色 | 职责 | 我见过的坑 |
|---|---|---|
| AbstractExpression(抽象表达式) | 声明一个解释操作,所有节点都要实现它 | 很多人忘了定义纯虚析构函数 |
| TerminalExpression(终结符表达式) | 处理语法中的最小单元,比如数字、变量 | 终结符往往是最容易被忽略的 |
| NonterminalExpression(非终结符表达式) | 处理组合规则,比如加法、乘法、if条件 | 递归调用时容易栈溢出 |
| Context(上下文) | 存储全局信息,比如变量值、环境参数 | 我习惯用shared_ptr管理上下文生命周期 |
结构上,它其实就是一个组合模式 + 递归调用。每个非终结符节点都包含子节点,解释时递归往下走。
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);
}
};
写个简单的解析器,把“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判断字符串,然后硬编码处理逻辑。每次加新语法,就得改核心代码,风险极高。
用解释器模式,加新语法就是加一个新的表达式子类,完全符合开闭原则。
再举一个真实案例。我曾经给一个游戏引擎写过AI行为树解析器。行为树节点包括:Sequence(顺序执行)、Selector(选择执行)、Condition(条件判断)、Action(动作)。每个节点就是一个表达式,组合起来就是一棵巨大的行为树。解释器模式在这里简直是天作之合。
嗯,说到这儿,我想强调一点:解释器模式的核心价值不在于“解析”,而在于“可扩展”。解析字符串只是第一步,真正厉害的是你可以随时组合出新的行为,而不需要改动已有代码。
最后,给个实用建议:别试图用解释器模式解析完整的编程语言。那工作量太大了。它最适合的是领域特定语言(DSL),小而精,刚好够用。
公众号:蓝海资料掘金营,微信deep3321