設計模式的征途—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)劉偉,《設計模式的藝術—軟體開發人員內功修煉之道》
作者:周旭龍
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連結。
相關推薦
設計模式的征途—23.直譯器(Interpreter)模式
雖然目前計算機程式語言有好幾百種,但有時人們還是希望用一些簡單的語言來實現特定的操作,只需要向計算機輸入一個句子或檔案,就能按照預定的文法規則來對句子或檔案進行解釋。例如,我們想要只輸入一個加法/減法表示式,它就能夠計算出表示式結果。例如輸入“1+2+3-4+1”時,將輸出計算結果為3。像C++,Java或C
PHP設計模式之直譯器(Interpreter)模式瞭解下
直譯器模式,它是什麼呢? 意思就是,給定一個語言, 定義它的文法的一種表示,並定義一個直譯器,該直譯器使用該表示來解釋語言中的句子,這是最實在的一種說法。 我們還可以理解為它是用於分析一個實體的關鍵元素,並且針對每個元素提供自己的解釋或相應動作。直譯器模式非常常用,比如P
設計模式的征途—23.解釋器(Interpreter)模式
args 參考資料 轉載 返回 下一個 tle title 缺點 images 雖然目前計算機編程語言有好幾百種,但有時人們還是希望用一些簡單的語言來實現特定的操作,只需要向計算機輸入一個句子或文件,就能按照預定的文法規則來對句子或文件進行解釋。例如,我們想要只輸入一個加法
設計模式的征途—5.原型(Prototype)模式
pla width 共享 太多的 isp text 一模一樣 軟件 集合 相信大多數的人都看過《西遊記》,對孫悟空拔毛變出小猴子的故事情節應該都很熟悉。孫悟空可以用猴毛根據自己的形象復制出很多跟自己一模一樣的小猴兵出來,其實在設計模式中也有一個類似的模式,我們可以通過一個原
設計模式的征途—16.訪問者(Visitor)模式
lose mar rtm image 3.1 conf 系統 .get 封裝性 在患者就醫時,醫生會根據病情開具處方單,很多醫院都會存在以下這個流程:劃價人員拿到處方單之後根據藥品名稱和數量計算總價,而藥房工作人員根據藥品名稱和數量準備藥品,如下圖所示。 在軟件開發中
設計模式的征途—18.策略(Strategy)模式
滿足 應用 基礎 blog title pla 生成 display 多個 俗話說條條大路通羅馬,很多情況下實現某個目標地途徑都不只一條。在軟件開發中,也會時常遇到這樣的情況,實現某一個功能有多條途徑,每一條途徑都對應一種算法。此時,可以使用一種設計模式來實現靈活地選擇解決
設計模式的征途—20.備忘錄(Memento)模式
行為 修煉之道 mda 3.2 ima 位置 pri 捕獲 spl 相信每個人都有後悔的時候,但是人生並無後悔藥,有些錯誤一旦發生就無法再挽回,有些事一旦錯過就不會再重來,有些話一旦說出口也就不可能再收回,這就是人生。為了不讓自己後悔,我們總是需要三思而後行。這裏我們要學習
設計模式的征途—10.裝飾(Decorator)模式
雖然目前房價依舊很高,就連我所在的成都郊區(非中心城區)的房價均價都早已破萬,但卻還是阻擋不了大家對新房的渴望和買房的熱情。如果大家買的是清水房,那麼無疑還有一項艱鉅的任務在等著大家,那就是裝修。對新房的裝修並沒有改變房屋用於居住的本質,但它可以讓房子變得更加漂亮和溫馨以及更加實用。在軟體設計中,也有一種類似
設計模式的征途—11.外觀(Facade)模式
在軟體開發中,有時候為了完成一項較為複雜的功能,一個類需要和多個其他業務類互動,而這些需要互動的業務類經常會作為一個完整的整體出現,由於涉及的類比較多,導致使用時程式碼較為複雜,此時,特別需要一個類似服務員一樣的角色,由他來負責和多個業務類進行互動,而使用這些業務類的類只需要和該類進行互動即可。外觀模式通過引
設計模式的征途—13.代理(Proxy)模式
所謂代購,簡單說來就是找人幫忙購買所需要的商品。代購分為兩種型別,一種是因為在當地買不到某件商品,又或者是因為當地這件商品的價格比其他地區的貴,因此託人在其他地區甚至國外購買該商品,然後通過快遞發貨或直接攜帶回來。另一種則是消費者對想要購買的商品相關資訊的缺乏,自己無法確定其實際價值,因此只好委託中介講價或購
設計模式的征途—19.命令(Command)模式
在生活中,我們裝修新房的最後幾道工序之一是安裝插座和開關,通過開關可以控制一些電器的開啟和關閉,例如電燈或換氣扇。在購買開關時,使用者並不知道它將來到底用於控制什麼電器,也就是說,開關與電燈、換氣扇並無直接關係,一個開關在安裝之後可能用來控制電燈,也可能用來控制換氣扇或者其他電器裝置。相同的開關可以通過不同的
設計模式的征途—7.介面卡(Adapter)模式
在現實生活中,我們的膝上型電腦的工作電壓大多數都是20V,而我國的家庭用電是220V,如何讓20V的膝上型電腦能夠工作在220V的電壓下工作?答案:引入一個電源介面卡,俗稱變壓器,有了這個電源介面卡,生活用電和膝上型電腦即可相容。 在軟體開發中,有時候也會存在這種不相容的情況,我們也可以像電源介面卡一樣引入
設計模式:裝飾器(Decorator)模式
讓我 分享圖片 底部 .com 一件事 輸出 PE 新的 int 設計模式:裝飾器(Decorator)模式 一、前言 裝飾器模式也是一種非常重要的模式,在Java以及程序設計中占據著重要的地位。比如Java的數據流處理,我們可能看到數據流經過不同的類的包裝和包裹,最
設計模式:觀察者(Observer)模式
image 強制轉換 trace vat PE sta obs observer -a 設計模式:觀察者(Observer)模式 一、前言 觀察者模式其實最好的名稱應該是“發布訂閱”模式,和我們現在大數據之中的發布訂閱方式比較類似,但是也有區別的地方,在上一個設計模式,
設計模式:享元(FlyWeight)模式
例子 清理 什麽 public == lean http 變量 -- 設計模式:享元(FlyWeight)模式 一、前言 享元(FlyWeight)模式顧名思義,既是輕量級的,原因就是享元,共享元素,這裏的元素指的是對象。如何共享對象,那就是在檢測對象產生的時候,如
Koffee設計模式學習之路(一) —— 模式學習總結思路
這篇部落格沒有相關技術細節,僅作為自己對設計模式這個東西的一點感悟和以後設計模式系列部落格的一個寫作思路。 作為非科班出身,誤打誤撞進入程式設計的人,在上研究生期間對於程式的唯一要求就是:能用。彼時,不知道有面向物件,記憶體管理,多執行緒,
設計模式之裝飾者(Decorator)模式
首先來看一個場景,如圖: 工人分為很多種類,比如電工,管道工等等,同時又有A公司的電工,B公司的電工,A公司的管道工,B公司的管道工等等,那麼當有M個工種和N個公司的時候,就會有 M * N 個子類,這個繼承體系就會變得很龐大和複雜。那麼如何簡化呢,那麼
【java設計模式】之 代理(Proxy)模式
代理模式的核心作用就是通過代理,控制對物件的訪問。這跟實際中是一樣的,比如說明星都有經紀人,這就是一個代理,比如有人要找某明星拍戲,那麼首先處理這事的是他的經紀人,雖然拍戲需要自己拍,但是拍戲前後的一些必須要做的事等等,都由這個經紀人來處理。 在程式中也是如此,通過
有關模式窗體和無(非)模式窗體的區別
最小化 調用方法 關閉 對話 一起 兩種模式 獲得 windows new 在客戶端和網頁的開發過程中,我們都會或多或少遇到過模式窗體和無(非)模式窗體(以下簡稱無模式窗體), 在傳統的Windows窗體開發過程中,對話框有兩種模式:模式窗體和無模式窗體。 模式
【Unity與23種設計模式】解釋器模式(Interpreter)
engine 位置 文本 腳本語言 包含 編輯 網頁設計 流行 程序設計 GoF中定義: “定義一個程序設計語言所需要的語句,並提供解釋來解析(執行)該語言。” 傳統上,執行程序代碼通常通過兩種方式 第一種:編譯程序 第二種:解釋器 常見的使用解釋器的程序設計語