1.动机

  • 如果在系统中某一特定类型的问题发生的频率很高,此时可以考虑将这些问题的实例表述为一个语言中的句子,因此可以构建一个解释器,该解释器通过解释这些句子来解决这些问题。
  • 解释器模式描述了如何构成一个简单的语言解释器,主要应用在使用面向对象语言开发的编译器中。

2.定义

解释器模式 (Interpreter Pattern)给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题。

  • 有个游戏,输入up walk 5,玩家必须按照:移动方向+移动方式+移动距离这种格式输入我的指令,而这种格式的指令就是一种文法,只有按照了我定义的这种文法去输入,才能控制屏幕上的小狗去移动。当然了,我输入up walk 5,屏幕上的小狗肯定是听不懂的,它不知道我输入的是什么,这个时候需要怎么办?我需要一个工具去将我输入的内容翻译成小狗能听懂的东西,而这个工具就是定义中提到的解释器,解释器对我输入的指令进行解释,然后将解释得到的指令发送给屏幕上的小狗,小狗听懂了,就进行实际的移动。

3.文法规则和抽象语法树

再拿上面的游戏这个例子进行说明,我可以定义以下五条文法:

expression ::= direction action distance | composite //表达式
composite ::= expression 'and' expression //复合表达式
direction ::= 'up' | 'down' | 'left' | 'right' //移动方向
action ::= 'move' | 'walk' //移动方式
distance ::= an integer //移动距离
  • 上面的5条文法规则,对应5个语言单位,这些语言单位可以分为两大类:一类为终结符(也叫做终结符表达式),例如上面的direction、action和distance,它们是语言的最小组成单位,不能再进行拆分;另一类为非终结符(也叫做非终结符表达式),例如上面的expression和composite,它们都是一个完整的句子,包含一系列终结符或非终结符。
  • 我们就是根据上面定义的一些文法可以构成更多复杂的语句,计算机程序将根据这些语句进行某种操作;而我们这里列出的文法,计算机是无法直接看懂的,所以,我们需要对我们定义的文法进行解释;就好比,我们编写的C++代码,计算机是看不懂的,我们需要进行编译一样。解释器模式,就提供一种模式去给计算机解释我们定义的文法,让计算机根据我们的文法去进行工作。
  • 在文法规则定义中可以使用一些符号来表示不同的含义,如使用“|”表示或,使用“{”和“}”表示组合,使用“*”表示出现0次或多次等,其中使用频率最高的符号是表示“或”关系的“|”,如文法规则“bool Value ::= 0 | 1”表示终结符表达式bool Value的取值可以为0或者1。
  • 除了使用文法规则来定义一个语言,在解释器模式中还可以通过一种称之为抽象语法树的图形方式来直观地表示语言的构成,每一棵语法树对应一个语言实例,对于上面的游戏文法规则,可以通过以下的抽象语法树来进行表示:

clipboard.png

4.模式结构

clipboard.png

AbstractExpression:声明一个抽象的解释操作,这个接口被抽象语法树中所有的节点所共享;

TernimalExpression:一个句子中的每个终结符需要该类的一个实例,它实现与文法中的终结符相关联的解释操作;

NonternimalExpression:

  • 对于文法中的每一条规则都需要一个NonternimalExpression类;
  • 为文法中的的每个符号都维护一个AbstractExpression类型的实例变量;
  • 为文法中的非终结符实现解释操作,在实现时,一般要递归地调用表示文法符号的那些对象的解释操作;

Context:包含解释器之外的一些全局信息;
Client:构建一个需要进行解释操作的文法句子,然后调用解释操作进行解释。

实际进行解释时,按照以下时序进行的:

  • Client构建一个句子,它是NonterminalExpression和TerminalExpression的实例的一个抽象语法树,然后初始化上下文并调用解释操作;
  • 每一非终结符表达式节点定义相应子表达式的解释操作。而各终结符表达式的解释操作构成了递归的基础;
  • 每一节点的解释操作用上下文来存储和访问解释器的状态

5.代码实现

#include <string> 
#include <iostream> 
using namespace std;

/************************************************************************
* description: 演奏内容
* remark:
************************************************************************/
class playContext
{
public:
    string getPlayText()
    {
        return m_strText;
    }
    void setPlayText(const string& strText)
    {
        m_strText = strText;
    }
private:
    string m_strText;
};


/************************************************************************
* description: 表达式类
* remark:
************************************************************************/
class expression
{
public:
    // 解释器 
    void interpret(playContext& PlayContext)
    {
        if (PlayContext.getPlayText().empty())
        {
            return;
        }
        else
        {
            string strPlayKey = PlayContext.getPlayText().substr(0, 1);
            string strtemp = PlayContext.getPlayText().substr(2);
            PlayContext.setPlayText(strtemp);

            size_t nPos = PlayContext.getPlayText().find(" ");
            string strPlayValue = PlayContext.getPlayText().substr(0, nPos);
            int  nPlayValue = stoi(strPlayValue);
            
            nPos = PlayContext.getPlayText().find(" ");
            //递归处理
            PlayContext.setPlayText(PlayContext.getPlayText().substr(nPos + 1));
            //cout << PlayContext.getPlayText() << endl;
            excute(strPlayKey, nPlayValue);
            
        }
    }
    // 执行 
    virtual void excute(string& strKey, const int nValue) = 0;
};


/************************************************************************
* description: 音符类
* remark:
************************************************************************/
class note : public expression
{
public:
    virtual void excute(string& strKey, const int nValue)
    {
        char szKey = strKey.substr(0, 1)[0];

        string strNote;
        switch (szKey)
        {
        case 'C':
            strNote = "N1";
            break;
        case 'D':
            strNote = "N2";
            break;
        case 'E':
            strNote = "N3";
            break;
        case 'F':
            strNote = "N4";
            break;
        case 'G':
            strNote = "N5";
            break;
        case 'A':
            strNote = "N6";
            break;
        case 'B':
            strNote = "N7";
            break;
        default:
            strNote = "error";
            break;
        }
        cout << strNote << " ";
    }
};


/************************************************************************
* description: 音阶类
* remark:
************************************************************************/
class scale : public expression
{
public:
    virtual void excute(string& strKey, const int nValue)
    {
        string strScale;
        switch (nValue)
        {
        case 1:
            strScale = "低音";
            break;
        case 2:
            strScale = "中音";
            break;
        case 3:
            strScale = "高音";
            break;
        default:
            strScale = "error";
            break;
        }
        cout << strScale << " ";
    }
private:
};

//音乐速度类
class speed : public expression
{
public:
    virtual void excute(string& strKey, const int nValue)
    {
        string strSpeed;
        if (nValue < 3)
        {
            strSpeed = "慢速";
        }
        else if (nValue >= 6)
        {
            strSpeed = "快速";
        }
        else
        {
            strSpeed = "中速";
        }
        cout << strSpeed << " ";
    }
};


int main()
{
    playContext context;
    cout << "Music:";

    context.setPlayText("T 2 O 2 E 2 G 3 G 4 ");
    expression* expressObj = nullptr;

    while (!context.getPlayText().empty())
    {
        char szKey = context.getPlayText().substr(0, 1)[0];

        switch (szKey)
        {
        case 'O':
            expressObj = new scale();
            break;
        case 'T':
            expressObj = new speed();
            break;
        case 'C':
        case 'D':
        case 'E':
        case 'F':
        case 'G':
        case 'A':
        case 'B':
        case 'P':
            expressObj = new note();
            break;
        default:
            break;
        }
        if (nullptr != expressObj)
        {
            expressObj->interpret(context);
        }
    }

    system("pause");
    return 0;
}

6.优点

  • 易于改变和扩展文法。由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法;
  • 每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言;
  • 实现文法较为容易;在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂;
  • 增加新的解释表达式较为方便。如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式类或非终结符表达式类,原有表达式类代码无须修改,符合“开闭原则”。

7.缺点

  • 对于复杂文法难以维护;在解释器模式中,每一条规则至少需要定义一个类,因此如果一个语言包含太多文法规则,类的个数将会急剧增加,导致系统难以管理和维护,此时可以考虑使用语法分析程序等方式来取代解释器模式;
  • 执行效率低;由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而且代码的调试过程也很麻烦。

8.适用场景

  • 可以将一个需要解释执行的语言中的句子表示为一个抽象语法树;
  • 一些重复出现的问题可以用一种简单的语言来进行表达;
  • 一个语言的文法较为简单
  • 执行效率不是关键问题

本文固定链接: http://www.js-code.com/c/c_63096.html