在軟件開發(fā)中惭适,會遇到有些問題多次重復出現(xiàn)辆苔,而且有一定的相似性和規(guī)律性。如果將它們歸納成一種簡單的語言忿族,那么這些問題實例將是該語言的一些句子锣笨,這樣就可以用“編譯原理”中的解釋器模式來實現(xiàn)了。
雖然使用解釋器模式的實例不是很多道批,但對于滿足以上特點票唆,且對運行效率要求不是很高的應用實例,如果用解釋器模式來實現(xiàn)屹徘,其效果是非常好的走趋,本文將介紹其工作原理與使用方法。
模式的定義與特點
解釋器(Interpreter)模式的定義:給分析對象定義一個語言噪伊,并定義該語言的文法表示簿煌,再設計一個解析器來解釋語言中的句子。也就是說鉴吹,用編譯語言的方式來分析應用中的實例姨伟。這種模式實現(xiàn)了文法表達式處理的接口,該接口解釋一個特定的上下文豆励。
這里提到的文法和句子的概念同編譯原理中的描述相同夺荒,“文法”指語言的語法規(guī)則,而“句子”是語言集中的元素良蒸。例如技扼,漢語中的句子有很多,“我是中國人”是其中的一個句子嫩痰,可以用一棵語法樹來直觀地描述語言中的句子剿吻。
解釋器模式是一種類行為型模式,其主要優(yōu)點如下串纺。
- 擴展性好丽旅。由于在解釋器模式中使用類來表示語言的文法規(guī)則椰棘,因此可以通過繼承等機制來改變或擴展文法。
- 容易實現(xiàn)榄笙。在語法樹中的每個表達式節(jié)點類都是相似的邪狞,所以實現(xiàn)其文法較為容易。
解釋器模式的主要缺點如下茅撞。
- 執(zhí)行效率較低外恕。解釋器模式中通常使用大量的循環(huán)和遞歸調(diào)用乡翅,當要解釋的句子較復雜時,其運行速度很慢罪郊,且代碼的調(diào)試過程也比較麻煩蠕蚜。
- 會引起類膨脹。解釋器模式中的每條規(guī)則至少需要定義一個類悔橄,當包含的文法規(guī)則很多時靶累,類的個數(shù)將急劇增加,導致系統(tǒng)難以管理與維護癣疟。
- 可應用的場景比較少挣柬。在軟件開發(fā)中,需要定義語言文法的應用實例非常少睛挚,所以這種模式很少被使用到邪蛔。
模式的結構與實現(xiàn)
解釋器模式常用于對簡單語言的編譯或分析實例中,為了掌握好它的結構與實現(xiàn)扎狱,必須先了解編譯原理中的“文法侧到、句子、語法樹”等相關概念淤击。
1) 文法
文法是用于描述語言的語法結構的形式規(guī)則匠抗。沒有規(guī)矩不成方圓,例如污抬,有些人認為完美愛情的準則是“相互吸引汞贸、感情專一、任何一方都沒有戀愛經(jīng)歷”印机,雖然最后一條準則較苛刻矢腻,但任何事情都要有規(guī)則,語言也一樣射赛,不管它是機器語言還是自然語言踏堡,都有它自己的文法規(guī)則。例如咒劲,中文中的“句子”的文法如下顷蟆。
〈句子〉::=〈主語〉〈謂語〉〈賓語〉
〈主語〉::=〈代詞〉|〈名詞〉
〈謂語〉::=〈動詞〉
〈賓語〉::=〈代詞〉|〈名詞〉
〈代詞〉你|我|他
〈名詞〉7大學生I筱霞I英語
〈動詞〉::=是|學習
注:這里的符號“::=”表示“定義為”的意思诫隅,用“〈”和“〉”括住的是非終結符,沒有括住的是終結符帐偎。
2) 句子
句子是語言的基本單位逐纬,是語言集中的一個元素,它由終結符構成削樊,能由“文法”推導出豁生。例如,上述文法可以推出“我是大學生”漫贞,所以它是句子甸箱。
3) 語法樹
語法樹是句子結構的一種樹型表示,它代表了句子的推導結果迅脐,它有利于理解句子語法結構的層次芍殖。圖 1 所示是“我是大學生”的語法樹。
圖1 句子“我是大學生”的語法樹
有了以上基礎知識谴蔑,現(xiàn)在來介紹解釋器模式的結構就簡單了豌骏。解釋器模式的結構與組合模式相似,不過其包含的組成元素比組合模式多隐锭,而且組合模式是對象結構型模式窃躲,而解釋器模式是類行為型模式。
1. 模式的結構
解釋器模式包含以下主要角色钦睡。
- 抽象表達式(Abstract Expression)角色:定義解釋器的接口蒂窒,約定解釋器的解釋操作,主要包含解釋方法 interpret()荞怒。
- 終結符表達式(Terminal Expression)角色:是抽象表達式的子類刘绣,用來實現(xiàn)文法中與終結符相關的操作,文法中的每一個終結符都有一個具體終結表達式與之相對應挣输。
- 非終結符表達式(Nonterminal Expression)角色:也是抽象表達式的子類纬凤,用來實現(xiàn)文法中與非終結符相關的操作,文法中的每條規(guī)則都對應于一個非終結符表達式撩嚼。
- 環(huán)境(Context)角色:通常包含各個解釋器需要的數(shù)據(jù)或是公共的功能停士,一般用來傳遞被所有解釋器共享的數(shù)據(jù),后面的解釋器可以從這里獲取這些值完丽。
- 客戶端(Client):主要任務是將需要分析的句子或表達式轉換成使用解釋器對象描述的抽象語法樹恋技,然后調(diào)用解釋器的解釋方法,當然也可以通過環(huán)境角色間接訪問解釋器的解釋方法逻族。
解釋器模式的結構圖如圖 2 所示蜻底。
圖2 解釋器模式的結構圖
2. 模式的實現(xiàn)
解釋器模式實現(xiàn)的關鍵是定義文法規(guī)則、設計終結符類與非終結符類聘鳞、畫出結構圖薄辅,必要時構建語法樹要拂,其代碼結構如下:
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) {
//非對終結符表達式的處理
}
}
//環(huán)境類
class Context {
private AbstractExpression exp;
public Context() {
//數(shù)據(jù)初始化
}
public void operation(String info) {
//調(diào)用相關表達式類的解釋方法
}
}
模式的應用實例
【例1】用解釋器模式設計一個“韶粵通”公交車卡的讀卡器程序。
說明:假如“韶粵通”公交車讀卡器可以判斷乘客的身份站楚,如果是“韶關”或者“廣州”的“老人” “婦女”“兒童”就可以免費乘車脱惰,其他人員乘車一次扣 2 元。
分析:本實例用“解釋器模式”設計比較適合窿春,首先設計其文法規(guī)則如下拉一。
<expression> ::= <city>的<person>
<city> ::= 韶關|廣州
<person> ::= 老人|婦女|兒童
然后,根據(jù)文法規(guī)則按以下步驟設計公交車卡的讀卡器程序的類圖旧乞。
- 定義一個抽象表達式(Expression)接口蔚润,它包含了解釋方法 interpret(String info)。
- 定義一個終結符表達式(Terminal Expression)類尺栖,它用集合(Set)類來保存滿足條件的城市或人嫡纠,并實現(xiàn)抽象表達式接口中的解釋方法 interpret(Stringinfo),用來判斷被分析的字符串是否是集合中的終結符决瞳。
- 定義一個非終結符表達式(AndExpressicm)類,它也是抽象表達式的子類左权,它包含滿足條件的城市的終結符表達式對象和滿足條件的人員的終結符表達式對象皮胡,并實現(xiàn) interpret(String info) 方法,用來判斷被分析的字符串是否是滿足條件的城市中的滿足條件的人員赏迟。
- 最后屡贺,定義一個環(huán)境(Context)類,它包含解釋器需要的數(shù)據(jù)锌杀,完成對終結符表達式的初始化甩栈,并定義一個方法 freeRide(String info) 調(diào)用表達式對象的解釋方法來對被分析的字符串進行解釋。其結構圖如圖 3 所示糕再。
圖3 “韶粵通”公交車讀卡器程序的結構圖
程序代碼如下:
package net.biancheng.c.interpreter;
import java.util.*;
/*文法規(guī)則
<expression> ::= <city>的<person>
<city> ::= 韶關|廣州
<person> ::= 老人|婦女|兒童
*/
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<String> set = new HashSet<String>();
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]);
}
}
//環(huán)境類
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元髓介!
模式的應用場景
前面介紹了解釋器模式的結構與特點,下面分析它的應用場景筋现。
- 當語言的文法較為簡單唐础,且執(zhí)行效率不是關鍵問題時。
- 當問題重復出現(xiàn)矾飞,且可以用一種簡單的語言來進行表達時一膨。
- 當一個語言需要解釋執(zhí)行,并且語言中的句子可以表示為一個抽象語法樹的時候,如 XML 文檔解釋诵叁。
注意:解釋器模式在實際的軟件開發(fā)中使用比較少墅拭,因為它會引起效率、性能以及維護等問題瞒津。如果碰到對表達式的解釋,在 Java 中可以用 Expression4J 或 Jep 等來設計括尸。
模式的擴展
在項目開發(fā)中巷蚪,如果要對數(shù)據(jù)表達式進行分析與計算,無須再用解釋器模式進行設計了濒翻,Java 提供了以下強大的數(shù)學公式解析器:Expression4J屁柏、MESP(Math Expression String Parser) 和 Jep 等,它們可以解釋一些復雜的文法有送,功能強大淌喻,使用簡單。
現(xiàn)在以 Jep 為例來介紹該工具包的使用方法雀摘。Jep 是 Java expression parser 的簡稱裸删,即 Java 表達式分析器,它是一個用來轉換和計算數(shù)學表達式的 Java 庫阵赠。通過這個程序庫烁落,用戶可以以字符串的形式輸入一個任意的公式,然后快速地計算出其結果豌注。而且 Jep 支持用戶自定義變量伤塌、常量和函數(shù),它包括許多常用的數(shù)學函數(shù)和常量轧铁。
使用前先下載 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();
//定義要計算的數(shù)據(jù)表達式
String 存款利息 = "本金*利率*時間";
//給相關變量賦值
jep.addVariable("本金", 10000);
jep.addVariable("利率", 0.038);
jep.addVariable("時間", 2);
jep.parse(存款利息); //解析表達式
Object accrual = jep.evaluate(); //計算
System.out.println("存款利息:" + accrual);
}
}
程序運行結果如下:
存款利息:760.0