1. 程式人生 > >研磨設計模式 之 直譯器模式(Interpreter)2——跟著cc學設計系列

研磨設計模式 之 直譯器模式(Interpreter)2——跟著cc學設計系列

21.2  解決方案

21.2.1  直譯器模式來解決

用來解決上述問題的一個合理的解決方案,就是使用直譯器模式。那麼什麼是直譯器模式呢?

(1)直譯器模式定義

 

       這裡的文法,簡單點說就是我們俗稱的“語法規則”。

(2)應用直譯器模式來解決的思路

       要想解決當xml的結構發生改變後,不用修改解析部分的程式碼,一個自然的思路就是要把解析部分的程式碼寫成公共的,而且還要是通用的,能夠滿足各種xml取值的需要,比如:獲取單個元素的值,獲取多個相同名稱的元素的值,獲取單個元素的屬性的值,獲取多個相同名稱的元素的屬性的值,等等。

       要寫成通用的程式碼,又有幾個問題要解決,如何組織這些通用的程式碼?如何呼叫這些通用的程式碼?以何種方式來告訴這些通用程式碼,客戶端的需要?

       要解決這些問題,其中的一個解決方案就是直譯器模式。在描述這個模式的解決思路之前,先解釋兩個概念,一個是解析器(不是指xml的解析器),一個是直譯器。

  • 這裡的解析器,指的是把描述客戶端呼叫要求的表示式,經過解析,形成一個抽象語法樹的程式,不是指xml的解析器。
  • 這裡的直譯器,指的是解釋抽象語法樹,並執行每個節點對應的功能的程式。

       要解決通用解析xml的問題,第一步:需要先設計一個簡單的表示式語言,在客戶端呼叫解析程式的時候,傳入用這個表示式語言描述的一個表示式,然後把這個表示式通過解析器的解析,形成一個抽象的語法樹。

       第二步:解析完成後,自動呼叫直譯器來解釋抽象語法樹,並執行每個節點所對應的功能,從而完成通用的xml解析。

       這樣一來,每次當xml結構發生了更改,也就是在客戶端呼叫的時候,傳入不同的表示式即可,整個解析xml過程的程式碼都不需要再修改了。

21.2.2  模式結構和說明

直譯器模式的結構如圖21.1所示:

 

圖21.1  直譯器模式結構圖

AbstractExpression:

       定義直譯器的介面,約定直譯器的解釋操作。

TerminalExpression:

       終結符直譯器,用來實現語法規則中和終結符相關的操作,不再包含其它的直譯器,如果用組合模式來構建抽象語法樹的話,就相當於組合模式中的葉子物件,可以有多種終結符直譯器。

NonterminalExpression:

       非終結符直譯器,用來實現語法規則中非終結符相關的操作,通常一個直譯器對應一個語法規則,可以包含其它的直譯器,如果用組合模式來構建抽象語法樹的話,就相當於組合模式中的組合物件,可以有多種非終結符直譯器。

Context:

       上下文,通常包含各個直譯器需要的資料,或是公共的功能。

Client:

       客戶端,指的是使用直譯器的客戶端,通常在這裡去把按照語言的語法做的表示式,轉換成為使用直譯器物件描述的抽象語法樹,然後呼叫解釋操作。

21.2.3  直譯器模式示例程式碼

(1)先看看抽象表示式的定義,非常簡單,定義一個執行解釋的方法,示例程式碼如下:

/**

 * 抽象表示式

 */

public abstract class AbstractExpression {

    /**

     * 解釋的操作

     * @param ctx 上下文物件

     */

    public abstract void interpret(Context ctx);

}

(2)再來看看終結符表示式的定義,示例程式碼如下:

/**

 * 終結符表示式

 */

public class TerminalExpression extends AbstractExpression{

    public void interpret(Context ctx) {

       //實現與語法規則中的終結符相關聯的解釋操作

    }

}

(3)接下來該看看非終結符表示式的定義了,示例程式碼如下:

/**

 * 非終結符表示式

 */

public class NonterminalExpression extends AbstractExpression{

    public void interpret(Context ctx) {

       //實現與語法規則中的非終結符相關聯的解釋操作

    }

}

(4)上下文的定義,示例程式碼如下:

/**

 * 上下文,包含直譯器之外的一些全域性資訊

 */

public class Context {

}

(5)最後來看看客戶端的定義,示例程式碼如下:

/**

 * 使用直譯器的客戶

 */

public class Client {

    //主要按照語法規則對特定的句子構建抽象語法樹

    //然後呼叫解釋操作

}

看到這裡,可能有些朋友會覺得,上面的示例程式碼裡面什麼都沒有啊。這主要是因為直譯器模式是跟具體的語法規則聯絡在一起的,沒有相應的語法規則,自然寫不出對應的處理程式碼來。

但是這些示例還是有意義的,可以通過它們看出直譯器模式實現的基本架子,只是沒有內部具體的處理罷了。

21.2.4  使用直譯器模式重寫示例

       通過上面的講述可以看出,要使用直譯器模式,一個重要的前提就是要定義一套語法規則,也稱為文法。不管這套文法的規則是簡單還是複雜,必須有這麼個東西,因為直譯器模式就是來按照這些規則進行解析並執行相應的功能的。

1:為表示式設計簡單的文法

為了通用,用root表示根元素,a、b、c、d等來代表元素,一個簡單的xml如下:

<?xml version="1.0" encoding="UTF-8"?>

<root id="rootId">

    <a>

       <b>

           <c name="testC">12345</c>

           <d id="1">d1</d>

           <d id="2">d2</d>

           <d id="3">d3</d>

           <d id="4">d4</d>

       </b>

    </a>

</root>

約定表示式的文法如下:

  • 獲取單個元素的值:從根元素開始,一直到想要獲取值的元素,元素中間用“/”分隔,根元素前不加“/”。比如表示式“root/a/b/c”就表示獲取根元素下、a元素下、b元素下的c元素的值
  • 獲取單個元素的屬性的值:要獲取值的屬性一定是表示式的最後一個元素的屬性,在最後一個元素後面新增“.”然後再加上屬性的名稱。比如表示式“root/a/b/c.name”就表示獲取根元素下、a元素下、b元素下、c元素的name屬性的值
  • 獲取相同元素名稱的值,當然是多個:要獲取值的元素一定是表示式的最後一個元素,在最後一個元素後面新增“$”。比如表示式“root/a/b/d$”就表示獲取根元素下、a元素下、b元素下的多個d元素的值的集合
  • 獲取相同元素名稱的屬性的值,當然也是多個:要獲取屬性值的元素一定是表示式的最後一個元素,在最後一個元素後面新增“$”,然後在後面新增“.”然後再加上屬性的名稱,在屬性名稱後面也新增“$”。比如表示式“root/a/b/d$.id$”就表示獲取根元素下、a元素下、b元素下的多個d元素的id屬性的值的集合

2:示例說明

為了示例的通用性,就使用上面這個xml來實現功能,不去使用前面定義的具體的xml了,解決的方法是一樣的。

另外一個問題,直譯器模式主要解決的是“解釋抽象語法樹,並執行每個節點所對應的功能”,並不包含如何從一個表示式轉換成為抽象的語法樹。因此下面的範例就先來實現直譯器模式所要求的功能。至於如何從一個表示式轉換成為相應的抽象語法樹,後面會給出一個示例。

對於抽象的語法樹這個樹狀結構,很明顯可以使用組合模式來構建。直譯器模式把需要解釋的物件分成了兩大類,一類是節點元素,就是可以包含其它元素的組合元素,比如非終結符元素,對應成為組合模式的Composite;另一類是終結符元素,相當於組合模式的葉子物件。解釋整個抽象語法樹的過程,也就是執行相應物件的功能的過程。

比如上面的xml,對應成為抽象語法樹,可能的結構如下圖21.2所示:

 

圖21.2  xml對應的抽象語法樹示意圖

3:具體示例

從簡單的開始,先來演示獲取單個元素的值和單個元素的屬性的值。在看具體程式碼前,先來看看此時系統的整體結構,如圖21.3所示:

 

圖21.3  直譯器模式示例的結構示意圖

(1)定義抽象的直譯器

要實現直譯器的功能,首先定義一個抽象的直譯器,來約束所有被解釋的語法物件,也就是節點元素和終結符元素都要實現的功能。示例程式碼如下:

/**

 * 用於處理自定義Xml取值表示式的介面

 */

public abstract class ReadXmlExpression {

    /**

     * 解釋表示式

     * @param c 上下文

     * @return 解析過後的值,為了通用,可能是單個值,也可能是多個值,

     *         因此就返回一個數組

     */

    public abstract String[] interpret(Context c);

}

(2)定義上下文

上下文是用來封裝直譯器需要的一些全域性資料,也可以在裡面封裝一些直譯器的公共功能,可以相當於各個直譯器的公共物件,示例程式碼如下:

/**

 *  上下文,用來包含直譯器需要的一些全域性資訊

 */

public class Context {

    /**

     * 上一個被處理的元素

     */

    private Element preEle = null;

    /**

     * Dom解析Xml的Document物件

     */

    private Document document = null;

    /**

     * 構造方法

     * @param filePathName 需要讀取的xml的路徑和名字

     * @throws Exception

     */

    public Context(String filePathName) throws Exception{

       //通過輔助的Xml工具類來獲取被解析的xml對應的Document物件

       this.document = XmlUtil.getRoot(filePathName);

    }

    /**

     * 重新初始化上下文

     */

    public void reInit(){

       preEle = null;

    }

    /**

     * 各個Expression公共使用的方法,

     * 根據父元素和當前元素的名稱來獲取當前的元素

     * @param pEle 父元素

     * @param eleName 當前元素的名稱

     * @return 找到的當前元素

     */

    public Element getNowEle(Element pEle,String eleName){

       NodeList tempNodeList = pEle.getChildNodes();

       for(int i=0;i<tempNodeList.getLength();i++){

           if(tempNodeList.item(i) instanceof Element){

              Element nowEle = (Element)tempNodeList.item(i);

              if(nowEle.getTagName().equals(eleName)){

                  return nowEle;

              }

           }

       }

       return null;

    }

    public Element getPreEle() {

       return preEle;

    }

    public void setPreEle(Element preEle) {

       this.preEle = preEle;

    }

    public Document getDocument() {

       return document;

    }

}

在上下文中使用了一個工具物件XmlUtil來獲取Document物件,就是Dom解析xml,獲取相應的Document物件,示例如下:

public class XmlUtil {

    public static Document getRoot(String filePathName) throws Exception{

       Document doc = null;

          //建立一個解析器工廠

          DocumentBuilderFactory factory =

DocumentBuilderFactory.newInstance();

          //獲得一個DocumentBuilder物件,這個物件代表了具體的DOM解析器

          DocumentBuilder builder=factory.newDocumentBuilder();

          //得到一個表示XML文件的Document物件

          doc=builder.parse(filePathName);

//去掉XML文件中作為格式化內容的空白而對映在DOM樹中的TextNode物件

          doc.normalize();

          return doc;

    }

}

(3)定義元素作為非終結符對應的直譯器

接下來該看看如何解釋執行中間元素了,首先這個元素相當於組合模式的Composite物件,因此需要對子元素進行維護,另外這個元素的解釋處理,只是需要把自己找到,作為下一個元素的父元素就好了。示例程式碼如下:

/**

 * 元素作為非終結符對應的直譯器,解釋並執行中間元素

 */

public class ElementExpression extends ReadXmlExpression{

    /**

     * 用來記錄組合的ReadXmlExpression元素

     */

    private Collection<ReadXmlExpression> eles =

new ArrayList<ReadXmlExpression>();

    /**

     * 元素的名稱

     */

    private String eleName = "";

    public ElementExpression(String eleName){

       this.eleName = eleName;

    }

    public boolean addEle(ReadXmlExpression ele){

       this.eles.add(ele);

       return true;

    }

    public boolean removeEle(ReadXmlExpression ele){

       this.eles.remove(ele);

       return true;

    }  

    public String[] interpret(Context c) {

       //先取出上下文裡的當前元素作為父級元素

       //查詢到當前元素名稱所對應的xml元素,並設定回到上下文中

       Element pEle = c.getPreEle();

       if(pEle==null){

           //說明現在獲取的是根元素

           c.setPreEle(c.getDocument().getDocumentElement());

       }else{

           //根據父級元素和要查詢的元素的名稱來獲取當前的元素

           Element nowEle = c.getNowEle(pEle, eleName);

           //把當前獲取的元素放到上下文裡面

           c.setPreEle(nowEle);

       }

       //迴圈呼叫子元素的interpret方法

       String [] ss = null;

       for(ReadXmlExpression ele : eles){

           ss = ele.interpret(c);

       }

       return ss;

    }

}

(4)定義元素作為終結符對應的直譯器

       對於單個元素的處理,終結符有兩種,一個是元素終結,一個是屬性終結。如果是元素終結,就是要獲取元素的值;如果是屬性終結,就是要獲取屬性的值。

分別來看看如何實現的,先看元素作為終結的直譯器,示例程式碼如下:

/**

 * 元素作為終結符對應的直譯器

 */

public class ElementTerminalExpression  extends ReadXmlExpression{

    /**

     * 元素的名字

     */

    private String eleName = "";

    public ElementTerminalExpression(String name){

       this.eleName = name;

    }  

    public String[] interpret(Context c) {

       //先取出上下文裡的當前元素作為父級元素

       Element pEle = c.getPreEle();

       //查詢到當前元素名稱所對應的xml元素

       Element ele = null;

       if(pEle==null){

           //說明現在獲取的是根元素

           ele = c.getDocument().getDocumentElement();

           c.setPreEle(ele);

       }else{

           //根據父級元素和要查詢的元素的名稱來獲取當前的元素

           ele = c.getNowEle(pEle, eleName);

           //把當前獲取的元素放到上下文裡面

           c.setPreEle(ele);

       }

       //然後需要去獲取這個元素的值

       String[] ss = new String[1];

       ss[0] = ele.getFirstChild().getNodeValue();

       return ss;

    }

}

(5)定義屬性作為終結符對應的直譯器

接下來看看屬性終結符的實現,就會比較簡單,直接獲取最後的元素物件,然後獲取相應的屬性的值,示例程式碼如下:

/**

 * 屬性作為終結符對應的直譯器

 */

public class PropertyTerminalExpression extends ReadXmlExpression{

    /**

     * 屬性的名字

     */

    private String propName;

    public PropertyTerminalExpression(String propName){

       this.propName = propName;

    }

    public String[] interpret(Context c) {

       //直接獲取最後的元素的屬性的值

       String[] ss = new String[1];

       ss[0] = c.getPreEle().getAttribute(this.propName);

       return ss;

    }

}

(6)使用直譯器

定義好了各個直譯器的實現,可以寫個客戶端來測試一下這些直譯器物件的功能了。使用直譯器的客戶端的工作會比較多,最主要的就是要組裝抽象的語法樹。

先來看看如何使用直譯器獲取單個元素的值,示例程式碼如下:

public class Client {

    public static void main(String[] args) throws Exception {

       //準備上下文

       Context c = new Context("InterpreterTest.xml");     

       //想要獲取c元素的值,也就是如下表達式的值:"root/a/b/c"

       //首先要構建直譯器的抽象語法樹

        ElementExpression root = new ElementExpression("root");

       ElementExpression aEle = new ElementExpression("a");

       ElementExpression bEle = new ElementExpression("b");

       ElementTerminalExpression cEle =

new ElementTerminalExpression("c");

       //組合起來

       root.addEle(aEle);

       aEle.addEle(bEle);

       bEle.addEle(cEle);

       //呼叫

       String ss[] = root.interpret(c);

       System.out.println("c的值是="+ss[0]);

    }

}

把前面定義的xml取名叫作“InterpreterTest.xml”,放到當前工程的根下面,執行看看,能正確獲取值嗎,執行的結果如下:

c的值是=12345

再來測試一下獲取單個元素的屬性的值,示例程式碼如下:

public class Client {

    public static void main(String[] args) throws Exception {

       //準備上下文

Context c = new Context("InterpreterTest.xml");

       //想要獲取c元素的name屬性,也就是如下表達式的值:"root/a/b/c.name"

//這個時候c不是終結了,需要把c修改成ElementExpressioin

       ElementExpression root = new ElementExpression("root");

       ElementExpression aEle = new ElementExpression("a");

       ElementExpression bEle = new ElementExpression("b");

       ElementExpression cEle = new ElementExpression("c");

       PropertyTerminalExpression prop =

new PropertyTerminalExpression("name");

       //組合

       root.addEle(aEle);

       aEle.addEle(bEle);

       bEle.addEle(cEle);

       cEle.addEle(prop);

       //呼叫

       String ss[] = root.interpret(c);

       System.out.println("c的屬性name的值是="+ss[0]);

       //如果要使用同一個上下文,連續進行解析,需要重新初始化上下文物件

       //比如要連續的重新再獲取一次屬性name的值,當然你可以重新組合元素,

       //重新解析,只要是在使用同一個上下文,就需要重新初始化上下文物件

       c.reInit();

       String ss2[] = root.interpret(c);

       System.out.println("重新獲取c的屬性name的值是="+ss2[0]);

    }

}

執行的結果如下:

c的屬性name的值是=testC

重新獲取c的屬性name的值是=testC

       就像前面講述的那樣,制定一種簡單的語言,讓客戶端用來表達從xml中取值的表示式的語言,然後為它們定義一種文法的表示,也就是語法規則,然後用直譯器物件來表示那些表示式,接下來通過執行直譯器來解釋並執行這些功能。

       但是從前面的示例中,我們只能看到客戶端直接使用直譯器物件,來表示客戶要從xml中取什麼值的語法樹,而沒有看到如何從語言的表示式轉換成為這種直譯器的表示,這個功能是屬於解析器的功能,沒有劃分在標準的直譯器模式中,所以這裡就先不演示,在後面會有示例來講解析器。


---------------------------------------------------------------------------

研磨設計討論群【252780326】

---------------------------------------------------------------------------