設計模式的征途—23.解釋器(Interpreter)模式
雖然目前計算機編程語言有好幾百種,但有時人們還是希望用一些簡單的語言來實現特定的操作,只需要向計算機輸入一個句子或文件,就能按照預定的文法規則來對句子或文件進行解釋。例如,我們想要只輸入一個加法/減法表達式,它就能夠計算出表達式結果。例如輸入“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)執行效率不是關鍵問題 => 高效的解釋器通常不是通過直接解釋抽象語法樹來實現的
參考資料
(1)劉偉,《設計模式的藝術—軟件開發人員內功修煉之道》
作者:周旭龍
出處:http://edisonchou.cnblogs.com
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。
設計模式的征途—23.解釋器(Interpreter)模式