解釋器模式

source:http://www.cnblogs.com/cbf4life/archive/2009/12/17/1626125.html

    在銀行、證券類項目中,經常會有一些模型運算,通過對現有數據的統計、分析而預測不可知或未來可能發生的商業行爲。模型運算大部分是針對海量數據的,例如建立一個模型公式,分析一個城市的消費傾向,進而影響銀行的營銷和業務擴張方向,一般的模型運算都有一個或多個運算公式,通常是加減乘除四則運算,偶爾也有指數、開方等複雜運算。具體到一個金融業務中,模型公式是非常複雜的,雖然只有加減乘除四則運算,但是公式有可能有十多個參數,而且上百個業務品各有不同的取參路徑,同時相關表的數據量都在百萬級,呵呵,複雜了吧,不復雜那就不叫金融業務,我們就來講講運算的核心——模型公式,如何實現。

      業務需求:輸入一個模型公式(加減四則運算),然後輸入模型中的參數,運算出結果。

      設計要求:

  • 公式可以運行期編輯,並且符合正常算術書寫方式,例如a+b-c;
  • 高擴展性,未來增加指數、開方、極限、求導等運算符號時,較少改動量;
  • 效率可以不用考慮,晚間批量運算。

      需求不復雜,若僅僅對數字採用四則運算,每個程序員都可以寫出來。但是增加了增加模型公式就複雜了。先解釋一下爲什麼需要公式, 而不採用直接計算的方法,例如有如下3個公式:

  • 業務種類1的公式:a+b+c-d;
  • 業務種類2的公式:a+b+e-d;
  • 業務種類3的公式:a-f。

      其中,a、b、c、d、e、f參數的值都可以取得,如果使用直接計算數值的方法需要爲每個品種寫一個算法,目前僅僅是3個業務種類,那上百個品種呢?歇菜了吧!建立公式,然後通過公式運算纔是王道。

      我們以實現加減算法(由於篇幅所限,乘除法的運算讀者可以自行擴展)的公式爲例,講解如何解析一個固定語法邏輯。由於使用語法解析的場景比較少,而且一些商業公司(比如SAS、SPSS等統計分析軟件)都支持類似的規則運算,親自編寫語法解析的工作已經非常少,以下例程採用逐步分析方法,帶領大家瞭解這一實現過程。

      我們來想,公式中有什麼?僅有兩類元素:運算元素和運算符號,運算元素就是指a、b、c等符號,需要具體賦值的對象,也叫做終結符號,爲什麼叫終結符號呢?因爲這些元素除了需要賦值外,不需要做任何處理,所有運算元素都對應一個具體的業務參數,這是語法中最小的單元邏輯,不可再拆分;運算符號就是加減符號,需要我們編寫算法進行處理,每個運算符號都要對應處理單元,否則公式無法運行,運算符號也叫做非終結符號。兩類元素的共同點是都要被解析,不同點是所有的運算元素具有相同的功能,可以用一個類表示,而運算符號則是需要分別進行解釋,加法需要加法解析器,減法也需要減法解析器。分析到這裏,我們就可以先畫一個簡單的類圖,如圖27-1所示。

clip_image002

圖27-1 初步分析加減法類圖

      很簡單的一個類圖,VarExpression用來解析運算元素,各個公式能運算元素的數量是不同的,但每個運算元素都對應一個VarExpression對象。SybmolExpression負責運算符號解析,由兩個子類AddExpression(負責加法運算)和SubExpression(負責減法運算)來實現。解析的工作完成了,我們還需要把安排運行的先後順序(加減法是不用考慮,但是乘除法呢?注意擴展性),並且還要返回結果,因此我們需要增加一個封裝類來進行封裝處理,由於我們只做運算,暫時還不與業務有關聯,定義爲Calculator類,分析到這裏,思路就比較清晰了,優化後加減法類圖如圖27-2所示。

clip_image004

圖27-2 優化後加減法類圖

      Calcuator的作用是封裝,根據迪米特原則,Client只與直接的朋友Calcuator交流,與其他類沒關係。整個類圖的結構也比較清晰,於是我們開始填充類圖中的方法,完整類圖如圖27-3所示。

clip_image006

圖27-3 完整加減法類圖

      類圖已經完成,我們來看代碼實現。Expression抽象類如代碼清單27-1所示。

代碼清單27-1 抽象表達式類

1
2
3
4
5
6
7
public abstract class Expression {
 
//解析公式和數值,其中var中的key值是是公式中的參數,value值是具體的數字
 
public abstract int interpreter(HashMap<String,Integer> var);
 
}

      抽象類非常簡單,僅一個方法interpreter負責對對傳遞進來的參數和值進行解析和匹配,其中輸入參數爲HashMap類型,key值爲模型中的參數,如a、b、c等,value爲運算時取得的具體數字。

變量的解析器如代碼清單27-2所示。

代碼清單27-2 變量解析器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class VarExpressionextends Expression {
 
private String key;
 
public VarExpression(String _key){
 
this.key = _key;
 
}
 
//從map中取之
 
public int interpreter(HashMap<String, Integer> var) {
 
return var.get(this.key);
 
}
 
}

      抽象運算符號解析器如代碼清單27-3所示。

代碼清單27-3 抽象運算符號解析器

public abstract class SymbolExpression extends Expression {

protected Expression left;

protected Expression right;

//所有的解析公式都應只關心自己左右兩個表達式的結果

public SymbolExpression(Expression _left,Expression _right){

this.left = _left;

this.right = _right;

}

}

      這個解析過程還是比較有意思的,每個運算符號都只和自己左右兩個數字有關係,但左右兩個數字有可能也是一個解析的結果,無論何種類型,都是Expression的實現類,於是在對運算符解析的子類中增加了一個構造函數,傳遞左右兩個表達式。具體的加、減法解析器如代碼清單27-4、27-5所示。

代碼清單27-4 加法解析器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class AddExpressionextends SymbolExpression {
 
public AddExpression(Expression _left,Expression _right){
 
super(_left,_right);
 
}
 
//把左右兩個表達式運算的結果加起來
 
public int interpreter(HashMap<String, Integer> var) {
 
return super.left.interpreter(var) + super.right.interpreter(var);
 
}
 
}
 
代碼清單27-5 減法解析器
 
public class SubExpressionextends SymbolExpression {
 
public SubExpression(Expression _left,Expression _right){
 
super(_left,_right);
 
}
 
//左右兩個表達式相減
 
public int interpreter(HashMap<String, Integer> var) {
 
return super.left.interpreter(var) - super.right.interpreter(var);
 
}
 
}

      解析器的開發工作已經完成了,但是需求還沒有完全實現,我們還需要對解析器進行封裝,封裝類Calculator如代碼清單27-6所示。

代碼清單27-6 解析器封裝類

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public class Calculator {
 
//定義的表達式
 
private Expression expression;
 
//構造函數傳參,並解析
 
public Calculator(String expStr){
 
//定義一個堆棧,安排運算的先後順序
 
Stack<Expression> stack = new Stack<Expression>();
 
//表達式拆分爲字符數組
 
char[] charArray = expStr.toCharArray();
 
//運算
 
Expression left = null;
 
Expression right = null;
 
for(int i=0;i<charArray.length;i++){
 
switch(charArray[i]) {
 
case '+'://加法
 
//加法結果放到堆棧中
 
left = stack.pop();
 
right = new VarExpression(String.valueOf(charArray[++i]));
 
stack.push(new AddExpression(left,right));
 
break;
 
case '-':
 
left = stack.pop();
 
right = new VarExpression(String.valueOf(charArray[++i]));
 
stack.push(new SubExpression(left,right));
 
break;
 
default://公式中的變量
 
stack.push(new VarExpression(String.valueOf(charArray[i])));
 
}
 
}
 
//把運算結果拋出來
 
this.expression = stack.pop();
 
}
 
//開始運算
 
public int run(HashMap<String,Integer> var){
 
return this.expression.interpreter(var);
 
}
 
}

      方法比較長,我們來分析一下,Calculator構造函數接收一個表達式,然後把表達式轉化爲char數組,並判斷運算符號,如果是“+”則進行加法運算,把左邊的數(left變量)和右邊的數(right變量)加起來就可以了,那左邊的數爲什麼是在堆棧中呢?例如這個公式:a+b-c,根據for循環,首先被壓入堆棧中的應該是有a元素生成的VarExpression對象,然後判斷到加號時,把a元素的對象VarExpression從堆棧中彈出,與右邊的數組b進行相加,b又是怎麼得來的呢?當前的數組遊標下移一個單元格即可,同時爲了防止該元素再次被遍歷,則通過++i的方式跳過下一個遍歷——於是一個加法的運行結束。減法也是相同的運行原理。

      爲了滿足業務要求,我們設置了一個Client類來模擬用戶情況,用戶要求可以擴展,可以修改公式,那就通過接收鍵盤事件來處理,Client類如代碼清單27-7所示。

代碼清單27-7 客戶模擬類

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class Client {
 
//運行四則運算
 
public static void main(String[] args) throws IOException{
 
String expStr = getExpStr();
 
//賦值
 
HashMap<String,Integer> var = getValue(expStr);
 
Calculator cal = new Calculator(expStr);
 
System.out.println("運算結果爲:"+expStr +"="+cal.run(var));
 
}
 
//獲得表達式
 
public static String getExpStr() throws IOException{
 
System.out.print("請輸入表達式:");
 
return (new BufferedReader(new InputStreamReader(System.in))).readLine();
 
}
 
//獲得值映射
 
public static HashMap<String,Integer> getValue(String exprStr) throws IOException{
 
HashMap<String,Integer> map = new HashMap<String,Integer>();
 
//解析有幾個參數要傳遞
 
for(char ch:exprStr.toCharArray()){
 
if(ch != '+' && ch != '-'){
 
//解決重複參數的問題
 
if(!map.containsKey(String.valueOf(ch))){ System.out.print("請輸入"+ch+"的值:");
 
String in = (new BufferedReader(new InputStreamReader(System.in))).readLine();
 
map.put(String.valueOf(ch),Integer.valueOf(in));
 
}
 
}
 
}
 
return map;
 
}
 
}

      其中,getExpStr是從鍵盤事件中獲得的表達式,getValue方法是從鍵盤事件中獲得表達式中的元素映射值,運行過程如下。

      首先,要求輸入公式。

請輸入表達式:a+b-c

      其次,要求輸入公式中的參數。

請輸入a的值:100

請輸入b的值:20

請輸入c的值:40

      最後,運行出結果。

運算結果爲:a+b-c=80

      看,要求輸入一個公式,然後輸入參數,運行結果出來了!那我們是不是可以修改公式?當然可以了,我們只要輸入公式,然後輸入相應的值就可以了,公式是在運行期定義的,而不是在運行前就制定好的,是不是類似於初中學過的“代數”這門課?先公式,然後賦值,運算出結果。

      需求已經開發完畢,公式可以自由定義,只要符合規則(有變量有運算符合)就可以運算出結果;若需要擴展也非常容易,只要增加SymbolExpression的子類就可以了 ,這就是解釋器模式。

27.2 解釋器模式的定義

      解釋器模式(Interpreter Pattern)是一種按照規定語法進行解析的方案,在現在項目中使用較少(誰沒事幹會去寫一個PHP或者Ruby的解析器),其定義如下:Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language。給定一個語言, 定義它的文法的一種表示,並定義一個解釋器,該解釋器使用該表示來解釋語言中的句子。

      解釋器模式的通用類圖如圖27-4所示。

clip_image008

圖27-4 解釋器模式通用類圖

  • AbstractExpression 抽象解釋器

      具體的解釋任務由各個實現類完成,具體的解釋器分別由TerminalExpression和NonterminalExpression完成。

  • TerminalExpression終結符表達式

      實現與文法中的元素相關聯的解釋操作,通常一個解釋器模式中只有一個終結符表達式,但有多個實例,對應不同的終結符。具體到我們例子就是VarExpression類,表達式中的每個終結符都在堆棧中產生了一個VarExpression對象。

  • NonterminalExpression 非終結符表達式

      文法中的每條規則對應於一個非終結表達式,具體到我們的例子就是加減法規則分別對應到AddExpression和SubExpression兩個類。非終結符表達式根據邏輯的複雜程度而增加,原則上每個文法規則都對應一個非終結符表達式。

  • Context 環境角色

      具體到我們的例子中是採用HashMap代替。

      解釋器是一個比較少用的模式,以下爲其通用源碼,可以作爲參考。抽象表達式通常只有一個方法,如代碼清單27-8所示。

代碼清單27-8 抽象表達式

1
2
3
4
5
6
7
public abstract class Expression {
 
//每個表達式必須有一個解析任務
 
public abstract Object interpreter(Context ctx);
 
}

      抽象表達式是生成語法集合(也叫做語法樹)的關鍵,每個語法集合完成指定語法解析任務,它是通過遞歸調用的方式,最終由最小的語法單元進行解析完成。終結符表達式如代碼清單27-9所示。

代碼清單27-9 終結符表達式

1
2
3
4
5
6
7
8
9
10
11
public class TerminalExpressionextends Expression {
 
//通常終結符表達式只有一個,但是有多個對象
 
public Object interpreter(Context ctx) {
 
return null;
 
}
 
}

      通常,終結符表達式比較簡單,主要是處理場景元素和數據的轉換。

非終結符表達式如代碼清單27-10所示。

代碼清單27-10 非終結符表達式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class NonterminalExpressionextends Expression {
 
//每個非終結符表達式都會對其他表達式產生依賴
 
public NonterminalExpression(Expression... expression){
 
}
 
public Object interpreter(Context ctx) {
 
//進行文法處理
 
return null;
 
}
 
}

      每個非終結符表達式都代表了一個文法規則,並且每個文法規則都只關心自己周邊的文法規則的結果(注意是結果),因此這就產生了每個非終結符表達式調用自己周邊的非終結符表達式,然後最終、最小的文法規則就是終結符表達式,終結符表達式的概念就是如此,不能夠再參與比自己更小的文法運算了。

客戶類如代碼清單27-11所示。

代碼清單27-11 客戶類

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Client {
 
public static void main(String[] args) {
 
Context ctx = new Context();
 
//通常定一個語法容器,容納一個具體的表達式,通常爲ListArray,LinkedList,Stack等類型
 
Stack<Expression> stack = null;
 
for(;;){
 
//進行語法判斷,併產生遞歸調用
 
}
 
//產生一個完整的語法樹,由各各個具體的語法分析進行解析
 
Expression exp = stack.pop();
 
//具體元素進入場景
 
exp.interpreter(ctx);
 
}
 
}

      通常Client是一個封裝類,封裝的結果就是傳遞進來一個規範語法文件,解析器分析後產生結果並返回,避免了調用者與語法解析器的耦合關係。

27.3 解釋器模式的應用

27.3.1 解釋器模式的優點

      解釋器是一個簡單語法分析工具,它最顯著的優點就是擴展性,修改語法規則只要修改相應的非終結符表達式就可以了,若擴展語法,則只要增加非終結符類就可以了。

27.3.2 解釋器模式的缺點
  • 解釋器模式會引起類膨脹

      每個語法都要產生一個非終結符表達式,語法規則比較複雜時,就可能產生大量的類文件,爲維護帶來了非常多的麻煩。

  • 解釋器模式採用遞歸調用方法

      每個非終結符表達式只關心與自己有關的表達式,每個表達式需要知道最終的結果,必須一層一層地剝繭,無論是面向過程的語言還是面向對象的語言,遞歸都是在必要條件下使用的,它導致調試非常複雜。想想看,如果要排查一個語法錯誤,我們是不是要一個一個斷點的調試下去,直到最小的語法單元。

  • 效率問題

      解釋器模式由於使用了大量的循環和遞歸,效率是個不容忽視的問題,特別是用於解析複雜、冗長的語法時,效率是難以忍受的。

27.3.3 解釋器模式使用的場景
  • 重複發生的問題可以使用解釋器模式

      例如,多個應用服務器,每天產生大量的日誌,需要對日誌文件進行分析處理,由於各個服務器的日誌格式不同,但是數據要素是相同的,按照解釋器的說法就是終結符表達式都是相同的,但是非終結符表達式就需要制定了。在這種情況下,可以通過程序來一勞永逸地解決該問題。

  • 一個簡單語法需要解釋的場景

      爲什麼是簡單?看看非終結表達式,文法規則越多,複雜度越高,而且類間還要進行遞歸調用(看看我們例子中的堆棧),不是一般地複雜。想想看,多個類之間的調用你需要什麼樣的耐心和信心去排查問題。因此,解釋器模式一般用來解析比較標準的字符集,例如SQL語法分析,不過該部分逐漸被專用工具所取代。

      在某些特用的商業環境下也會採用解釋器模式,我們剛剛的例子就是一個商業環境,而且現在模型運算的例子非常多,目前很多商業機構已經能夠提供出大量的數據進行分析。

27.3.4 解釋器模式的注意事項

      儘量不要在重要的模塊中使用解釋器模式,否則維護會是一個很大的問題。在項目中可以使用shell、JRuby、Groovy等腳本語言來代替解釋器模式,彌補Java編譯型語言的不足。我們在一個銀行的分析型項目中就採用JRuby進行運算處理,避免使用解釋器模式的四則運算,效率和性能各方面表現良好。

27.4 最佳實踐

      解釋器模式在實際的系統開發中使用的非常少,因爲它會引起效率、性能以及維護等問題,一般在大中型的框架型項目能夠找到它的身影,比如一些數據分析工具、報表設計工具、科學計算工具等等,若你確實遇到“一種特定類型的問題發生的頻率足夠高”的情況,準備使用解釋器模式時,可以考慮一下Expression4J、MESP(Math Expression String Parser)、Jep等開源的解析工具包(這三個開源產品都可以百度、Google中搜索到,請讀者自行查詢),功能都異常強大,而且非常容易使用,效率也還不錯,實現大多數的數學運算完全沒有問題,自己沒有必要從頭開始編寫解釋器,有人已經建立了一條康莊大道,何必再走自己的泥濘小路呢?

發佈了47 篇原創文章 · 獲贊 13 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章