在軟件開發中,會遇到有些問題多次重複出現,而且有一定的相似性和規律性。如果將它們歸納成一種簡單的語言,那麼這些問題實例將是該語言的一些句子,這樣就可以用“編譯原理”中的解釋器模式來實現了。
雖然使用解釋器模式的實例不是很多,但對於滿足以上特點,且對運行效率要求不是很高的應用實例,如果用解釋器模式來實現,其效果是非常好的,本文將介紹其工作原理與使用方法。
認識解釋器模式
解釋器(Interpreter)模式的定義:給分析對象定義一個語言,並定義該語言的文法表示,再設計一個解析器來解釋語言中的句子。也就是說,用編譯語言的方式來分析應用中的實例。這種模式實現了文法表達式處理的接口,該接口解釋一個特定的上下文。
這裏提到的文法和句子的概念同編譯原理中的描述相同,“文法”指語言的語法規則,而“句子”是語言集中的元素。例如,漢語中的句子有很多,“我是中國人”是其中的一個句子,可以用一棵語法樹來直觀地描述語言中的句子。
解釋器模式是一種類行爲型模式,其主要優點如下。
- 擴展性好。由於在解釋器模式中使用類來表示語言的文法規則,因此可以通過繼承等機制來改變或擴展文法。
- 容易實現。在語法樹中的每個表達式節點類都是相似的,所以實現其文法較爲容易。
解釋器模式的主要缺點如下。
- 執行效率較低。解釋器模式中通常使用大量的循環和遞歸調用,當要解釋的句子較複雜時,其運行速度很慢,且代碼的調試過程也比較麻煩。
- 會引起類膨脹。解釋器模式中的每條規則至少需要定義一個類,當包含的文法規則很多時,類的個數將急劇增加,導致系統難以管理與維護。
- 可應用的場景比較少。在軟件開發中,需要定義語言文法的應用實例非常少,所以這種模式很少被使用到。
原理類圖與角色說明
角色說明
- 抽象表達式(Abstract Expression)角色:定義解釋器的接口,約定解釋器的解釋操作,主要包含解釋方法 interpret()。
- 終結符表達式(Terminal Expression)角色:是抽象表達式的子類,用來實現文法中與終結符相關的操作,文法中的每一個終結符都有一個具體終結表達式與之相對應。
- 非終結符表達式(Nonterminal Expression)角色:也是抽象表達式的子類,用來實現文法中與非終結符相關的操作,文法中的每條規則都對應於一個非終結符表達式。
- 環境(Context)角色:通常包含各個解釋器需要的數據或是公共的功能,一般用來傳遞被所有解釋器共享的數據,後面的解釋器可以從這裏獲取這些值。
- 客戶端(Client):主要任務是將需要分析的句子或表達式轉換成使用解釋器對象描述的抽象語法樹,然後調用解釋器的解釋方法,當然也可以通過環境角色間接訪問解釋器的解釋方法。
模式的應用場景。
- 可以將一個需要解釋執行的語言中的句子表示爲一個抽象語法樹
- 一些重複出現的問題可以用一種簡單的語言來表達
- 一個簡單語法需要解釋的場景
這樣的例子還有,比如編譯器、運算表達式計算、正則表達式、機器人等
注意:解釋器模式在實際的軟件開發中使用比較少,因爲它會引起效率、性能以及維護等問題。如果碰到對錶達式的解釋,在 Java 中可以用 Expression4J 或 Jep 等來設計。
應用實例
四則運算
通過解釋器模式來實現四則運算, 如計算 a+b-c 的值
思路分析與圖解
代碼實現
創建抽象表達式(Abstract Expression)
//抽象類表達式,通過 HashMap 鍵值對, 可以獲取到變量的值
public abstract class Expression {
// a + b - c
// 解釋公式和數值, key 就是公式(表達式) 參數[a,b,c], value 就是就是具體值
// HashMap {a=10, b=20}
public abstract int interpreter(HashMap<String, Integer> var);
}
創建終結符表達式(Terminal Expression):變量解析器
//變量的解釋器
public class VarExpression extends Expression {
private String key;// key=a,key=b,key=c
public VarExpression(String key) {
this.key = key;
}
// var 就是{a=10, b=20}
// interpreter 根據 變量名稱,返回對應值
@Override
public int interpreter(HashMap<String, Integer> var) {
return var.get(key);
}
}
創建終結符表達式(Terminal Expression):運算符號解析器
/**
* 抽象運算符號解析器 這裏,每個運算符號,都只和自己左右兩個數字有關係,
* 但左右兩個數字有可能也是一個解析的結果,無論何種類型,都是 Expression 類的實現類
*/
public class SymbolExpression extends Expression {
protected Expression left;
protected Expression right;
public SymbolExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
//因爲 SymbolExpression 是讓其子類來實現,因此 interpreter 是一個默認實現
@Override
public int interpreter(HashMap<String, Integer> var) {
return 0;
}
}
//加法解析器
public class AddExpression extends SymbolExpression {
public AddExpression(Expression left, Expression right) {
super(left, right);
}
//求出 left 和 right 表達式相加後的結果
@Override
public int interpreter(HashMap<String, Integer> var) {
return super.left.interpreter(var) + super.right.interpreter(var);
}
}
//減法解析器
public class SubExpression extends SymbolExpression {
public SubExpression(Expression left,Expression right) {
super(left,right);
}
//求出 left 和 right 表達式相減後的結果
@Override
public int interpreter(HashMap<String, Integer> var) {
return super.left.interpreter(var) - super.right.interpreter(var);
}
}
Calcuator封裝類
Calcuator的作用是封裝,根據迪米特原則,Client只與直接的朋友Calcuator交流,與其他類沒關係。
public class Calculator {
// 定義表達式
private Expression expression;
// 構造函數傳參,並解析
public Calculator(String expStr) {
// 安排運算先後順序
Stack<Expression> stack = new Stack<>();
// 表達式拆分成字符數組
char[] charArray = expStr.toCharArray();// [a, +, b]
Expression left = null;
Expression right = null;
//遍歷我們的字符數組, 即遍歷 [a, +, b]
//針對不同的情況,做處理
for (int i = 0; i < charArray.length; i++) {
switch (charArray[i]) {
case '+':
left = stack.pop();// 從 stack 取 出 left => "a"
right = new VarExpression(String.valueOf(charArray[++i]));// 取出右表達式 "b"
stack.push(new AddExpression(left, right));// 然後根據得到 left 和 right 構建 AddExpresson 加入stack
break;
case '-':
left = stack.pop();// 從 stack 取 出 left => "a"
right = new VarExpression(String.valueOf(charArray[++i]));// 取出右表達式 "b"
stack.push(new SubExpression(left, right));// 然後根據得到 left 和 right 構建 SubExpresson 加入stack
break;
default:
//如果是一個 Var 就創建要給 VarExpression 對象,並 push 到 stack
stack.push(new VarExpression(String.valueOf(charArray[i])));
break;
}
}
//當遍歷完整個 charArray 數組後,stack 就得到最後 Expression
this.expression = stack.pop();
}
public int run(HashMap<String, Integer> var) {
//最後將表達式 a+b 和 var = {a=10,b=20}
//然後傳遞給 expression 的 interpreter 進行解釋執行
return this.expression.interpreter(var);
}
}
我們來分析一下,Calculator構造函數接收一個表達式,然後把表達式轉化爲char數組,並判斷運算符號,如果是“+”則進行加法運算,把左邊的數(left變量)和右邊的數(right變量)加起來就可以了,那左邊的數爲什麼是在堆棧中呢?例如這個公式:a+b-c,根據for循環,首先被壓入堆棧中的應該是有a元素生成的VarExpression對象,然後判斷到加號時,把a元素的對象VarExpression從堆棧中彈出,與右邊的數組b進行相加,b又是怎麼得來的呢?當前的數組遊標下移一個單元格即可,同時爲了防止該元素再次被遍歷,則通過++i的方式跳過下一個遍歷——於是一個加法的運行結束。減法也是相同的運行原理。
創建上下文與客戶端使用
public class Client {
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
String expStr = getExpStr(); // a+b
HashMap<String, Integer> var = getValue(expStr);// var {a=10, b=20}
Calculator cal = new Calculator(expStr);
System.out.println("運算結果:" + expStr + "=" + cal.run(var));
}
//獲得表達式
private static String getExpStr() throws IOException {
System.out.print("請輸入表達式:");
return (new BufferedReader(new InputStreamReader(System.in))).readLine();
}
// 獲得值映射
public static HashMap<String, Integer> getValue(String expStr) throws IOException {
HashMap<String, Integer> map = new HashMap<>();
for (char ch:expStr.toCharArray()) {
if (ch != '+' && ch != '-') {
if (!map.containsKey(String.valueOf(ch))) {
System.out.println("請輸入" + String.valueOf(ch) + "的值:");
String in = (new BufferedReader(new InputStreamReader(System.in))).readLine();
map.put(String.valueOf(ch),Integer.valueOf(in));
}
}
}
return map;
}
}
程序的運行結果
請輸入表達式:a+b-c
請輸入a的值:
2
請輸入b的值:
4
請輸入c的值:
6
運算結果:a+b-c=0