【學(xué)習(xí)難度:★★★★★,使用頻率:★☆☆☆☆】
直接出處:解釋器模式
梳理和學(xué)習(xí):https://github.com/BruceOuyang/boy-design-pattern
簡(jiǎn)書日期: 2018/03/20
簡(jiǎn)書首頁(yè):http://www.lxweimin.com/p/0fb891a7c5ed
自定義語(yǔ)言的實(shí)現(xiàn)——解釋器模式(一)
有朋友一直在等待我的解釋器模式文稿,微笑,現(xiàn)把某個(gè)版本發(fā)在博客上,歡迎大家討論!
雖然目前計(jì)算機(jī)編程語(yǔ)言有好幾百種,但有時(shí)候我們還是希望能用一些簡(jiǎn)單的語(yǔ)言來(lái)實(shí)現(xiàn)一些特定的操作,我們只要向計(jì)算機(jī)輸入一個(gè)句子或文件,它就能夠按照預(yù)先定義的文法規(guī)則來(lái)對(duì)句子或文件進(jìn)行解釋,從而實(shí)現(xiàn)相應(yīng)的功能。例如提供一個(gè)簡(jiǎn)單的加法/減法解釋器,只要輸入一個(gè)加法/減法表達(dá)式,它就能夠計(jì)算出表達(dá)式結(jié)果,如圖18-1所示,當(dāng)輸入字符串表達(dá)式為“1 + 2 + 3 – 4 + 1”時(shí),將輸出計(jì)算結(jié)果為3。
我們知道,像C++、Java和C#等語(yǔ)言無(wú)法直接解釋類似“1+ 2 + 3 – 4 + 1”這樣的字符串(如果直接作為數(shù)值表達(dá)式時(shí)可以解釋),我們必須自己定義一套文法規(guī)則來(lái)實(shí)現(xiàn)對(duì)這些語(yǔ)句的解釋,即設(shè)計(jì)一個(gè)自定義語(yǔ)言。在實(shí)際開發(fā)中,這些簡(jiǎn)單的自定義語(yǔ)言可以基于現(xiàn)有的編程語(yǔ)言來(lái)設(shè)計(jì),如果所基于的編程語(yǔ)言是面向?qū)ο笳Z(yǔ)言,此時(shí)可以使用解釋器模式來(lái)實(shí)現(xiàn)自定義語(yǔ)言。
18.1 機(jī)器人控制程序
Sunny軟件公司欲為某玩具公司開發(fā)一套機(jī)器人控制程序,在該機(jī)器人控制程序中包含一些簡(jiǎn)單的英文控制指令,每一個(gè)指令對(duì)應(yīng)一個(gè)表達(dá)式(expression),該表達(dá)式可以是簡(jiǎn)單表達(dá)式也可以是復(fù)合表達(dá)式,每一個(gè)簡(jiǎn)單表達(dá)式由移動(dòng)方向(direction),移動(dòng)方式(action)和移動(dòng)距離(distance)三部分組成,其中移動(dòng)方向包括上(up)、下(down)、左(left)、右(right);移動(dòng)方式包括移動(dòng)(move)和快速移動(dòng)(run);移動(dòng)距離為一個(gè)正整數(shù)。兩個(gè)表達(dá)式之間可以通過(guò)與(and)連接,形成復(fù)合(composite)表達(dá)式。
用戶通過(guò)對(duì)圖形化的設(shè)置界面進(jìn)行操作可以創(chuàng)建一個(gè)機(jī)器人控制指令,機(jī)器人在收到指令后將按照指令的設(shè)置進(jìn)行移動(dòng),例如輸入控制指令:up move 5,則“向上移動(dòng)5個(gè)單位”;輸入控制指令:down run 10 and left move 20,則“向下快速移動(dòng)10個(gè)單位再向左移動(dòng)20個(gè)單位”。
Sunny軟件公司開發(fā)人員決定自定義一個(gè)簡(jiǎn)單的語(yǔ)言來(lái)解釋機(jī)器人控制指令,根據(jù)上述需求描述,用形式化語(yǔ)言來(lái)表示該簡(jiǎn)單語(yǔ)言的文法規(guī)則如下:
expression ::= direction action distance | composite //表達(dá)式
composite ::= expression 'and' expression //復(fù)合表達(dá)式
direction ::= 'up' | 'down' | 'left' | 'right' //移動(dòng)方向
action ::= 'move' | 'run' //移動(dòng)方式
distance ::= an integer //移動(dòng)距離
上述語(yǔ)言一共定義了五條文法規(guī)則,對(duì)應(yīng)五個(gè)語(yǔ)言單位,這些語(yǔ)言單位可以分為兩類,一類為終結(jié)符(也稱為終結(jié)符表達(dá)式),例如direction、action和distance,它們是語(yǔ)言的最小組成單位,不能再進(jìn)行拆分;另一類為非終結(jié)符(也稱為非終結(jié)符表達(dá)式),例如expression和composite,它們都是一個(gè)完整的句子,包含一系列終結(jié)符或非終結(jié)符。
我們根據(jù)上述規(guī)則定義出的語(yǔ)言可以構(gòu)成很多語(yǔ)句,計(jì)算機(jī)程序?qū)⒏鶕?jù)這些語(yǔ)句進(jìn)行某種操作。為了實(shí)現(xiàn)對(duì)語(yǔ)句的解釋,可以使用解釋器模式,在解釋器模式中每一個(gè)文法規(guī)則都將對(duì)應(yīng)一個(gè)類,擴(kuò)展、改變文法以及增加新的文法規(guī)則都很方便,下面就讓我們正式進(jìn)入解釋器模式的學(xué)習(xí),看看使用解釋器模式如何來(lái)實(shí)現(xiàn)對(duì)機(jī)器人控制指令的處理。
自定義語(yǔ)言的實(shí)現(xiàn)——解釋器模式(二)
18.2 文法規(guī)則和抽象語(yǔ)法樹
解釋器模式描述了如何為簡(jiǎn)單的語(yǔ)言定義一個(gè)文法,如何在該語(yǔ)言中表示一個(gè)句子,以及如何解釋這些句子。在正式分析解釋器模式結(jié)構(gòu)之前,我們先來(lái)學(xué)習(xí)如何表示一個(gè)語(yǔ)言的文法規(guī)則以及如何構(gòu)造一棵抽象語(yǔ)法樹。
在前面所提到的加法/減法解釋器中,每一個(gè)輸入表達(dá)式,例如“1 + 2 + 3 – 4 + 1”,都包含了三個(gè)語(yǔ)言單位,可以使用如下文法規(guī)則來(lái)定義:
expression ::= value | operation
operation ::= expression '+' expression | expression '-' expression
value ::= an integer //一個(gè)整數(shù)值
該文法規(guī)則包含三條語(yǔ)句,第一條表示表達(dá)式的組成方式,其中value和operation是后面兩個(gè)語(yǔ)言單位的定義,每一條語(yǔ)句所定義的字符串如operation和value稱為語(yǔ)言構(gòu)造成分或語(yǔ)言單位,符號(hào)“::=”表示“定義為”的意思,其左邊的語(yǔ)言單位通過(guò)右邊來(lái)進(jìn)行說(shuō)明和定義,語(yǔ)言單位對(duì)應(yīng)終結(jié)符表達(dá)式和非終結(jié)符表達(dá)式。如本規(guī)則中的operation是非終結(jié)符表達(dá)式,它的組成元素仍然可以是表達(dá)式,可以進(jìn)一步分解,而value是終結(jié)符表達(dá)式,它的組成元素是最基本的語(yǔ)言單位,不能再進(jìn)行分解。
在文法規(guī)則定義中可以使用一些符號(hào)來(lái)表示不同的含義,如使用“|”表示或,使用“{”和“}”表示組合,使用“*”表示出現(xiàn)0次或多次等,其中使用頻率最高的符號(hào)是表示“或”關(guān)系的“|”,如文法規(guī)則“boolValue ::= 0 | 1”表示終結(jié)符表達(dá)式boolValue的取值可以為0或者1。
除了使用文法規(guī)則來(lái)定義一個(gè)語(yǔ)言,在解釋器模式中還可以通過(guò)一種稱之為抽象語(yǔ)法樹(Abstract Syntax Tree, AST)的圖形方式來(lái)直觀地表示語(yǔ)言的構(gòu)成,每一棵抽象語(yǔ)法樹對(duì)應(yīng)一個(gè)語(yǔ)言實(shí)例,如加法/減法表達(dá)式語(yǔ)言中的語(yǔ)句“1+ 2 + 3 – 4 + 1”,可以通過(guò)如圖18-2所示抽象語(yǔ)法樹來(lái)表示:
在該抽象語(yǔ)法樹中,可以通過(guò)終結(jié)符表達(dá)式value和非終結(jié)符表達(dá)式operation組成復(fù)雜的語(yǔ)句,每個(gè)文法規(guī)則的語(yǔ)言實(shí)例都可以表示為一個(gè)抽象語(yǔ)法樹,即每一條具體的語(yǔ)句都可以用類似圖18-2所示的抽象語(yǔ)法樹來(lái)表示,在圖中終結(jié)符表達(dá)式類的實(shí)例作為樹的葉子節(jié)點(diǎn),而非終結(jié)符表達(dá)式類的實(shí)例作為非葉子節(jié)點(diǎn),它們可以將終結(jié)符表達(dá)式類的實(shí)例以及包含終結(jié)符和非終結(jié)符實(shí)例的子表達(dá)式作為其子節(jié)點(diǎn)。抽象語(yǔ)法樹描述了如何構(gòu)成一個(gè)復(fù)雜的句子,通過(guò)對(duì)抽象語(yǔ)法樹的分析,可以識(shí)別出語(yǔ)言中的終結(jié)符類和非終結(jié)符類。
自定義語(yǔ)言的實(shí)現(xiàn)——解釋器模式(三)
18.3 解釋器模式概述
解釋器模式是一種使用頻率相對(duì)較低但學(xué)習(xí)難度較大的設(shè)計(jì)模式,它用于描述如何使用面向?qū)ο笳Z(yǔ)言構(gòu)成一個(gè)簡(jiǎn)單的語(yǔ)言解釋器。在某些情況下,為了更好地描述某一些特定類型的問(wèn)題,我們可以創(chuàng)建一種新的語(yǔ)言,這種語(yǔ)言擁有自己的表達(dá)式和結(jié)構(gòu),即文法規(guī)則,這些問(wèn)題的實(shí)例將對(duì)應(yīng)為該語(yǔ)言中的句子。此時(shí),可以使用解釋器模式來(lái)設(shè)計(jì)這種新的語(yǔ)言。對(duì)解釋器模式的學(xué)習(xí)能夠加深我們對(duì)面向?qū)ο笏枷氲睦斫猓⑶艺莆站幊陶Z(yǔ)言中文法規(guī)則的解釋過(guò)程。
解釋器模式定義如下: 解釋器模式(Interpreter Pattern):定義一個(gè)語(yǔ)言的文法,并且建立一個(gè)解釋器來(lái)解釋該語(yǔ)言中的句子,這里的“語(yǔ)言”是指使用規(guī)定格式和語(yǔ)法的代碼。解釋器模式是一種類行為型模式。
由于表達(dá)式可分為終結(jié)符表達(dá)式和非終結(jié)符表達(dá)式,因此解釋器模式的結(jié)構(gòu)與組合模式的結(jié)構(gòu)有些類似,但在解釋器模式中包含更多的組成元素,它的結(jié)構(gòu)如圖18-3所示:
在解釋器模式結(jié)構(gòu)圖中包含如下幾個(gè)角色:
AbstractExpression(抽象表達(dá)式):在抽象表達(dá)式中聲明了抽象的解釋操作,它是所有終結(jié)符表達(dá)式和非終結(jié)符表達(dá)式的公共父類。
TerminalExpression(終結(jié)符表達(dá)式):終結(jié)符表達(dá)式是抽象表達(dá)式的子類,它實(shí)現(xiàn)了與文法中的終結(jié)符相關(guān)聯(lián)的解釋操作,在句子中的每一個(gè)終結(jié)符都是該類的一個(gè)實(shí)例。通常在一個(gè)解釋器模式中只有少數(shù)幾個(gè)終結(jié)符表達(dá)式類,它們的實(shí)例可以通過(guò)非終結(jié)符表達(dá)式組成較為復(fù)雜的句子。
NonterminalExpression(非終結(jié)符表達(dá)式):非終結(jié)符表達(dá)式也是抽象表達(dá)式的子類,它實(shí)現(xiàn)了文法中非終結(jié)符的解釋操作,由于在非終結(jié)符表達(dá)式中可以包含終結(jié)符表達(dá)式,也可以繼續(xù)包含非終結(jié)符表達(dá)式,因此其解釋操作一般通過(guò)遞歸的方式來(lái)完成。
Context(環(huán)境類):環(huán)境類又稱為上下文類,它用于存儲(chǔ)解釋器之外的一些全局信息,通常它臨時(shí)存儲(chǔ)了需要解釋的語(yǔ)句。
在解釋器模式中,每一種終結(jié)符和非終結(jié)符都有一個(gè)具體類與之對(duì)應(yīng),正因?yàn)槭褂妙悂?lái)表示每一條文法規(guī)則,所以系統(tǒng)將具有較好的靈活性和可擴(kuò)展性。對(duì)于所有的終結(jié)符和非終結(jié)符,我們首先需要抽象出一個(gè)公共父類,即抽象表達(dá)式類,其典型代碼如下所示:
abstract class AbstractExpression {
public abstract void interpret(Context ctx);
}
終結(jié)符表達(dá)式和非終結(jié)符表達(dá)式類都是抽象表達(dá)式類的子類,對(duì)于終結(jié)符表達(dá)式,其代碼很簡(jiǎn)單,主要是對(duì)終結(jié)符元素的處理,其典型代碼如下所示:
class TerminalExpression extends AbstractExpression {
public void interpret(Context ctx) {
//終結(jié)符表達(dá)式的解釋操作
}
}
對(duì)于非終結(jié)符表達(dá)式,其代碼相對(duì)比較復(fù)雜,因?yàn)榭梢酝ㄟ^(guò)非終結(jié)符將表達(dá)式組合成更加復(fù)雜的結(jié)構(gòu),對(duì)于包含兩個(gè)操作元素的非終結(jié)符表達(dá)式類,其典型代碼如下:
class NonterminalExpression extends AbstractExpression {
private AbstractExpression left;
private AbstractExpression right;
public NonterminalExpression(AbstractExpression left,AbstractExpression right) {
this.left=left;
this.right=right;
}
public void interpret(Context ctx) {
//遞歸調(diào)用每一個(gè)組成部分的interpret()方法
//在遞歸調(diào)用時(shí)指定組成部分的連接方式,即非終結(jié)符的功能
}
}
除了上述用于表示表達(dá)式的類以外,通常在解釋器模式中還提供了一個(gè)環(huán)境類Context,用于存儲(chǔ)一些全局信息,通常在Context中包含了一個(gè)HashMap或ArrayList等類型的集合對(duì)象(也可以直接由HashMap等集合類充當(dāng)環(huán)境類),存儲(chǔ)一系列公共信息,如變量名與值的映射關(guān)系(key/value)等,用于在進(jìn)行具體的解釋操作時(shí)從中獲取相關(guān)信息。其典型代碼片段如下:
class Context {
private HashMap map = new HashMap();
public void assign(String key, String value) {
//往環(huán)境類中設(shè)值
}
public String lookup(String key) {
//獲取存儲(chǔ)在環(huán)境類中的值
}
}
當(dāng)系統(tǒng)無(wú)須提供全局公共信息時(shí)可以省略環(huán)境類,可根據(jù)實(shí)際情況決定是否需要環(huán)境類。
思考
繪制加法/減法解釋器的類圖并編寫核心實(shí)現(xiàn)代碼。
自定義語(yǔ)言的實(shí)現(xiàn)——解釋器模式(四)
18.4 完整解決方案
為了能夠解釋機(jī)器人控制指令,Sunny軟件公司開發(fā)人員使用解釋器模式來(lái)設(shè)計(jì)和實(shí)現(xiàn)機(jī)器人控制程序。針對(duì)五條文法規(guī)則,分別提供五個(gè)類來(lái)實(shí)現(xiàn),其中終結(jié)符表達(dá)式direction、action和distance對(duì)應(yīng)DirectionNode類、ActionNode類和DistanceNode類,非終結(jié)符表達(dá)式expression和composite對(duì)應(yīng)SentenceNode類和AndNode類。
我們可以通過(guò)抽象語(yǔ)法樹來(lái)表示具體解釋過(guò)程,例如機(jī)器人控制指令“down run 10 and left move 20”對(duì)應(yīng)的抽象語(yǔ)法樹如圖18-4所示:
機(jī)器人控制程序?qū)嵗窘Y(jié)構(gòu)如圖18-5所示:
在圖18-5中,AbstractNode充當(dāng)抽象表達(dá)式角色,DirectionNode、ActionNode和DistanceNode充當(dāng)終結(jié)符表達(dá)式角色,AndNode和SentenceNode充當(dāng)非終結(jié)符表達(dá)式角色。完整代碼如下所示:
//注:本實(shí)例對(duì)機(jī)器人控制指令的輸出結(jié)果進(jìn)行模擬,將英文指令翻譯為中文指令,實(shí)際情況是調(diào)用不同的控制程序進(jìn)行機(jī)器人的控制,包括對(duì)移動(dòng)方向、方式和距離的控制等
import java.util.*;
//抽象表達(dá)式
abstract class AbstractNode {
public abstract String interpret();
}
//And解釋:非終結(jié)符表達(dá)式
class AndNode extends AbstractNode {
private AbstractNode left; //And的左表達(dá)式
private AbstractNode right; //And的右表達(dá)式
public AndNode(AbstractNode left, AbstractNode right) {
this.left = left;
this.right = right;
}
//And表達(dá)式解釋操作
public String interpret() {
return left.interpret() + "再" + right.interpret();
}
}
//簡(jiǎn)單句子解釋:非終結(jié)符表達(dá)式
class SentenceNode extends AbstractNode {
private AbstractNode direction;
private AbstractNode action;
private AbstractNode distance;
public SentenceNode(AbstractNode direction,AbstractNode action,AbstractNode distance) {
this.direction = direction;
this.action = action;
this.distance = distance;
}
//簡(jiǎn)單句子的解釋操作
public String interpret() {
return direction.interpret() + action.interpret() + distance.interpret();
}
}
//方向解釋:終結(jié)符表達(dá)式
class DirectionNode extends AbstractNode {
private String direction;
public DirectionNode(String direction) {
this.direction = direction;
}
//方向表達(dá)式的解釋操作
public String interpret() {
if (direction.equalsIgnoreCase("up")) {
return "向上";
}
else if (direction.equalsIgnoreCase("down")) {
return "向下";
}
else if (direction.equalsIgnoreCase("left")) {
return "向左";
}
else if (direction.equalsIgnoreCase("right")) {
return "向右";
}
else {
return "無(wú)效指令";
}
}
}
//動(dòng)作解釋:終結(jié)符表達(dá)式
class ActionNode extends AbstractNode {
private String action;
public ActionNode(String action) {
this.action = action;
}
//動(dòng)作(移動(dòng)方式)表達(dá)式的解釋操作
public String interpret() {
if (action.equalsIgnoreCase("move")) {
return "移動(dòng)";
}
else if (action.equalsIgnoreCase("run")) {
return "快速移動(dòng)";
}
else {
return "無(wú)效指令";
}
}
}
//距離解釋:終結(jié)符表達(dá)式
class DistanceNode extends AbstractNode {
private String distance;
public DistanceNode(String distance) {
this.distance = distance;
}
//距離表達(dá)式的解釋操作
public String interpret() {
return this.distance;
}
}
//指令處理類:工具類
class InstructionHandler {
private String instruction;
private AbstractNode node;
public void handle(String instruction) {
AbstractNode left = null, right = null;
AbstractNode direction = null, action = null, distance = null;
Stack stack = new Stack(); //聲明一個(gè)棧對(duì)象用于存儲(chǔ)抽象語(yǔ)法樹
String[] words = instruction.split(" "); //以空格分隔指令字符串
for (int i = 0; i < words.length; i++) {
//本實(shí)例采用棧的方式來(lái)處理指令,如果遇到“and”,則將其后的三個(gè)單詞作為三個(gè)終結(jié)符表達(dá)式連成一個(gè)簡(jiǎn)單句子SentenceNode作為“and”的右表達(dá)式,而將從棧頂彈出的表達(dá)式作為“and”的左表達(dá)式,最后將新的“and”表達(dá)式壓入棧中。 if (words[i].equalsIgnoreCase("and")) {
left = (AbstractNode)stack.pop(); //彈出棧頂表達(dá)式作為左表達(dá)式
String word1= words[++i];
direction = new DirectionNode(word1);
String word2 = words[++i];
action = new ActionNode(word2);
String word3 = words[++i];
distance = new DistanceNode(word3);
right = new SentenceNode(direction,action,distance); //右表達(dá)式
stack.push(new AndNode(left,right)); //將新表達(dá)式壓入棧中
}
//如果是從頭開始進(jìn)行解釋,則將前三個(gè)單詞組成一個(gè)簡(jiǎn)單句子SentenceNode并將該句子壓入棧中
else {
String word1 = words[i];
direction = new DirectionNode(word1);
String word2 = words[++i];
action = new ActionNode(word2);
String word3 = words[++i];
distance = new DistanceNode(word3);
left = new SentenceNode(direction,action,distance);
stack.push(left); //將新表達(dá)式壓入棧中
}
}
this.node = (AbstractNode)stack.pop(); //將全部表達(dá)式從棧中彈出
}
public String output() {
String result = node.interpret(); //解釋表達(dá)式
return result;
}
}
工具類InstructionHandler用于對(duì)輸入指令進(jìn)行處理,將輸入指令分割為字符串?dāng)?shù)組,將第1個(gè)、第2個(gè)和第3個(gè)單詞組合成一個(gè)句子,并存入棧中;如果發(fā)現(xiàn)有單詞“and”,則將“and”后的第1個(gè)、第2個(gè)和第3個(gè)單詞組合成一個(gè)新的句子作為“and”的右表達(dá)式,并從棧中取出原先所存句子作為左表達(dá)式,然后組合成一個(gè)And節(jié)點(diǎn)存入棧中。依此類推,直到整個(gè)指令解析結(jié)束。
編寫如下客戶端測(cè)試代碼:
class Client {
public static void main(String args[]) {
String instruction = "up move 5 and down run 10 and left move 5";
InstructionHandler handler = new InstructionHandler();
handler.handle(instruction);
String outString;
outString = handler.output();
System.out.println(outString);
}
}
編譯并運(yùn)行程序,輸出結(jié)果如下:
向上移動(dòng)5再向下快速移動(dòng)10再向左移動(dòng)5
自定義語(yǔ)言的實(shí)現(xiàn)——解釋器模式(五)
18.5 再談Context的作用
在解釋器模式中,環(huán)境類Context用于存儲(chǔ)解釋器之外的一些全局信息,它通常作為參數(shù)被傳遞到所有表達(dá)式的解釋方法interpret()中,可以在Context對(duì)象中存儲(chǔ)和訪問(wèn)表達(dá)式解釋器的狀態(tài),向表達(dá)式解釋器提供一些全局的、公共的數(shù)據(jù),此外還可以在Context中增加一些所有表達(dá)式解釋器都共有的功能,減輕解釋器的職責(zé)。
在上面的機(jī)器人控制程序?qū)嵗校覀兪÷粤谁h(huán)境類角色,下面再通過(guò)一個(gè)簡(jiǎn)單實(shí)例來(lái)說(shuō)明環(huán)境類的用途:
Sunny軟件公司開發(fā)了一套簡(jiǎn)單的基于字符界面的格式化指令,可以根據(jù)輸入的指令在字符界面中輸出一些格式化內(nèi)容,例如輸入“LOOP 2 PRINT楊過(guò) SPACE SPACE PRINT 小龍女 BREAK END PRINT郭靖 SPACE SPACE PRINT 黃蓉”,將輸出如下結(jié)果:
楊過(guò) 小龍女
楊過(guò) 小龍女
郭靖 黃蓉
其中關(guān)鍵詞LOOP表示“循環(huán)”,后面的數(shù)字表示循環(huán)次數(shù);PRINT表示“打印”,后面的字符串表示打印的內(nèi)容;SPACE表示“空格”;BREAK表示“換行”;END表示“循環(huán)結(jié)束”。每一個(gè)關(guān)鍵詞對(duì)應(yīng)一條命令,計(jì)算機(jī)程序?qū)⒏鶕?jù)關(guān)鍵詞執(zhí)行相應(yīng)的處理操作。
現(xiàn)使用解釋器模式設(shè)計(jì)并實(shí)現(xiàn)該格式化指令的解釋,對(duì)指令進(jìn)行分析并調(diào)用相應(yīng)的操作執(zhí)行指令中每一條命令。
Sunny軟件公司開發(fā)人員通過(guò)分析,根據(jù)該格式化指令中句子的組成,定義了如下文法規(guī)則:
expression ::= command* //表達(dá)式,一個(gè)表達(dá)式包含多條命令
command ::= loop | primitive //語(yǔ)句命令
loop ::= 'loopnumber' expression 'end' //循環(huán)命令,其中number為自然數(shù)
primitive ::= 'printstring' | 'space' | 'break' //基本命令,其中string為字符串
根據(jù)以上文法規(guī)則,通過(guò)進(jìn)一步分析,繪制如圖18-6所示結(jié)構(gòu)圖:
在圖18-6中,Context充當(dāng)環(huán)境角色,Node充當(dāng)抽象表達(dá)式角色,ExpressionNode、CommandNode和LoopCommandNode充當(dāng)非終結(jié)符表達(dá)式角色,PrimitiveCommandNode充當(dāng)終結(jié)符表達(dá)式角色。完整代碼如下所示:
import java.util.*;
//環(huán)境類:用于存儲(chǔ)和操作需要解釋的語(yǔ)句,在本實(shí)例中每一個(gè)需要解釋的單詞可以稱為一個(gè)動(dòng)作標(biāo)記(Action Token)或命令
class Context {
private StringTokenizer tokenizer; //StringTokenizer類,用于將字符串分解為更小的字符串標(biāo)記(Token),默認(rèn)情況下以空格作為分隔符
private String currentToken; //當(dāng)前字符串標(biāo)記
public Context(String text) {
tokenizer = new StringTokenizer(text); //通過(guò)傳入的指令字符串創(chuàng)建StringTokenizer對(duì)象
nextToken();
}
//返回下一個(gè)標(biāo)記
public String nextToken() {
if (tokenizer.hasMoreTokens()) {
currentToken = tokenizer.nextToken();
}
else {
currentToken = null;
}
return currentToken;
}
//返回當(dāng)前的標(biāo)記
public String currentToken() {
return currentToken;
}
//跳過(guò)一個(gè)標(biāo)記
public void skipToken(String token) {
if (!token.equals(currentToken)) {
System.err.println("錯(cuò)誤提示:" + currentToken + "解釋錯(cuò)誤!");
}
nextToken();
}
//如果當(dāng)前的標(biāo)記是一個(gè)數(shù)字,則返回對(duì)應(yīng)的數(shù)值
public int currentNumber() {
int number = 0;
try{
number = Integer.parseInt(currentToken); //將字符串轉(zhuǎn)換為整數(shù)
}
catch(NumberFormatException e) {
System.err.println("錯(cuò)誤提示:" + e);
}
return number;
}
}
//抽象節(jié)點(diǎn)類:抽象表達(dá)式
abstract class Node {
public abstract void interpret(Context text); //聲明一個(gè)方法用于解釋語(yǔ)句
public abstract void execute(); //聲明一個(gè)方法用于執(zhí)行標(biāo)記對(duì)應(yīng)的命令
}
//表達(dá)式節(jié)點(diǎn)類:非終結(jié)符表達(dá)式
class ExpressionNode extends Node {
private ArrayList<Node> list = new ArrayList<Node>(); //定義一個(gè)集合用于存儲(chǔ)多條命令
public void interpret(Context context) {
//循環(huán)處理Context中的標(biāo)記
while (true){
//如果已經(jīng)沒有任何標(biāo)記,則退出解釋
if (context.currentToken() == null) {
break;
}
//如果標(biāo)記為END,則不解釋END并結(jié)束本次解釋過(guò)程,可以繼續(xù)之后的解釋
else if (context.currentToken().equals("END")) {
context.skipToken("END");
break;
}
//如果為其他標(biāo)記,則解釋標(biāo)記并將其加入命令集合
else {
Node commandNode = new CommandNode();
commandNode.interpret(context);
list.add(commandNode);
}
}
}
//循環(huán)執(zhí)行命令集合中的每一條命令
public void execute() {
Iterator iterator = list.iterator();
while (iterator.hasNext()){
((Node)iterator.next()).execute();
}
}
}
//語(yǔ)句命令節(jié)點(diǎn)類:非終結(jié)符表達(dá)式
class CommandNode extends Node {
private Node node;
public void interpret(Context context) {
//處理LOOP循環(huán)命令
if (context.currentToken().equals("LOOP")) {
node = new LoopCommandNode();
node.interpret(context);
}
//處理其他基本命令
else {
node = new PrimitiveCommandNode();
node.interpret(context);
}
}
public void execute() {
node.execute();
}
}
//循環(huán)命令節(jié)點(diǎn)類:非終結(jié)符表達(dá)式
class LoopCommandNode extends Node {
private int number; //循環(huán)次數(shù)
private Node commandNode; //循環(huán)語(yǔ)句中的表達(dá)式
//解釋循環(huán)命令
public void interpret(Context context) {
context.skipToken("LOOP");
number = context.currentNumber();
context.nextToken();
commandNode = new ExpressionNode(); //循環(huán)語(yǔ)句中的表達(dá)式
commandNode.interpret(context);
}
public void execute() {
for (int i=0;i<number;i++)
commandNode.execute();
}
}
//基本命令節(jié)點(diǎn)類:終結(jié)符表達(dá)式
class PrimitiveCommandNode extends Node {
private String name;
private String text;
//解釋基本命令
public void interpret(Context context) {
name = context.currentToken();
context.skipToken(name);
if (!name.equals("PRINT") && !name.equals("BREAK") && !name.equals ("SPACE")){
System.err.println("非法命令!");
}
if (name.equals("PRINT")){
text = context.currentToken();
context.nextToken();
}
}
public void execute(){
if (name.equals("PRINT"))
System.out.print(text);
else if (name.equals("SPACE"))
System.out.print(" ");
else if (name.equals("BREAK"))
System.out.println();
}
}
在本實(shí)例代碼中,環(huán)境類Context類似一個(gè)工具類,它提供了用于處理指令的方法,如nextToken()、currentToken()、skipToken()等,同時(shí)它存儲(chǔ)了需要解釋的指令并記錄了每一次解釋的當(dāng)前標(biāo)記(Token),而具體的解釋過(guò)程交給表達(dá)式解釋器類來(lái)處理。我們還可以將各種解釋器類包含的公共方法移至環(huán)境類中,更好地實(shí)現(xiàn)這些方法的重用和擴(kuò)展。
針對(duì)本實(shí)例代碼,我們編寫如下客戶端測(cè)試代碼:
class Client{
public static void main(String[] args){
String text = "LOOP 2 PRINT 楊過(guò) SPACE SPACE PRINT 小龍女 BREAK END PRINT 郭靖 SPACE SPACE PRINT 黃蓉";
Context context = new Context(text);
Node node = new ExpressionNode();
node.interpret(context);
node.execute();
}
}
編譯并運(yùn)行程序,輸出結(jié)果如下:
楊過(guò) 小龍女
楊過(guò) 小龍女
郭靖 黃蓉
思考
預(yù)測(cè)指令“LOOP 2 LOOP 2 PRINT楊過(guò) SPACE SPACE PRINT 小龍女 BREAK END PRINT 郭靖 SPACE SPACE PRINT 黃蓉 BREAK END”的輸出結(jié)果。
自定義語(yǔ)言的實(shí)現(xiàn)——解釋器模式(六)
18.6 解釋器模式總結(jié)
解釋器模式為自定義語(yǔ)言的設(shè)計(jì)和實(shí)現(xiàn)提供了一種解決方案,它用于定義一組文法規(guī)則并通過(guò)這組文法規(guī)則來(lái)解釋語(yǔ)言中的句子。雖然解釋器模式的使用頻率不是特別高,但是它在正則表達(dá)式、XML文檔解釋等領(lǐng)域還是得到了廣泛使用。與解釋器模式類似,目前還誕生了很多基于抽象語(yǔ)法樹的源代碼處理工具,例如Eclipse中的Eclipse AST,它可以用于表示Java語(yǔ)言的語(yǔ)法結(jié)構(gòu),用戶可以通過(guò)擴(kuò)展其功能,創(chuàng)建自己的文法規(guī)則。
- 主要優(yōu)點(diǎn)
解釋器模式的主要優(yōu)點(diǎn)如下:
(1) 易于改變和擴(kuò)展文法。由于在解釋器模式中使用類來(lái)表示語(yǔ)言的文法規(guī)則,因此可以通過(guò)繼承等機(jī)制來(lái)改變或擴(kuò)展文法。
(2) 每一條文法規(guī)則都可以表示為一個(gè)類,因此可以方便地實(shí)現(xiàn)一個(gè)簡(jiǎn)單的語(yǔ)言。
(3) 實(shí)現(xiàn)文法較為容易。在抽象語(yǔ)法樹中每一個(gè)表達(dá)式節(jié)點(diǎn)類的實(shí)現(xiàn)方式都是相似的,這些類的代碼編寫都不會(huì)特別復(fù)雜,還可以通過(guò)一些工具自動(dòng)生成節(jié)點(diǎn)類代碼。
(4) 增加新的解釋表達(dá)式較為方便。如果用戶需要增加新的解釋表達(dá)式只需要對(duì)應(yīng)增加一個(gè)新的終結(jié)符表達(dá)式或非終結(jié)符表達(dá)式類,原有表達(dá)式類代碼無(wú)須修改,符合“開閉原則”。
- 主要缺點(diǎn)
解釋器模式的主要缺點(diǎn)如下:
(1) 對(duì)于復(fù)雜文法難以維護(hù)。在解釋器模式中,每一條規(guī)則至少需要定義一個(gè)類,因此如果一個(gè)語(yǔ)言包含太多文法規(guī)則,類的個(gè)數(shù)將會(huì)急劇增加,導(dǎo)致系統(tǒng)難以管理和維護(hù),此時(shí)可以考慮使用語(yǔ)法分析程序等方式來(lái)取代解釋器模式。
(2) 執(zhí)行效率較低。由于在解釋器模式中使用了大量的循環(huán)和遞歸調(diào)用,因此在解釋較為復(fù)雜的句子時(shí)其速度很慢,而且代碼的調(diào)試過(guò)程也比較麻煩。
- 適用場(chǎng)景
在以下情況下可以考慮使用解釋器模式:
(1) 可以將一個(gè)需要解釋執(zhí)行的語(yǔ)言中的句子表示為一個(gè)抽象語(yǔ)法樹。
(2) 一些重復(fù)出現(xiàn)的問(wèn)題可以用一種簡(jiǎn)單的語(yǔ)言來(lái)進(jìn)行表達(dá)。
(3) 一個(gè)語(yǔ)言的文法較為簡(jiǎn)單。
(4) 執(zhí)行效率不是關(guān)鍵問(wèn)題。【注:高效的解釋器通常不是通過(guò)直接解釋抽象語(yǔ)法樹來(lái)實(shí)現(xiàn)的,而是需要將它們轉(zhuǎn)換成其他形式,使用解釋器模式的執(zhí)行效率并不高。】
練習(xí)
Sunny軟件公司欲為數(shù)據(jù)庫(kù)備份和同步開發(fā)一套簡(jiǎn)單的數(shù)據(jù)庫(kù)同步指令,通過(guò)指令可以對(duì)數(shù)據(jù)庫(kù)中的數(shù)據(jù)和結(jié)構(gòu)進(jìn)行備份,例如,輸入指令“COPY VIEW FROM srcDB TO desDB”表示將數(shù)據(jù)庫(kù)srcDB中的所有視圖(View)對(duì)象都拷貝至數(shù)據(jù)庫(kù)desDB;輸入指令“MOVE TABLE Student FROM srcDB TO desDB”表示將數(shù)據(jù)庫(kù)srcDB中的Student表移動(dòng)至數(shù)據(jù)庫(kù)desDB。試使用解釋器模式來(lái)設(shè)計(jì)并實(shí)現(xiàn)該數(shù)據(jù)庫(kù)同步指令。
【注:本練習(xí)是2010年我在給某公司進(jìn)行設(shè)計(jì)模式內(nèi)訓(xùn)時(shí)該公司正在開發(fā)的一個(gè)小工具!】
解釋器模式可以說(shuō)是所有設(shè)計(jì)模式中難度較大、使用頻率較低的一個(gè)模式,如果您能夠靜下心來(lái)把這幾篇文章都看完,我相信您對(duì)解釋器模式應(yīng)該有了一個(gè)較為全面的了解,歡迎大家與我交流和討論。
感謝您能夠堅(jiān)持看完這六篇關(guān)于解釋器模式的文章!
練習(xí)會(huì)在我的github上做掉