設計模式的征途—23.直譯器(Interpreter)模式

Edison Chou發表於2017-09-12

雖然目前計算機程式語言有好幾百種,但有時人們還是希望用一些簡單的語言來實現特定的操作,只需要向計算機輸入一個句子或檔案,就能按照預定的文法規則來對句子或檔案進行解釋。例如,我們想要只輸入一個加法/減法表示式,它就能夠計算出表示式結果。例如輸入“1+2+3-4+1”時,將輸出計算結果為3。像C++,Java或C#都無法直接解釋類似這樣的字串,因此使用者必須自定義一套文法規則來實現對這些語句的解釋,即設計一個自定義語言。如果所基於的程式語言是面嚮物件語言,此時可以使用直譯器模式實現自定義語言。

直譯器模式(Interpreter) 學習難度:★★★★★ 使用頻率:★☆☆☆☆

一、格式化指令的需求背景

Background:M公司開發了一套簡單的基於字元介面的格式化指令,可以根據輸入的指令在字元介面輸出一些格式化內容,例如輸入“LOOP 2 PRINT 楊過 SPACE SPACE PRINT 小龍女 BREAK END PRINT 郭靖 SPACE SPACE PRINT 黃蓉”,將輸出以下結果:

其中,關鍵詞LOOP表示迴圈,後面的數字表示迴圈次數;PRINT表示列印,後面的字串表示列印的內容;SPACE表示空格;BREAK表示換行;END表示迴圈結束。每一個關鍵詞對應一條指令,計算機程式將根據關鍵詞執行相應的處理操作。

  M公司的開發人員分析之後,根據格式化指令中句子的組成,定義瞭如下文法規則:

expression ::= command*          // 表示式,一個表示式包含多條指令

command ::= loop | primitive           // 語句指令

loop ::= 'loop number' expression 'end'      // 迴圈指令,其中number為自然數

primitive ::= 'print string' | 'space' | 'byeak'    // 基本指令,其中string為字串  

二、直譯器模式概述

2.1 直譯器模式簡介

  直譯器模式是一種使用頻率相對較低但學習難度較大的設計模式,它主要用於描述如何使用面嚮物件語言構成一個簡單的語言直譯器。

直譯器(Interpreter)模式:定義一個語言的文法,並且建立一個直譯器來解釋該語言中的句子,這裡的“語言”是指使用規定格式和語法的程式碼。直譯器模式是一種行為型模式。

2.2 直譯器模式結構

  直譯器模式主要包含以下4個角色:

  (1)AbstractExpression(抽象表示式):宣告瞭抽象的解釋操作;

  (2)TerminalExpression(終結符表示式):抽象表示式的子類,實現了與文法中的終結符相關聯的解釋操作,在句中的每一個終結符都是該類的一個例項;

  (3)NonterminalExpression(非終結符表示式):抽象表示式的子類,實現了文法中非終結符的解釋操作,由於在非終結符表示式中可以包含終結符表示式,也可以繼續包含非終結符表示式,因此其解釋操作一般通過遞迴完成。

  (4)Context(環境類):又稱為上下文類,用於儲存直譯器之外的一些全域性資訊,通常它臨時儲存了需要解釋的語句。

三、格式化指令的具體實現

3.1 設計結構

  M公司根據文法規則,通過進一步分析,結合直譯器模式繪製瞭如下圖所示的結構圖:

  其中,Context充當環境類角色,Node充當抽象表示式角色,ExpressionNode、CommandNode和LoopCommandNode充當非終結符表示式角色,PrimitiveCommandNode充當終結符表示式角色。

3.2 程式碼實現

  (1)環境類:Context

    /// <summary>
    /// 環境類:用於儲存和操作需要解釋的語句,
    /// 在本例項中每一個需要解釋的單詞都可以稱為一個動作標記(ActionToker)或命令
    /// </summary>
    public class Context
    {
        private int index = -1;
        private string[] tokens;
        private string currentToken;

        public Context(string text)
        {
            text = text.Replace("  ", " ");
            tokens = text.Split(' ');
            NextToken();
        }

        // 獲取下一個標記
        public string NextToken()
        {
            if (index < tokens.Length - 1)
            {
                currentToken = tokens[++index];
            }
            else
            {
                currentToken = null;
            }

            return currentToken;
        }

        // 返回當前的標記
        public string GetCurrentToken()
        {
            return currentToken;
        }

        // 跳過一個標記
        public void SkipToken(string token)
        {
            if (!token.Equals(currentToken, StringComparison.OrdinalIgnoreCase))
            {
                Console.WriteLine("錯誤提示:{0} 解釋錯誤!", currentToken);
            }

            NextToken();
        }

        // 如果當前的標記是一個數字,則返回對應的數值
        public int GetCurrentNumber()
        {
            int number = 0;
            try
            {
                // 將字串轉換為整數
                number = Convert.ToInt32(currentToken);
            }
            catch (Exception ex)
            {
                Console.WriteLine("錯誤提示:{0}", ex.Message);
            }

            return number;
        }
    }

  (2)抽象表示式:Node

    /// <summary>
    /// 抽象表示式:抽象節點類
    /// </summary>
    public abstract class Node
    {
        // 宣告一個方法用於解釋語句
        public abstract void Interpret(Context context);
        // 宣告一個方法用於執行標記對應的命令
        public abstract void Execute();
    }

  (3)非終結符表示式:ExpressionNode、CommandNode和LoopCommandNode

    /// <summary>
    ///  非終結符表示式:表示式節點類
    /// </summary>
    public class ExpressionNode : Node
    {
        // 用於儲存多條命令的集合
        private IList<Node> nodeList = new List<Node>();

        public override void Interpret(Context context)
        {
            // 迴圈處理Context中的標記
            while (true)
            {
                // 如果已經沒有任何標記,則退出解釋
                if (context.GetCurrentToken() == null)
                {
                    break;
                }
                // 如果標記為END,則不解釋END並結束本次解釋過程,可以繼續之後的解釋
                else if (context.GetCurrentToken().Equals("END", StringComparison.OrdinalIgnoreCase))
                {
                    context.SkipToken("END");
                    break;
                }
                // 如果為其它標記,則解釋標記並加入命令集合
                else
                {
                    Node node = new CommandNode();
                    node.Interpret(context);
                    nodeList.Add(node);
                }
            }
        }

        // 迴圈執行命令集合中的每一條指令
        public override void Execute()
        {
            foreach (var node in nodeList)
            {
                node.Execute();
            }
        }
    }

    /// <summary>
    /// 非終結符表示式:語句命令節點類
    /// </summary>
    public class CommandNode : Node
    {
        private Node node;

        public override void Interpret(Context context)
        {
            // 處理LOOP指令
            if (context.GetCurrentToken().Equals("LOOP", StringComparison.OrdinalIgnoreCase))
            {
                node = new LoopCommand();
                node.Interpret(context);
            }
            // 處理其他指令
            else
            {
                node = new PrimitiveCommand();
                node.Interpret(context);
            }
        }

        public override void Execute()
        {
            node.Execute();
        }
    }

    /// <summary>
    /// 非終結符表示式:迴圈命令類
    /// </summary>
    public class LoopCommand : Node
    {
        // 迴圈次數
        private int number;
        // 迴圈語句中的表示式
        private Node commandNode;

        public override void Interpret(Context context)
        {
            context.SkipToken("LOOP");
            number = context.GetCurrentNumber();
            context.NextToken();
            // 迴圈語句中的表示式
            commandNode = new ExpressionNode();
            commandNode.Interpret(context);
        }

        public override void Execute()
        {
            for (int i = 0; i < number; i++)
            {
                commandNode.Execute();
            }
        }
    }

  (4)終結符表示式:PrimitiveCommandNode

    /// <summary>
    /// 終結符表示式:基本命令類
    /// </summary>
    public class PrimitiveCommand : Node
    {
        private string name;
        private string text;

        public override void Interpret(Context context)
        {
            name = context.GetCurrentToken();
            context.SkipToken(name);

            if (!name.Equals("PRINT", StringComparison.OrdinalIgnoreCase) 
                && !name.Equals("BREAK", StringComparison.OrdinalIgnoreCase)
                && !name.Equals("SPACE", StringComparison.OrdinalIgnoreCase))
            {
                Console.WriteLine("非法命令!");
            }

            if (name.Equals("PRINT", StringComparison.OrdinalIgnoreCase))
            {
                text = context.GetCurrentToken();
                context.NextToken();
            }
        }

        public override void Execute()
        {
            if (name.Equals("PRINT", StringComparison.OrdinalIgnoreCase))
            {
                Console.Write(text);
            }
            else if (name.Equals("SPACE", StringComparison.OrdinalIgnoreCase))
            {
                Console.Write(" ");
            }
            else if (name.Equals("BREAK", StringComparison.OrdinalIgnoreCase))
            {
                Console.Write("\r\n");
            }
        }
    }

  (5)客戶端測試:

    public class Program
    {
        public static void Main(string[] args)
        {
            string instruction = "LOOP 2 PRINT 楊過 SPACE SPACE PRINT 小龍女 BREAK END PRINT 郭靖 SPACE SPACE PRINT 黃蓉";
            Context context = new Context(instruction);

            Node node = new ExpressionNode();
            node.Interpret(context);

            Console.WriteLine("源指令 : {0}", instruction);
            Console.WriteLine("解釋後 : ");

            node.Execute();

            Console.ReadKey();
        }
    }

  編譯除錯後執行結果如下圖所示:

  

四、直譯器模式小結

4.1 主要優點

  (1)易於改變和擴充套件文法 => 通過繼承來改變或擴充套件

  (2)增加新的解釋表示式較為方便 => 只需對應新增一個新的終結符或非終結符表示式,原有程式碼無須修改,符合開閉原則!

4.2 主要缺點

  (1)對於複雜文法難以維護 => 一條規則一個類,如果太多文法規則,類的個數會劇增!

  (2)執行效率較低 => 使用了大量迴圈和遞迴,在解釋複雜句子時速度很慢!

4.3 應用場景

  (1)可以將一個需要解釋執行的語言中的句子表示為一個抽象語法樹

  (2)一些重複出現的問題可以用一種簡單的語言來進行表達

  (3)一個語言的文法較為簡單

  (4)執行效率不是關鍵問題 => 高效的直譯器通常不是通過直接解釋抽象語法樹來實現的

參考資料

  DesignPattern

  (1)劉偉,《設計模式的藝術—軟體開發人員內功修煉之道》

 

相關文章