(二十四)直譯器模式詳解
作者:zuoxiaolong8810(左瀟龍),轉載請註明出處,特別說明:本博文來自博主原部落格,為保證新部落格中博文的完整性,特複製到此留存,如需轉載請註明新部落格地址即可。
直譯器模式是平時工作當中相對冷門的一個設計模式,也非常的難於理解,百度百科上的解釋也非常之少,只是簡單的介紹了一下,並且說了一句,可以參考正則表示式為一個實際的應用例子。
不過資料的匱乏並不能阻止我們對真理的探索,下面LZ先將百度百科上的定義以及解決的問題拔到這裡,方便各位觀看。
定義:給定一個語言,定義它的文法的一種表示,並定義一個直譯器,這個直譯器使用該表示來解釋語言中的句子。
使用場景:直譯器模式需要解決的是,如果一種特定型別的問題發生的頻率足夠高,那麼可能就值得將該問題的各個例項表述為一個簡單語言中的句子。這樣就可以構建一個直譯器,該直譯器通過解釋這些句子來解決該問題。
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、可以比較方便的修改和擴充套件文法規則。
相對於優點來說,它的缺點也非常明顯,那就是由於我們幾乎針對每一個規則都定義了一個類,所以如果一個文法的規則比較多,那對於文法的維護工作也會變得非常困難。
下面LZ將我們例子的類圖貼上來,各位參考一下。
針對這個類圖,LZ簡單的說兩點。
1、Client本來是關聯的Expression介面,不過由於中間加了個語法分析器(GrammerParser),所以變成了Client關聯語法分析器,語法分析器再關聯Expression介面。
2、由於我們採用反向計算的方式,所以非終結者表示式沒有到Expression介面的聚合線,而是由兩條關聯線代替了兩條聚合線。
直譯器模式的分享就到此結束了,各位只要大致瞭解一下即可,如果實在理解不了,也不必死摳這個模式。
學習設計模式,有時候就像追女生一樣,如果你怎麼追都追不上,那說明緣分未到,該放棄的時候就得放棄,說不定哪天緣分到了,不需要你追,你心中的她自然就上鉤了。所以如果有哪一個設計模式一時半會理解不了,請不要著急去理解它,不理解的原因是因為你的積累還不夠,如果霸王硬上弓的話,往往會被設計模式暴揍一頓,最後還得自己付醫藥費。
LZ只想說,何必呢?
到這篇文章為止,LZ已經將所有24種設計模式全部講解了一遍,其中有好有壞,有對有錯。不過不管怎樣,LZ本人的收穫還是很大的,也很感謝這當中支援LZ的猿友。設計模式系列或許還會有最後一篇,內容自然是對24種設計模式的總結,LZ最近也在為此而準備著,敬請各位猿友擦亮雙眼期待吧。
一個系列結束了,不代表LZ的學習之路結束了,在上一章已經提到過,LZ最近在研究虛擬機器原始碼,在接下來的時間裡,LZ或許會寫一些與虛擬機器相關的內容,如果有哪位猿友對虛擬機器有興趣的話,可以繼續關注下LZ。
感謝各位的收看,我們下次再見。
版權宣告
作者:zuoxiaolong(左瀟龍)
出處:部落格園左瀟龍的技術部落格--http://www.cnblogs.com/zuoxiaolong
您的支援是對博主最大的鼓勵,感謝您的認真閱讀。
本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。