一、什麼是直譯器模式
直譯器這個名詞想必大家都不會陌生,比如編譯原理中,一個算術表示式通過詞法分析器形成詞法單元,而後這些詞法單元再通過語法分析器構建語法分析樹,最終形成一顆抽象的語法分析樹。諸如此類的例子也有很多,比如編譯器、正規表示式等等。
如果一種特定型別的問題發生的頻率足夠高,那麼可能就值得將該問題的各個例項表述為一個簡單語言中的句子,這樣就可以構建一個直譯器,該直譯器通過解釋這些句子來解決該問題。
就比如正規表示式,它就是直譯器模型的一種應用,直譯器為正規表示式定義了一個文法,如何表示一個特定的正規表示式,以及如何解釋這個正規表示式。
直譯器模式(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 }
運算結果如下: