一、什么是解釋器模式
解釋器這個名詞想必大家都不會陌生,比如編譯原理中,一個算術表達式通過詞法分析器形成詞法單元,而后這些詞法單元再通過語法分析器構建語法分析樹,最終形成一顆抽象的語法分析樹。諸如此類的例子也有很多,比如編譯器、正則表達式等等。
如果一種特定類型的問題發生的頻率足夠高,那么可能就值得將該問題的各個實例表述為一個簡單語言中的句子,這樣就可以構建一個解釋器,該解釋器通過解釋這些句子來解決該問題。
就比如正則表達式,它就是解釋器模型的一種應用,解釋器為正則表達式定義了一個文法,如何表示一個特定的正則表達式,以及如何解釋這個正則表達式。
解釋器模式(Interpreter),給定一個語言,定義它的文法的一種表示,並定義一個解釋器,這個解釋器使用該表示來解釋語言中的句子。UML結構圖如下:
其中,Context是環境角色,包含解釋器之外的一些全局信息;AbstractExpression為抽象表達式,聲明一個抽象的解釋操作,這個接口為抽象語法樹中所有的節點所共享;TerminalExression為終結符表達式,實現與文法中的終結符相關聯的解釋操作;NonterminalExpression為非終結符表達式,為文法中的非終結符實現解釋操作,對文法中每一條規則R1、R2……Rn都需要一個具體的非終結符表達式類。
1. Context環境角色
1 public class Context { 2 3 private String input; 4 private String output; 5 6 public String getInput() { 7 return input; 8 } 9 public void setInput(String input) { 10 this.input = input; 11 } 12 public String getOutput() { 13 return output; 14 } 15 public void setOutput(String output) { 16 this.output = output; 17 } 18 19 }
2. 抽象表達式
抽象表達式是生成語法集合(語法樹)的關鍵,每個語法集合完成指定語法解析任務,它是通過遞歸調用的方式,最終由最小的語法單元進行解析完成。
1 public abstract class AbstractExpression { 2 public abstract void Interpret(Context context); 3 }
3. 終結符表達式
通常,終結符表達式比較簡單,主要處理場景元素和數據的轉換。
1 public class TerminalExpression extends AbstractExpression { 2 3 @Override 4 public void Interpret(Context context) { 5 System.out.println("終端解釋器"); 6 } 7 8 }
4. 非終結符表達式
每個非終結符表達式都代表了一個文法規則,並且每個文法規則都只關心自己周邊的文法規則的結果,因此這就產生了每個非終結符表達式調用自己周邊的非終結符表達式,然后最終、最小的文法規則就是終結符表達式。
1 public class NonterminalExpression extends AbstractExpression { 2 3 @Override 4 public void Interpret(Context context) { 5 System.out.println("非終端解釋器"); 6 } 7 8 }
5. Client客戶端
其中list為一個語法容器,容納一個具體的表達式。通常Client是一個封裝類,封裝的結果就是傳遞進來一個規范語法文件,解析器分析后產生結果並返回,避免了調用者與語法分析器的耦合關系。
1 public class Client { 2 3 public static void main(String[] args) { 4 Context context = new Context(); 5 List<AbstractExpression> list = new ArrayList<>(); 6 7 list.add(new TerminalExpression()); 8 list.add(new NonterminalExpression()); 9 list.add(new TerminalExpression()); 10 list.add(new TerminalExpression()); 11 12 for (AbstractExpression abstractExpression : list) { 13 abstractExpression.Interpret(context); 14 } 15 } 16 17 }
運行結果如下:
二、解釋器模式的應用
1. 何時使用
- 當有一個語言需要解釋執行,並且你可將該語言中的句子表示為一個抽象語法樹時
2. 方法
- 構建語法樹,定義終結符與非終結符
3. 優點
- 可擴展性好
4. 缺點
- 解釋器模式會引起類膨脹
- 解釋器模式采用遞歸調用方法,將會導致調試非常復雜
- 使用了大量的循環和遞歸,效率是一個不容忽視的問題
5. 使用場景
- 可以將一個需要解釋執行的語言中的句子表示為一個抽象語法樹
- 一些重復出現的問題可以用一種簡單的語言來表達
- 一個簡單語法需要解釋的場景
6. 應用實例
- 編譯器
- 運算表達式計算、正則表達式
- 機器人
7. 注意事項
- 盡量不要在重要的模塊中使用解釋器模式,否則維護會是一個很大的問題
三、解釋器模式的實現
我們現在通過解釋器模式來實現四則運算,如計算a+b的值。UML圖如下:
1. 解析器封裝類
使用Calculator構造函數傳參,並解析封裝。這里根據棧的“先進后出”來安排運算的先后順序(主要用在乘除法,這里只寫了加減法比較簡單)。以加法為例,Calculator構造函數接收一個表達式,然后把表達式轉換為char數組,並判斷運算符號,如果是‘+’則進行加法運算,把左邊的數(left變量)和右邊的數(right變量)加起來即可。
例如a+b-c這個表達式,根據for循環,首先被壓入棧中的是a元素生成的VarExpression對象,然后判斷到加號時,把a元素的對象從棧中pop出來,與右邊的數組b進行相加,而b是通過當前的數組游標下移一個單元格得來的(為了防止該元素被再次遍歷,通過++i的方式跳過下一遍歷)。減法運算同理。
1 public class Calculator { 2 3 //定義表達式 4 private Expression expression; 5 6 //構造函數傳參,並解析 7 public Calculator(String expStr) { 8 //安排運算先后順序 9 Stack<Expression> stack = new Stack<>(); 10 //表達式拆分為字符數組 11 char[] charArray = expStr.toCharArray(); 12 13 Expression left = null; 14 Expression right = null; 15 for(int i=0; i<charArray.length; i++) { 16 switch (charArray[i]) { 17 case '+': //加法 18 left = stack.pop(); 19 right = new VarExpression(String.valueOf(charArray[++i])); 20 stack.push(new AddExpression(left, right)); 21 break; 22 case '-': //減法 23 left = stack.pop(); 24 right = new VarExpression(String.valueOf(charArray[++i])); 25 stack.push(new SubExpression(left, right)); 26 break; 27 default: //公式中的變量 28 stack.push(new VarExpression(String.valueOf(charArray[i]))); 29 break; 30 } 31 } 32 this.expression = stack.pop(); 33 } 34 35 //計算 36 public int run(HashMap<String, Integer> var) { 37 return this.expression.interpreter(var); 38 } 39 40 }
2. 抽象表達式類
通過Map鍵值對,使鍵對應公式參數,如a、b、c等,值為運算時取得的具體數值。
1 public abstract class Expression { 2 3 //解析公式和數值,key是公式中的參數,value是具體的數值 4 public abstract int interpreter(HashMap<String, Integer> var); 5 6 }
3. 變量解析器
通過interpreter()方法從map中取之。
1 public class VarExpression extends Expression { 2 3 private String key; 4 5 public VarExpression(String key) { 6 this.key = key; 7 } 8 9 @Override 10 public int interpreter(HashMap<String, Integer> var) { 11 return var.get(this.key); 12 } 13 14 }
4. 抽象運算符號解析器
這里,每個運算符合都只和自己左右兩個數字有關系,但左右兩個數字有可能也是一個解析的結果,無論何種類型,都是Expression類的實現類。
1 public class SymbolExpression extends Expression { 2 3 protected Expression left; 4 protected Expression right; 5 6 public SymbolExpression(Expression left, Expression right) { 7 this.left = left; 8 this.right = right; 9 } 10 11 @Override 12 public int interpreter(HashMap<String, Integer> var) { 13 // TODO Auto-generated method stub 14 return 0; 15 } 16 17 }
5. 加法解析器
1 public class AddExpression extends SymbolExpression { 2 3 public AddExpression(Expression left, Expression right) { 4 super(left, right); 5 } 6 7 public int interpreter(HashMap<String, Integer> var) { 8 return super.left.interpreter(var) + super.right.interpreter(var); 9 } 10 11 }
6. 減法解析器
1 public class SubExpression extends SymbolExpression { 2 3 public SubExpression(Expression left, Expression right) { 4 super(left, right); 5 } 6 7 public int interpreter(HashMap<String, Integer> var) { 8 return super.left.interpreter(var) - super.right.interpreter(var); 9 } 10 11 }
7. Client客戶端
這里就比較簡單了,通過getExpStr()方法獲取表達式,再通過getValue()方法獲取值的映射,最后再實例化Calculator類,通過run()方法獲取最終的運算結果。
1 public class Client { 2 3 public static void main(String[] args) throws IOException { 4 String expStr = getExpStr(); 5 HashMap<String, Integer> var = getValue(expStr); 6 Calculator calculator = new Calculator(expStr); 7 System.out.println("運算結果:" + expStr + "=" + calculator.run(var)); 8 } 9 10 //獲得表達式 11 public static String getExpStr() throws IOException { 12 System.out.print("請輸入表達式:"); 13 return (new BufferedReader(new InputStreamReader(System.in))).readLine(); 14 } 15 16 //獲得值映射 17 public static HashMap<String, Integer> getValue(String expStr) throws IOException { 18 HashMap<String, Integer> map = new HashMap<>(); 19 20 for(char ch : expStr.toCharArray()) { 21 if(ch != '+' && ch != '-' ) { 22 if(! map.containsKey(String.valueOf(ch))) { 23 System.out.print("請輸入" + String.valueOf(ch) + "的值:"); 24 String in = (new BufferedReader(new InputStreamReader(System.in))).readLine(); 25 map.put(String.valueOf(ch), Integer.valueOf(in)); 26 } 27 } 28 } 29 30 return map; 31 } 32 33 }
運算結果如下: