1. 解釋器模式的概念
解釋器模式(interpreter),給定一個語言,定義它的文法的一種表示,并定義一個解釋器,這個解釋器使用該表示來解釋語言中的句子。
解釋器模式需要解決的是,如果一種特定類型的問題發生的頻率足夠高,那麼可能就值得将該問題的各個執行個體表述為一個簡單語言中的句子。這樣就可以建構一個解釋器,該解釋器通過解釋這些句子來解決各種問題。
舉個例子,正規表達式就是解釋器模式的一種運用,在沒有出現正規表達式之前,我們在 Email、電話這些場景中需要去判斷字元串是否合法,我們會分别對他們編寫特定的函數,這樣的話Email一套、電話一套、賬号一套、密碼一套…
如果我要比對的字元串特别多,這樣做未免太過耗時費力,那如何将他們全部統一起來呢?那就是寫一種無論在什麼情況,隻要我們規定好字元串規則,它就能進行比對判斷的表達式,正規表達式就這樣發明出來了。而解釋器模式就是為正規表達式定義了一個文法,如何表示一個特定的正規表達式,以及如何去解釋這個正規表達式。
再舉個例子,JVM的跨平台性,為什麼同一套Java代碼,在Window上、Linux上、MAC等平台都能被編譯運作呢?這是因為Java編譯器會将java檔案編譯成位元組碼檔案,
.class
檔案可以被JVM識别并執行,JVM就會将其轉換成作業系統識别的機器碼。
是以無論在什麼平台上,JVM隻認
.class
檔案。
其次,就算使用的不是Java語言,比如Python、JS,隻要你的代碼能被編譯器編譯成
.class
檔案,Java虛拟機就能解釋運作。
在這裡面JVM就是提供了解釋器的功能,它在向作業系統解釋程式員寫的代碼。
2. UML圖
-
AbstractExpression
抽象表達式
聲明一個抽象的解釋操作父類,并定義一個抽象的解釋方法,其具體的實作在各個具體的子類解釋器中完成
-
TerminalExpression
終結符表達式
實作文法中與終結符有關的解釋操作。文法中每一個終結符都有一個具體的終結表達式與之對應
-
NonterminalExpression
非終結表達式
實作文法中與非終結符有關的解釋操作
-
上下文環境Context
3. 實作一個簡單的解釋器
我們來實作一個對算術表達式的解釋,比如表達式“
m+n+p
”,如果我們使用解釋器模式對該表達式進行解釋,那麼代表數字m、n和p三個字母我們就可以看成是終結符号,而“+”這個算術運算符号則可當做是非終結符号。同樣地,我們先建立一個抽象解釋器表示數學運算:
public abstract class ArithmeticExpression{
/**
* 抽象的解釋方法
* 具體的解析邏輯由具體的子類實作
*
* @return 解析得到具體的值
*/
public abstract int interpret();
}
在該抽象解釋器的解釋方法interpet中,我們沒有像前面UML圖那樣使用一個Context對象作為Iinterpret方法的傳回類型,在本例中運算的結果都是作為參數傳回,是以,沒有必要使用額外的對象存儲資訊。
ArighmeticExpression
有兩個直接子類
NumExpression
和
OperatorExpression
,其中 NumExpression用于對數字進行解釋:
class NumExpression extends ArithmeticExpression{
private int num;
public NumExpression(int num) {
this.num = num;
}
@Override
public int interpret() {
return num;
}
}
代碼很簡單,邏輯也很明确,就不多說了,OperatorExpression依然是一個抽象類,其聲明兩個ArighmeticExpression類型的成員變量存儲運算符号兩邊的數字解釋器,這兩個成員變量會在構造方法中被指派。
public abstract class OperatorExpression extends ArithmeticExpression {
// 聲明兩個成員變量存儲運算符号兩邊的數字解釋器
protected ArithmeticExpression exp1,exp2;
public OperatorExpression(ArithmeticExpression exp1, ArithmeticExpression exp2) {
this.exp1 = exp1;
this.exp2 = exp2;
}
}
OperatorExpression也有一個直接子類
AdditionExpression
,就是用來對加法進行進行解釋:
public abstract class OperatorExpression extends ArithmeticExpression {
// 聲明兩個成員變量存儲運算符号兩邊的數字解釋器
protected ArithmeticExpression exp1,exp2;
public OperatorExpression(ArithmeticExpression exp1, ArithmeticExpression exp2) {
this.exp1 = exp1;
this.exp2 = exp2;
}
}
上面就是本例中所要使用到的所有解釋器。除此之外,我們建立一個Calculator類來處理一些相關的業務:
class Calculator {
// 聲明一個Stack棧存儲并操作所有相關的解釋器
private Stack<ArithmeticExpression> mExpStack = new Stack<>();
public Calculator(String expression) {
// 聲明兩個ArithmeticExpression類型的臨時變量,存儲運算符左右兩邊的數字解釋器
ArithmeticExpression exp1, exp2;
// 根據空格分割表達式字元串
String[] elements = expression.split(" ");
/**
* 循環周遊表達式元素數組
*/
for (int i = 0; i < elements.length; i++) {
/**
* 判斷運算符号
*/
if (elements[i].charAt(0) == '+') {// 如果是加号,則将解釋器彈出作為運算符号左邊的解釋器
exp1 = mExpStack.pop();
// 同時将運算符号數組下标下一個元素構造為一個數字解釋器
exp2 = new NumExpression(Integer.parseInt(elements[++i]));
// 通過上面的兩個數字解釋器構造加法運算解釋器
mExpStack.push(new AdditionExpression(exp1, exp2));
} else {// 如果為數字,則直接構造數字解釋器并壓入棧中
mExpStack.push(new NumExpression(Integer.parseInt(elements[i])));
}
}
}
/**
* 計算結果
*/
public int calculate() {
return mExpStack.pop().interpret();
}
}
這裡需要注意的是,為了簡化問題我們約定表達式字元串的每個元素必須使用空格間隔開,如“1 + 22 + 333 + 4444”這樣的表達式是合法的,而“1+22+333+4444”則不合法,是以,我們才能在Calculator的構造方法中通過空格來拆分字元串。
Calculator類的邏輯很好了解,這裡還是以“1 + 22 + 333 +4444”為例子,首先将其拆分成有7個元素組成的字元串數組,然後循環周遊,首先周遊到的元素為1,那麼将其作為參數構造一個 NumExpression對象壓入棧中,其次是加号運算符,此時我們将剛才壓入棧的由元素1作為參數夠着的NumExpression對象抛出作為加号運算符左邊的數字數字解釋器,将右邊的數字(下标+1)作為右邊的解釋器。
最後通過左右兩個數字解釋器作為參數夠着一個
AdditionExpression
加法解釋器對象壓入棧即可,這個過程其實就是在建構文法書,隻不過我們将其單獨的封裝在了一個類裡面而不是Client客戶類進行,最後,我們公布一個 calculate方法執行解釋并傳回結果,調用 calculate方法輸出結果即可。
最後是用戶端類
public class Main {
public static void main(String[] args) {
Calculator c = new Calculator("123 + 4512 + 123911");
System.out.println(c.calculate());
}
}
上面我們隻是簡單地對加法運算定義了解釋器,如果現在又想引入減法,就可以寫一個類似加法的減法解釋器即可,對拓展是保持開放的,但是需要修改
Calculate
類。
4. 小結
使用場景:
- 如果某個簡單的語言需要解釋執行而且可以将語言中的語句表示為一個抽象文法樹時,可以考慮使用解釋器模式
- 在某些特定的領域出現不斷重複的問題時,可以将該領域的問題轉化為一種文法規則下的語句,然後建構解釋器來解釋該語句