天天看點

設計模式--行為型模式--解釋器模式

在軟體開發中,會遇到有些問題多次重複出現,而且有一定的相似性和規律性。如果将它們歸納成一種簡單的語言,那麼這些問題執行個體将是該語言的一些句子,這樣就可以用“編譯原理”中的解釋器模式來實作了。

雖然使用解釋器模式的執行個體不是很多,但對于滿足以上特點,且對運作效率要求不是很高的應用執行個體,如果用解釋器模式來實作,其效果是非常好的,本文将介紹其工作原理與使用方法。

模式的定義與特點

解釋器(Interpreter)模式的定義:給分析對象定義一個語言,并定義該語言的文法表示,再設計一個解析器來解釋語言中的句子。也就是說,用編譯語言的方式來分析應用中的執行個體。這種模式實作了文法表達式處理的接口,該接口解釋一個特定的上下文。

這裡提到的文法和句子的概念同編譯原理中的描述相同,“文法”指語言的文法規則,而“句子”是語言集中的元素。例如,漢語中的句子有很多,“我是中國人”是其中的一個句子,可以用一棵文法樹來直覺地描述語言中的句子。

解釋器模式是一種類行為型模式,其主要優點如下。

  1. 擴充性好。由于在解釋器模式中使用類來表示語言的文法規則,是以可以通過繼承等機制來改變或擴充文法。
  2. 容易實作。在文法樹中的每個表達式節點類都是相似的,是以實作其文法較為容易。

解釋器模式的主要缺點如下。

  1. 執行效率較低。解釋器模式中通常使用大量的循環和遞歸調用,當要解釋的句子較複雜時,其運作速度很慢,且代碼的調試過程也比較麻煩。
  2. 會引起類膨脹。解釋器模式中的每條規則至少需要定義一個類,當包含的文法規則很多時,類的個數将急劇增加,導緻系統難以管理與維護。
  3. 可應用的場景比較少。在軟體開發中,需要定義語言文法的應用執行個體非常少,是以這種模式很少被使用到。

模式的結構與實作

解釋器模式常用于對簡單語言的編譯或分析執行個體中,為了掌握好它的結構與實作,必須先了解編譯原理中的“文法、句子、文法樹”等相關概念。

1) 文法

文法是用于描述語言的文法結構的形式規則。沒有規矩不成方圓,例如,有些人認為完美愛情的準則是“互相吸引、感情專一、任何一方都沒有戀愛經曆”,雖然最後一條準則較苛刻,但任何事情都要有規則,語言也一樣,不管它是機器語言還是自然語言,都有它自己的文法規則。例如,中文中的“句子”的文法如下。

〈句子〉::=〈主語〉〈謂語〉〈賓語〉
〈主語〉::=〈代詞〉|〈名詞〉
〈謂語〉::=〈動詞〉
〈賓語〉::=〈代詞〉|〈名詞〉
〈代詞〉你|我|他
〈名詞〉7大學生I筱霞I英語
〈動詞〉::=是|學習      

注:這裡的符号“::=”表示“定義為”的意思,用“〈”和“〉”包覆的是非終結符,沒有包覆的是終結符。

2) 句子

句子是語言的基本機關,是語言集中的一個元素,它由終結符構成,能由“文法”推導出。例如,上述文法可以推出“我是大學生”,是以它是句子。

3) 文法樹

文法樹是句子結構的一種樹型表示,它代表了句子的推導結果,它有利于了解句子文法結構的層次。圖 1 所示是“我是大學生”的文法樹。

設計模式--行為型模式--解釋器模式

 有了以上基礎知識,現在來介紹解釋器模式的結構就簡單了。解釋器模式的結構與​​組合模式​​相似,不過其包含的組成元素比組合模式多,而且組合模式是對象結構型模式,而解釋器模式是類行為型模式。

1. 模式的結構

解釋器模式包含以下主要角色。

  1. 抽象表達式(Abstract Expression)角色:定義解釋器的接口,約定解釋器的解釋操作,主要包含解釋方法 interpret()。
  2. 終結符表達式(Terminal Expression)角色:是抽象表達式的子類,用來實作文法中與終結符相關的操作,文法中的每一個終結符都有一個具體終結表達式與之相對應。
  3. 非終結符表達式(Nonterminal Expression)角色:也是抽象表達式的子類,用來實作文法中與非終結符相關的操作,文法中的每條規則都對應于一個非終結符表達式。
  4. 環境(Context)角色:通常包含各個解釋器需要的資料或是公共的功能,一般用來傳遞被所有解釋器共享的資料,後面的解釋器可以從這裡擷取這些值。
  5. 用戶端(Client):主要任務是将需要分析的句子或表達式轉換成使用解釋器對象描述的抽象文法樹,然後調用解釋器的解釋方法,當然也可以通過環境角色間接通路解釋器的解釋方法。

解釋器模式的結構圖如圖 2 所示。

設計模式--行為型模式--解釋器模式

2. 模式的實作

解釋器模式實作的關鍵是定義文法規則、設計終結符類與非終結符類、畫出結構圖,必要時建構文法樹,其代碼結構如下:

package net.biancheng.c.interpreter;
//抽象表達式類
interface AbstractExpression {
    public void interpret(String info);    //解釋方法
}
//終結符表達式類
class TerminalExpression implements AbstractExpression {
    public void interpret(String info) {
        //對終結符表達式的處理
    }
}
//非終結符表達式類
class NonterminalExpression implements AbstractExpression {
    private AbstractExpression exp1;
    private AbstractExpression exp2;
    public void interpret(String info) {
        //非對終結符表達式的處理
    }
}
//環境類
class Context {
    private AbstractExpression exp;
    public Context() {
        //資料初始化
    }
    public void operation(String info) {
        //調用相關表達式類的解釋方法
    }
}      

模式的應用執行個體

【例1】用解釋器模式設計一個“韶粵通”公共汽車卡的讀卡器程式。

說明:假如“韶粵通”公共汽車讀卡器可以判斷乘客的身份,如果是“韶關”或者“廣州”的“老人” “婦女”“兒童”就可以免費乘車,其他人員乘車一次扣 2 元。

分析:本執行個體用“解釋器模式”設計比較适合,首先設計其文法規則如下。

::= 的

 ::= 韶關|廣州

 ::= 老人|婦女|兒童

      

然後,根據文法規則按以下步驟設計公共汽車卡的讀卡器程式的類圖。

  • 定義一個抽象表達式(Expression)接口,它包含了解釋方法 interpret(String info)。
  • 定義一個終結符表達式(Terminal Expression)類,它用集合(Set)類來儲存滿足條件的城市或人,并實作抽象表達式接口中的解釋方法 interpret(Stringinfo),用來判斷被分析的字元串是否是集合中的終結符。
  • 定義一個非終結符表達式(AndExpressicm)類,它也是抽象表達式的子類,它包含滿足條件的城市的終結符表達式對象和滿足條件的人員的終結符表達式對象,并實作 interpret(String info) 方法,用來判斷被分析的字元串是否是滿足條件的城市中的滿足條件的人員。
  • 最後,定義一個環境(Context)類,它包含解釋器需要的資料,完成對終結符表達式的初始化,并定義一個方法 freeRide(String info) 調用表達式對象的解釋方法來對被分析的字元串進行解釋。其結構圖如圖 3 所示。
設計模式--行為型模式--解釋器模式

 程式代碼如下:

package net.biancheng.c.interpreter;
import java.util.*;
/*文法規則
   ::= 的

   ::= 韶關|廣州
  
 ::= 老人|婦女|兒童
*/
public class InterpreterPatternDemo {
    public static void main(String[] args) {
        Context bus = new Context();
        bus.freeRide("韶關的老人");
        bus.freeRide("韶關的年輕人");
        bus.freeRide("廣州的婦女");
        bus.freeRide("廣州的兒童");
        bus.freeRide("山東的兒童");
    }
}
//抽象表達式類
interface Expression {
    public boolean interpret(String info);
}
//終結符表達式類
class TerminalExpression implements Expression {
    private Set set = new HashSet();
    public TerminalExpression(String[] data) {
        for (int i = 0; i < data.length; i++) set.add(data[i]);
    }
    public boolean interpret(String info) {
        if (set.contains(info)) {
            return true;
        }
        return false;
    }
}
//非終結符表達式類
class AndExpression implements Expression {
    private Expression city = null;
    private Expression person = null;
    public AndExpression(Expression city, Expression person) {
        this.city = city;
        this.person = person;
    }
    public boolean interpret(String info) {
        String s[] = info.split("的");
        return city.interpret(s[0]) && person.interpret(s[1]);
    }
}
//環境類
class Context {
    private String[] citys = {"韶關", "廣州"};
    private String[] persons = {"老人", "婦女", "兒童"};
    private Expression cityPerson;
    public Context() {
        Expression city = new TerminalExpression(citys);
        Expression person = new TerminalExpression(persons);
        cityPerson = new AndExpression(city, person);
    }
    public void freeRide(String info) {
        boolean ok = cityPerson.interpret(info);
        if (ok) System.out.println("您是" + info + ",您本次乘車免費!");
        else System.out.println(info + ",您不是免費人員,本次乘車扣費2元!");
    }
}

      

程式運作結果如下:

您是韶關的老人,您本次乘車免費!
韶關的年輕人,您不是免費人員,本次乘車扣費2元!
您是廣州的婦女,您本次乘車免費!
您是廣州的兒童,您本次乘車免費!
山東的兒童,您不是免費人員,本次乘車扣費2元!      

模式的應用場景

前面介紹了解釋器模式的結構與特點,下面分析它的應用場景。

  1. 當語言的文法較為簡單,且執行效率不是關鍵問題時。
  2. 當問題重複出現,且可以用一種簡單的語言來進行表達時。
  3. 當一個語言需要解釋執行,并且語言中的句子可以表示為一個抽象文法樹的時候,如 XML 文檔解釋。

注意:解釋器模式在實際的軟體開發中使用比較少,因為它會引起效率、性能以及維護等問題。如果碰到對表達式的解釋,在 ​​Java​​ 中可以用 Expression4J 或 Jep 等來設計。

模式的擴充

在項目開發中,如果要對資料表達式進行分析與計算,無須再用解釋器模式進行設計了,Java 提供了以下強大的數學公式解析器:Expression4J、MESP(Math Expression String Parser) 和 Jep 等,它們可以解釋一些複雜的文法,功能強大,使用簡單。

現在以 Jep 為例來介紹該工具包的使用方法。Jep 是 Java expression parser 的簡稱,即 Java 表達式分析器,它是一個用來轉換和計算數學表達式的 Java 庫。通過這個程式庫,使用者可以以字元串的形式輸入一個任意的公式,然後快速地計算出其結果。而且 Jep 支援使用者自定義變量、常量和函數,它包括許多常用的數學函數和常量。

使用前先下載下傳 Jep 壓縮包,解壓後,将 jep-x.x.x.jar 檔案移到選擇的目錄中,在 Eclipse 的“Java 建構路徑”對話框的“庫”頁籤中選擇“添加外部 JAR(X)...”,将該 Jep 包添加項目中後即可使用其中的類庫。

下面以計算存款利息為例來介紹。存款利息的計算公式是:本金x利率x時間=利息,其相關代碼如下:

package net.biancheng.c.interpreter;
import com.singularsys.jep.*;
public class JepDemo {
    public static void main(String[] args) throws JepException {
        Jep jep = new Jep();
        //定義要計算的資料表達式
        String 存款利息 = "本金*利率*時間";
        //給相關變量指派
        jep.addVariable("本金", 10000);
        jep.addVariable("利率", 0.038);
        jep.addVariable("時間", 2);
        jep.parse(存款利息);    //解析表達式
        Object accrual = jep.evaluate();    //計算
        System.out.println("存款利息:" + accrual);
    }
}      
存款利息:760.0