作者:zuoxiaolong8810(左瀟龍),轉載請注明出處,特別說明:本博文來自博主原博客,為保證新博客中博文的完整性,特復制到此留存,如需轉載請注明新博客地址即可。
解釋器模式是平時工作當中相對冷門的一個設計模式,也非常的難於理解,百度百科上的解釋也非常之少,只是簡單的介紹了一下,並且說了一句,可以參考正則表達式為一個實際的應用例子。
不過資料的匱乏並不能阻止我們對真理的探索,下面LZ先將百度百科上的定義以及解決的問題拔到這里,方便各位觀看。
定義:給定一個語言,定義它的文法的一種表示,並定義一個解釋器,這個解釋器使用該表示來解釋語言中的句子。
使用場景:解釋器模式需要解決的是,如果一種特定類型的問題發生的頻率足夠高,那么可能就值得將該問題的各個實例表述為一個簡單語言中的句子。這樣就可以構建一個解釋器,該解釋器通過解釋這些句子來解決該問題。
LZ先給各位解釋一下定義當中所提到的文法。文法也稱為語法,指的是語言的結構方式。包括詞的構成和變化,詞組和句子的組織。對於文法來說,我們可以簡單的理解為一種語言的規則,那么從解釋器模式的定義可以看出,首先我們要先設計一種語言,然后給出語言的文法的表示,而在此基礎上,我們采用解釋器模式去解釋語言中的句子。
要想徹底的理解解釋器模式,LZ必須要先普及一下文法的定義,請各位暫且忍受住枯燥的理論知識,后面LZ會將這些理論用各位熟悉的代碼詮釋一遍。
首先我們來討論一下上下文無關文法的組成,有四種組成部分。
1,非終結符號集(LZ標注:像JAVA語言中的表達式,程序語句,標識符等)
2,終結符號集(LZ標注:類似JAVA語言中的+,-,*,\,=等)
3,產生式集合,也可以稱為規則集合(LZ標注:假設我們記JAVA中的標識符為id,那么下面這句話可以被成視為一條規則 id->a|b...|z|0..|9|_,其中|是或者的意思)
4,一個起始符號,這個符號是非終結符號集的一個元素(LZ標注:JAVA語言使用CompilationUnit(編譯單元)作為起始符號。)
上面所說的定義有些抽象,所以LZ在后面加了一些標注,那么上下文無關文法的作用是什么呢?
它可以生成一組由文法導出的語句,這些語句可以根據文法的產生式進行分析,下面LZ給一個《編譯原理》一書中的簡單例子,為了方便理解,LZ將符號稍微更改了一下。
假設有一上下文無關文法如下:
arithmetic -> arithmetic + number | arithmetic - number | number
number -> 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
我們根據這個文法可以得到所有個位數的加減表達式,比如對於 9 + 2 - 1 ,我們可以通過以下步驟推導出來。
arithmetic - >arithmetic - number -> arithmetic + number - number -> number + number - number -> 9 + number -number -> 9 + 2 - number -> 9 + 2 - 1
對於文法來說,一個語句如果能夠按照產生式推導出該語句,就稱該語句是符合文法的,所以9 + 2 - 1是符合上述文法的一個語句。
在這個文法當中,其中非終結者符號是 arithmetic 和 number, 而終結者符號是 0 - 9 、-、+ 。
我們從文法中可以得知由該文法組成的語句有以下規則。
1、operator的右邊必須是一個number。
2、operator的左邊必須是一個arithmetic。
3、arithmetic的最右邊一定是一個number。
4、由2和3,operator的左邊必須是number。
5、由4,number的右邊必須是空或者operator。
6、number只能是 0 和 1 - 9 的正整數。
7、operator只能是 - 和 + 。
針對這個文法,我們可以寫一個解釋器,去計算表達式的結果,而這個解釋器就可以使用解釋器模式編寫。而在編寫的過程中,我們需要驗證以上的規則,如果違反了規則,則表達式是非法的。為了便於使用程序語言表示,我們只驗證以上的后四條規則,這也是由原本的產生式推算出來的規則。
我們先來看下解釋器模式的類圖,引自《大話設計模式》。
可以看到類圖中有四個角色,抽象表達式(AbstractExpression)、終結符表達式(TerminalExpression)、非終結符表達式(NonterminalExpression)以及上下文(Context)。
四個角色所負責的任務在類圖中已有解釋,LZ這里不再重復,這里要說的是,這里具體的表達式類個數是不定的。
換句話說,終結符表達式(TerminalExpression)和非終結符表達式(NonterminalExpression)的個數都是根據文法需要而定的,並非是一成不變。
下面我們就使用上述的解釋器模式的結構去寫一個解釋器,用於解釋上面的加減表達式,首先我們先寫一個上下文,它記錄了一些全局信息,提供給表達式類使用,如下。
package com.interpreter; import java.util.ArrayList; import java.util.List; //上下文 public class Context { private int result;//結果 private int index;//當前位置 private int mark;//標志位 private char[] inputChars;//輸入的字符數組 private List<Integer> operateNumbers = new ArrayList<Integer>(2);//操作數 private char operator;//運算符 public Context(char[] inputChars) { super(); this.inputChars = inputChars; } public int getResult() { return result; } public void setResult(int result) { this.result = result; } public boolean hasNext(){ return index != inputChars.length; } public char next() { return inputChars[index++]; } public char current(){ return inputChars[index]; } public List<Integer> getOperateNumbers() { return operateNumbers; } public void setLeftOperateNumber(int operateNumber) { this.operateNumbers.add(0, operateNumber); } public void setRightOperateNumber(int operateNumber) { this.operateNumbers.add(1, operateNumber); } public char getOperator() { return operator; } public void setOperator(char operator) { this.operator = operator; } public void mark(){ mark = index; } public void reset(){ index = mark; } }
上下文的各個屬性,都是表達式在計算過程中需要使用的,也就是類圖中所說的全局信息,其中的操作數和運算符是模擬的計算機中寄存器加減指令的執行方式。下面我們給出抽象的表達式,它只是定義一個解釋操作。
package com.interpreter; //抽象表達式,定義一個解釋操作 public interface Expression { void interpreter(Context context); }
下面便是最重要的四個具體表達式了,這其中對應於上面文法提到的終結符和非終結符,如下。
package com.interpreter; //算數表達式(非終結符表達式,對應arithmetic) public class ArithmeticExpression implements Expression { public void interpreter(Context context) { context.setResult(getResult(context));//計算結果 context.getOperateNumbers().clear();//清空操作數 context.setLeftOperateNumber(context.getResult());//將結果壓入左操作數 } private int getResult(Context context){ int result = 0; switch (context.getOperator()) { case '+': result = context.getOperateNumbers().get(0) + context.getOperateNumbers().get(1); break; case '-': result = context.getOperateNumbers().get(0) - context.getOperateNumbers().get(1); break; default: break; } return result; } }
package com.interpreter; //非終結符表達式,對應number public class NumberExpression implements Expression{ public void interpreter(Context context) { //設置操作數 Integer operateNumber = Integer.valueOf(String.valueOf(context.current())); if (context.getOperateNumbers().size() == 0) { context.setLeftOperateNumber(operateNumber); context.setResult(operateNumber); }else { context.setRightOperateNumber(operateNumber); Expression expression = new ArithmeticExpression();//轉換成算數表達式 expression.interpreter(context); } } }
package com.interpreter; //終結符表達式,對應-、+ public class OperatorExpression implements Expression{ public void interpreter(Context context) { context.setOperator(context.current());//設置運算符 } }
package com.interpreter; //終結符表達式,對應0、1、2、3、4、5、6、7、8、9 public class DigitExpression implements Expression{ public void interpreter(Context context) { Expression expression = new NumberExpression();//如果是數字,則直接轉為number表達式 expression.interpreter(context); } }
這四個類就是簡單的解釋操作,值得一提的就是其中的兩次轉換,這個在稍后LZ會解釋一下。
下面本來該是客戶端程序了,不過由於我們的例子較為復雜,客戶端的代碼會比較臃腫,所以LZ抽出了一個語法分析類,分擔了一些客戶端的任務,在標准解釋器模式的類圖中是沒有這個類的。
各位可以把它的代碼想象成在客戶端里面就好,這並不影響各位理解解釋器模式本身,語法分析器的代碼如下。
package com.interpreter; //語法解析器(如果按照解釋器模式的設計,這些代碼應該是在客戶端,為了更加清晰,我們添加一個語法解析器) public class GrammarParser { //語法解析 public void parse(Context context) throws Exception{ while (context.hasNext()) { Expression expression = null; switch (context.current()) { case '+': case '-': checkGrammar(context); expression = new OperatorExpression(); break; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': context.mark(); checkGrammar(context, context.current()); context.reset(); expression = new DigitExpression(); break; default: throw new RuntimeException("語法錯誤!");//無效符號 } expression.interpreter(context); context.next(); } } //檢查語法 private void checkGrammar(Context context,char current){ context.next(); if (context.hasNext() && context.current() != '+' && context.current() != '-') { throw new RuntimeException("語法錯誤!");//第5條 } try { Integer.valueOf(String.valueOf(current)); } catch (Exception e) { throw new RuntimeException("語法錯誤!");//第6條 } } //檢查語法 private void checkGrammar(Context context){ if (context.getOperateNumbers().size() == 0) {//第4條 throw new RuntimeException("語法錯誤!"); } if (context.current() != '+' && context.current() != '-') {//第7條 throw new RuntimeException("語法錯誤!"); } } }
可以看到,我們的語法分析器不僅做了簡單的分析語句,從而得出相應表達式的工作,還做了一個工作,就是語法的正確性檢查。
下面我們寫個客戶端去計算幾個表達式試一下。
package com.interpreter; import java.util.ArrayList; import java.util.List; public class Client { public static void main(String[] args) { List<String> inputList = new ArrayList<String>(); //三個正確的,三個錯誤的 inputList.add("1+2+3+4+5+6+7+8+9"); inputList.add("1-2+3-4+5-6+7-8+9"); inputList.add("9"); inputList.add("-1+2+3+5"); inputList.add("1*2"); inputList.add("11+2+3+9"); GrammarParser grammarParser = new GrammarParser();//語法分析器 for (String input : inputList) { Context context = new Context(input.toCharArray()); try { grammarParser.parse(context);//語法分析器會調用解釋器解釋表達式 System.out.println(input + "=" + context.getResult()); } catch (Exception e) { System.out.println("語法錯誤,請輸入正確的表達式!"); } } } }
輸出結果:
1+2+3+4+5+6+7+8+9=45
1-2+3-4+5-6+7-8+9=5
9=9
語法錯誤,請輸入正確的表達式!
語法錯誤,請輸入正確的表達式!
語法錯誤,請輸入正確的表達式!
可以看到,前三個表達式是符合我們的文法規則的,而后三個都不符合規則,所以提示了錯誤,這樣的結果,與我們文法所表述的規則是相符的。
LZ需要提示的是,這里面本來是客戶端使用解釋器來解釋語句的,不過由於我們抽離出了語法分析器,所以由語法分析器調用解釋器來解釋語句,這消除了客戶端對解釋器的關聯,與標准類圖不符,不過這其實只是我們所做的簡單的改善而已,並不影響解釋器模式的結構。
另外,上面的例子當中,還有兩點是LZ要提一下的。LZ為了方便理解,已經盡量的將例子簡化,不過其中有兩個地方的轉換是值得注意的。
1、一個是操作數滿足條件時,會產生一個ArithmeticExpression表達式。
2、另外一個是從DigitExpression直接轉換成NumberExpression的地方,這其實和第1點一樣,都是對文法規則的使用,不過這個更加清晰。我們可以清楚的看到,0-9的數字或者說DigitExpression只對應唯一一種方式的非終結者符號,就是number,所以我們直接轉換成NumberExpression。
不過我們的轉換是由終結者符號反向轉換成非終結者符號的順序,也就是相當於從抽象語法樹的低端向上轉換的順序。其實相當於LZ省去了抽象語法樹的潛在構建過程,直接開始解釋表達式。
我們看上面的類圖中,非終結者表達式有一條到抽象表達式的聚合線,那其實是將非終結者表達式按照產生式分解的過程,這會是一個遞歸的過程,而我們省去了這一步,直接采用反向計算的方式。
然后再說說我們的語法分析器,它的工作就是將終結者符號對應上對應的表達式,可以看到它里面的swich結構就是用來選取表達式的。實際當中,我們當然不會寫這么糟糕的swich結構,我們可以使用很多方式優化它。當然,語法分析器的另外一個工作就是檢查語法的正確性,這點可以從兩個check方法明顯的看到。
不過很遺憾,在日常工作當中,我們使用到解釋器模式的概率幾乎為0,因為寫一個解釋器就基本相當於創造了一種語言,這對於大多數人來說,是幾乎不可能接到的工作。不過我們了解一下解釋器模式,還是對我們有好處的。
前面已經提到過解釋器模式適用的場景,我們這里結合上面的例子總結一下解釋器模式的優點:
1、由於我們使用具體的終止符和非終止符去解釋文法,所以會比較易於編寫。
2、可以比較方便的修改和擴展文法規則。
相對於優點來說,它的缺點也非常明顯,那就是由於我們幾乎針對每一個規則都定義了一個類,所以如果一個文法的規則比較多,那對於文法的維護工作也會變得非常困難。
下面LZ將我們例子的類圖貼上來,各位參考一下。
針對這個類圖,LZ簡單的說兩點。
1、Client本來是關聯的Expression接口,不過由於中間加了個語法分析器(GrammerParser),所以變成了Client關聯語法分析器,語法分析器再關聯Expression接口。
2、由於我們采用反向計算的方式,所以非終結者表達式沒有到Expression接口的聚合線,而是由兩條關聯線代替了兩條聚合線。
解釋器模式的分享就到此結束了,各位只要大致了解一下即可,如果實在理解不了,也不必死摳這個模式。
學習設計模式,有時候就像追女生一樣,如果你怎么追都追不上,那說明緣分未到,該放棄的時候就得放棄,說不定哪天緣分到了,不需要你追,你心中的她自然就上鈎了。所以如果有哪一個設計模式一時半會理解不了,請不要着急去理解它,不理解的原因是因為你的積累還不夠,如果霸王硬上弓的話,往往會被設計模式暴揍一頓,最后還得自己付醫葯費。
LZ只想說,何必呢?
到這篇文章為止,LZ已經將所有24種設計模式全部講解了一遍,其中有好有壞,有對有錯。不過不管怎樣,LZ本人的收獲還是很大的,也很感謝這當中支持LZ的猿友。設計模式系列或許還會有最后一篇,內容自然是對24種設計模式的總結,LZ最近也在為此而准備着,敬請各位猿友擦亮雙眼期待吧。
一個系列結束了,不代表LZ的學習之路結束了,在上一章已經提到過,LZ最近在研究虛擬機源碼,在接下來的時間里,LZ或許會寫一些與虛擬機相關的內容,如果有哪位猿友對虛擬機有興趣的話,可以繼續關注下LZ。
感謝各位的收看,我們下次再見。