二十四:解释器模式
定義:給定一個語言,定義它的文法的一種表示,并定義一個解釋器,這個解釋器使用該表示來解釋語言中的句子。
? ? ? ? ? ? ? ? ?使用場景:解釋器模式需要解決的是,如果一種特定類型的問題發生的頻率足夠高,那么可能就值得將該問題的各個實例表述為一個簡單語言中的句子。這樣就可以構建一個解釋器,該解釋器通過解釋這些句子來解決該問題。
? ? ? ? ? ? ? ? ?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、可以比較方便的修改和擴展文法規則。
? ? ? ? ? ? ? ? ?相對于優點來說,它的缺點也非常明顯,那就是由于我們幾乎針對每一個規則都定義了一個類,所以如果一個文法的規則比較多,那對于文法的維護工作也會變得非常困難。
轉載于:https://www.cnblogs.com/2019lgg/p/11103705.html
總結
- 上一篇: 张宴的博客
- 下一篇: 2011年5月19日盘后分析:把握行情运