行為型SEQ3 - 解釋器模式 Interpreter Pattern

【學(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。

圖18-1 加法/減法解釋器示意圖

我們知道,像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)表示:

圖18-2 抽象語(yǔ)法樹示意圖

在該抽象語(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所示:

圖18-3 解釋器模式結(jié)構(gòu)圖

在解釋器模式結(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所示:

圖18-4 機(jī)器人控制程序抽象語(yǔ)法樹實(shí)例

機(jī)器人控制程序?qū)嵗窘Y(jié)構(gòu)如圖18-5所示:

圖18-5 機(jī)器人控制程序結(jié)構(gòu)圖

在圖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 格式化指令結(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ī)則。

  1. 主要優(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ú)須修改,符合“開閉原則”。

  1. 主要缺點(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ò)程也比較麻煩。

  1. 適用場(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上做掉

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,460評(píng)論 6 538
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,067評(píng)論 3 423
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,467評(píng)論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,468評(píng)論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,184評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,582評(píng)論 1 325
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,616評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,794評(píng)論 0 289
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,343評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,096評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,291評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,863評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,513評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,941評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,190評(píng)論 1 291
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,026評(píng)論 3 396
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,253評(píng)論 2 375

推薦閱讀更多精彩內(nèi)容