18. 解释器模式
一、解释器模式
解释器模式(Interpreter Pattern)描述如何使用面向对象语言构成一个简单的语言解释器。在某些情况下,为了更好地描述某些特定类型的问题,可以创建一种新的语言。这种语言拥有自己的表达式和结构,即文法规则,这些问题的实例将对应为该语言中的句子。此时,可以使用解释器模式来设计这种新的语言。
解释器模式通常包含以下几种角色:
Context(环境类):环境类又称为上下文类,它用于存储解释器之外的一些全局信息,通常它临时存储了需要解释的语句。
- 抽象表达式(AbstractExpression) :在抽象表达式中声明了抽象的解释操作,它是所有终结符表达式和非终结符表达式的公共父类。
- 终结符表达式(TerminalExpression) :实现抽象表达式,用于处理语言中的终结符,即不可再分的基本元素。它实现了与文法中的终结符相关联的解释操作,在句子中的每一个终结符都是该类的一个实例。
- 非终结符表达式(NonterminalExpression):实现抽象表达式,用于处理语言中的非终结符,即可以进一步分解的组合元素。它实现了文法中非终结符的解释操作。由于在非终结符表达式中可以包含终结符表达式,也可以继续包含非终结符表达式,因此其解释操作一般通过递归的方式来完成。
- 环境类(Context):环境类又称为上下文类,它用于存储解释器之外的一些全局信息,通常它临时存储了需要解释的语句。
- 客户端(Client):使用解释器来解析和执行特定的语言或表达式。
解释器模式中,每一种终结符和非终结符都有一个具体类与之对应。正因为使用类来表示每一条文法规则,所以系统将具有较好的灵活性和可扩展性。
二、C++实现解释器模式
对于所有的终结符和非终结符,首先需要抽象出一个公共父类,即 抽象表达式类。
// 抽象表达式类
class Expression
{
public:
virtual int interpret(Context * context) = 0;
};
终结符表达式类和非终结符表达式类都是抽象表达式类的子类。对于 终结符表达式类,其代码很简单,主要是对终结符元素的处理。
// 终结符表达式类
class Number : public Expression
{
private:
std::string key;
public:
Number(std::string key);
int interpret(Context * context) override;
std::string getKey(void);
void setKey(std::string key);
};
Number::Number(std::string key) : key(key) {}
int Number::interpret(Context * context)
{
return context->getValue(key);
}
std::string Number::getKey(void)
{
return key;
}
void Number::setKey(std::string key)
{
this->key = key;
}
对于 非终结符表达式类,其代码相对比较复杂,因为可以通过非终结符将表达式组合成更加复杂的结构。
// 非终结符表达式
class Symbol : public Expression
{
private:
Expression * left;
Expression * right;
public:
Symbol(Expression * left, Expression * right);
Expression * getLeft(void);
void setLeft(Expression * left);
Expression * getRight(void);
void setRight(Expression * right);
};
Symbol::Symbol(Expression * left, Expression * right) : left(left), right(right) {}
Expression * Symbol::getLeft(void)
{
return left;
}
void Symbol::setLeft(Expression * left)
{
this->left = left;
}
Expression * Symbol::getRight(void)
{
return right;
}
void Symbol::setRight(Expression * right)
{
this->right = right;
}
// 非终结符表达式
class Plus : public Symbol
{
public:
using Symbol::Symbol;
int interpret(Context * context) override;
};
int Plus::interpret(Context * context)
{
return getLeft()->interpret(context) + getRight()->interpret(context);
}
// 非终结符表达式
class Minus : public Symbol
{
public:
using Symbol::Symbol;
int interpret(Context * context) override;
};
int Minus::interpret(Context * context)
{
return getLeft()->interpret(context) - getRight()->interpret(context);
}
除了上述用于表示表达式的类以外,通常在解释器模式中还提供了一个 环境类 Context,用于存储一些全局信息。
// 环境角色类
class Context
{
private:
// 定义一个map集合,用来存储变量即对应的值
std::map<std::string, int> contextMap;
public:
void assign(std::string key, int value);
int getValue(std::string key);
};
void Context::assign(std::string key, int value)
{
contextMap[key] = value;
}
int Context::getValue(std::string key)
{
return contextMap[key];
}
当系统无须提供全局公共信息时可以省略环境类,也可根据实际情况决定是否需要环境类。解释器模式中,环境类 Context 用于存储解释器之外的一些全局信息。它通常作为参数被传递到所有表达式的解释方法 interpret() 中,可以在 Context 对象中存储和访问表达式解释器的状态,向表达式解释器提供一些全局的、公共的数据。此外,还可以在 Context 中增加一些所有表达式解释器都共有的功能,减轻解释器的职责。
将字符串表达式拆分函数:
std::vector<std::string> splitExpression(const std::string & expression)
{
std::vector<std::string> tokens = std::vector<std::string>();
std::regex regex_expression(R"((\+|-|\*|/)|(\d+))");
auto begin = std::sregex_iterator(expression.begin(), expression.end(), regex_expression);
auto end = std::sregex_iterator();
for (auto it = begin; it != end; ++it)
{
tokens.push_back(it->str());
}
return tokens;
}
计算表达式函数:
int calculate(const std::vector<std::string> & tokens)
{
Expression * left = nullptr;
Expression * right = nullptr;
Expression * result = nullptr;
std::stack<Expression *> stack;
Context context;
for (int i = 0; i < tokens.size(); i++)
{
if (tokens[i] == "+" || tokens[i] == "-")
{
continue;
}
else
{
int value = std::stoi(tokens[i]); // 转换为int
context.assign(tokens[i], value);
}
}
for (int i = 0; i < tokens.size(); i++)
{
if (tokens[i] == "+")
{
left = stack.top(); // 获取栈顶元素,作为左表达式
stack.pop();
right = new Number(tokens[++i]);
Plus * plus = new Plus(left, right);
stack.push(plus);
}
else if (tokens[i] == "-")
{
left = stack.top(); // 获取栈顶元素,作为左表达式
stack.pop();
right = new Number(tokens[++i]);
Minus * minus = new Minus(left, right);
stack.push(minus);
}
else
{
// 遇到变量,则创建一个变量表达式,并压入栈中
Number * number = new Number(tokens[i]);
stack.push(number);
}
}
// 当遍历完这个vector后,栈中就得到最终的表达式
result = stack.top();
stack.pop();
return result->interpret(&context);
}
main() 函数:
#include <iostream>
#include <regex>
#include <map>
#include <vector>
#include <stack>
int main(void)
{
std::string expression;
std::vector<std::string> tokens;
int result = 0;
std::cout << "请输入表达式:" << std::endl;
getline(std::cin, expression);
tokens = splitExpression(expression);
result = calculate(tokens);
std::cout << "表达式的值为:" << result << std::endl;
return 0;
}
三、解释器模式的总结
3.1、解释器模式的优点
- 易于改变和扩展文法。由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。
- 每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言。
- 实现文法较为容易。在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂,还可以通过一些工具自动生成节点类代码。
- 增加新的解释表达式较为方便。如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式类,原有表达式类代码无须修改,符合开闭原则。
3.2、解释器模式的缺点
- 对于复杂文法难以维护。在解释器模式中,每一条规则至少需要定义一个类,因此如果一种语言包含太多文法规则,类的个数将会急剧增加,导致系统难以管理和维护,此时可以考虑使用语法分析程序等方式来取代解释器模式。
- 执行效率较低。由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而且代码的调试过程也比较麻烦。
3.3、解释器模式的适用场景
- 可以将一个需要解释执行的语言中的句子表示为一个抽象语法树。
- 一些重复出现的问题可以用一种简单的语言来进行表达。
- 一个语言的文法较为简单。
- 执行效率不是关键问题。(注:高效的解释器通常不是通过直接解释抽象语法树来实现的,而是需要将它们转换成其他形式,使用解释器模式的执行效率并不高。)