1. 程式人生 > >一天一模式之20直譯器模式

一天一模式之20直譯器模式

本節課程概覽

學習直譯器模式

一:初識直譯器模式

包括:定義、結構、參考實現

二:體會直譯器模式

包括:場景問題、不用模式的解決方案、使用模式的解決方案

三:理解直譯器模式

包括:認識直譯器模式、讀取多個元素或屬性的值、解析器、
直譯器模式的優缺點

四:思考直譯器模式

包括:直譯器模式的本質、何時選用

初識直譯器模式

定義

給定一個語言,定義它的文法的一種表示,並定義一個直譯器,這個解釋
器使用該表示來解釋語言中的句子。

結構和說明

體會直譯器模式

AbstractExpression:

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

TerminalExpression:

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

NonterminalExpression:

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

Context:

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

Client:

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

體會直譯器模式

讀取配置檔案

考慮這樣一個實際的應用,維護系統自定義的配置檔案。幾乎每個實際的
應用系統都有與應用自身相關的配置檔案,這個配置檔案是由開發人員根據需要
自定義的,系統執行時會根據配置的資料進行相應的功能處理。

系統現有的配置資料很簡單,主要是JDBC所需要的資料,還有預設讀取
Spring的配置檔案,目前系統只需要一個Spring的配置檔案。示例如下:

示例

抽象表示式

package cn.javass.dp.interpreter.example2;

/**
 * 抽象表示式
 */
public abstract class AbstractExpression
{
/** * 解釋的操作 * @param ctx 上下文物件 */ public abstract void interpret(Context ctx); }

終結符表示式

package cn.javass.dp.interpreter.example2;
/**
 * 終結符表示式
 */
public class TerminalExpression extends AbstractExpression{

    public void interpret(Context ctx) {
        //實現與語法規則中的終結符相關聯的解釋操作
    }
}

非終結符表示式===相當於組合物件

package cn.javass.dp.interpreter.example2;

import java.util.ArrayList;
import java.util.List;

/**
 * 非終結符表示式===相當於組合物件
 */
public class NonterminalExpression extends AbstractExpression{
    private List<AbstractExpression> list = new ArrayList<AbstractExpression>();

    public void addAbstractExpression(AbstractExpression ae){
        list.add(ae);
    }

    public void interpret(Context ctx) {
        //實現與語法規則中的非終結符相關聯的解釋操作
    }
}

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

package cn.javass.dp.interpreter.example2;
/**
 * 上下文,包含直譯器之外的一些全域性資訊
 */
public class Context {
    //多個直譯器  公共的屬性
    //多個直譯器  公共的方法
}

使用直譯器的客戶

package cn.javass.dp.interpreter.example2;

/**
 * 使用直譯器的客戶
 */
public class Client {
    //第一大步:主要按照語法規則對 特定的句子 構建抽象語法樹
    //注意,這個步驟不在直譯器模式裡面


    //第二大步:然後呼叫解釋操作
    //這個步驟才是直譯器模式要完成的工作
}

現在的功能需求是:如何能夠靈活的讀取配置檔案的內容?

不用模式的解決方案

不就是讀取配置檔案嗎?實現很簡單,直接讀取並解析xml就可以了。讀取
xml的應用包很多,這裡都不用,直接採用最基礎的Dom解析就可以了。另外,讀
取到xml中的值過後,後續如何處理,這裡也不去管,這裡只是實現把配置檔案
讀取並解析出來。

示例

app.xml
<?xml version="1.0" encoding="UTF-8"?>
<root>
    <jdbc>
        <driver-class>驅動類名</driver-class>
        <url>連線資料庫的URL</url>
        <user>連線資料庫的使用者名稱</user>
        <password>連線資料庫的密碼</password>
    </jdbc>
    <application-xml>預設讀取的Spring配置的檔名稱</application-xml>
</root>
app2.xml
<?xml version="1.0" encoding="UTF-8"?>
<root>
    <database-connection>
        <connection-type>連線資料庫的型別,1-用Spring整合的方式(也就是不用下面兩種方式了),2-DataSource(就是使用JNDI),3-使用JDBC自己來連線資料庫</connection-type>
        <jndi>DataSource的方式用,伺服器資料來源的JNDI名稱</jndi>
        <jdbc>跟上面一樣,省略了</jdbc>
    </database-connection>
    <system-operator>系統管理員ID</system-operator>
    <log>
        <operate-type>記錄日誌的方式,1-資料庫,2-檔案</operate-type>
        <file-name>記錄日誌的檔名稱</file-name>
    </log>
    <thread-interval>快取執行緒的間隔時長</thread-interval>
    <spring-default>
        <application-xmls>
            <application-xml>預設讀取的Spring配置的檔名稱</application-xml>
            <application-xml>其它需要讀取的Spring配置的檔名稱</application-xml>
        </application-xmls>
    </spring-default>
</root>
讀取配置檔案
package cn.javass.dp.interpreter.example1;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.*;
/**
 * 讀取配置檔案
 */
public class ReadAppXml {
    /**
     * 讀取配置檔案內容
     * @param filePathName 配置檔案的路徑和檔名
     * @throws Exception
     */
    public void read(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樹中的不必要的Text Node物件
        doc.normalize();

        //獲取jdbc
//      NodeList jdbc = doc.getElementsByTagName("jdbc");
//      //只有一個jdbc,獲取jdbc中的驅動類的名稱
//      NodeList driverClassNode = ((Element)jdbc.item(0)).getElementsByTagName("driver-class");
//      String driverClass = driverClassNode.item(0).getFirstChild().getNodeValue();
//      System.out.println("driverClass=="+driverClass);
//      //同理獲取url、user、password等值
//      NodeList urlNode = ((Element)jdbc.item(0)).getElementsByTagName("url");
//      String url = urlNode.item(0).getFirstChild().getNodeValue();
//      System.out.println("url=="+url);
//      
//      NodeList userNode = ((Element)jdbc.item(0)).getElementsByTagName("user");
//      String user = userNode.item(0).getFirstChild().getNodeValue();
//      System.out.println("user=="+user);
//      
//      NodeList passwordNode = ((Element)jdbc.item(0)).getElementsByTagName("password");
//      String password = passwordNode.item(0).getFirstChild().getNodeValue();
//      System.out.println("password=="+password);
//      
//      
//      //獲取application-xml
//      NodeList applicationXmlNode = doc.getElementsByTagName("application-xml");
//      String applicationXml = applicationXmlNode.item(0).getFirstChild().getNodeValue();
//      System.out.println("applicationXml=="+applicationXml);

        //先要獲取spring-default,然後獲取application-xmls
        //然後才能獲取application-xml     
        NodeList springDefaultNode = doc.getElementsByTagName("spring-default");
        NodeList appXmlsNode = ((Element)springDefaultNode.item(0)).getElementsByTagName("application-xmls");
        NodeList appXmlNode = ((Element)appXmlsNode.item(0)).getElementsByTagName("application-xml");
        //迴圈獲取每個application-xml元素的值
        for(int i=0;i<appXmlNode.getLength();i++){
            String applicationXml = appXmlNode.item(i).getFirstChild().getNodeValue();
            System.out.println("applicationXml=="+applicationXml);
        }
    }

    public static void main(String[] args) throws Exception {
        ReadAppXml t = new ReadAppXml();
        t.read("App2.xml");
    }
}

有何問題

看了上面的實現,多簡單啊,就是最基本的Dom解析嘛,要是採用其它的開
源工具包,比如dom4j、jDom之類的來處理,會更簡單,這好像不值得一提呀,
真的是這樣嗎?

請思考一個問題:如果配置檔案的結構需要變動呢?仔細想想,就會感覺
出問題來了。還是先看例子,然後再來總結這個問題。

隨著開發的深入進行,越來越多可配置的資料被抽取出來,需要新增到配
置檔案中,比如與資料庫的連線配置:就加入了是否需要、是否使用DataSource
等配置。除了這些還加入了一些其它需要配置的資料,例如:系統管理員、日誌
記錄方式、快取執行緒的間隔時長、預設讀取哪些Spring配置檔案等等,示例如
下:

改變一下配置檔案不是件大事情,但是帶來的一系列麻煩也不容忽視,比
如:修改了配置檔案的結構,那麼讀取配置檔案的程式就需要做出相應的變更;
用來封裝配置檔案資料的資料物件也需要相應的修改;外部使用配置檔案的地
方,獲取資料的地方也會相應變動。

當然在這一系列麻煩中,最讓人痛苦的莫過於修改讀取配置檔案的程式
了,有時候幾乎是重寫。比如在使用Dom讀取第一個配置檔案,讀取預設的
Spring配置檔案的值的時候,可能的片斷程式碼示例如下:

但是如果配置檔案改成第二個,檔案的結構發生了改變,需要讀取的配置
檔案變成了多個了,讀取的程式也發生了改變,而且application-xml節點也不是
直接從doc下獲取了。幾乎是完全重寫了,此時可能的片斷程式碼示例如下:

仔細對比上面在xml變化前後讀取值的程式碼,你會發現,由於xml結構的變
化,導致讀取xml檔案內容的程式碼,基本上完全重寫了。

問題還不僅僅限於讀取元素的值,同樣體現在讀取屬性上。可能有些朋友
說可以換不同的xml解析方式來簡化,不是還有Sax解析,實在不行換用其它開
源的解決方案。

確實通過使用不同的解析xml的方式是會讓程式變得簡單點,但是每次xml
的結構發生變化過後,或多或少都是需要修改程式中解析xml部分的。
有沒有辦法解決這個問題呢?也就是當xml的結構發生改變過後,能夠很方
便的獲取相應元素、或者是屬性的值,而不用再去修改解析xml的程式。

使用模式來解決的思路

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

要寫成通用的程式碼,又有幾個問題要解決,如何組織這些通用的程式碼?如
何呼叫這些通用的程式碼?以何種方式來告訴這些通用程式碼,客戶端的需要?
要解決這些問題,其中的一個解決方案就是直譯器模式。在描述這個模式
的解決思路之前,先解釋兩個概念,一個是解析器(不是指xml的解析器),一
個是直譯器。

  • 1:這裡的解析器,指的是把描述客戶端呼叫要求的表示式,經過解析,形成一個抽
    象語法樹的程式,不是指xml的解析器。

  • 2:這裡的直譯器,指的是解釋抽象語法樹,並執行每個節點對應的功能的程式。

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

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

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

為表示式設計簡單的文法

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

如下:

約定表示式的文法如下:

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

示例說明

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

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

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

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

示例

用於處理自定義Xml取值表示式的介面
package cn.javass.dp.interpreter.example3;
/**
 * 用於處理自定義Xml取值表示式的介面
 */
public abstract class ReadXmlExpression {
    /**
     * 解釋表示式
     * @param c 上下文
     * @return 解析過後的值,為了通用,可能是單個值,也可能是多個值,
     *         因此就返回一個數組
     */
    public abstract String[] interpret(Context c);
}
元素作為非終結符對應的直譯器,解釋並執行中間元素
package cn.javass.dp.interpreter.example3;
import java.util.*;

import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
/**
 * 元素作為非終結符對應的直譯器,解釋並執行中間元素
 */
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;
    }
}
元素作為終結符對應的直譯器
package cn.javass.dp.interpreter.example3;

import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
/**
 * 元素作為終結符對應的直譯器
 */
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;
    }
}
屬性作為終結符對應的直譯器
package cn.javass.dp.interpreter.example3;
/**
 * 屬性作為終結符對應的直譯器
 */
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;
    }
}
XmlUtil
package cn.javass.dp.interpreter.example3;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.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樹中的不必要的Text Node物件
          doc.normalize();
          return doc;
    }
}
上下文,用來包含直譯器需要的一些全域性資訊
package cn.javass.dp.interpreter.example3;

import java.util.ArrayList;
import java.util.List;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
/**
 *  上下文,用來包含直譯器需要的一些全域性資訊
 */
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;
    }
}
客戶端
package cn.javass.dp.interpreter.example3;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;

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(cEle);
//      aEle.addEle(bEle);
//      bEle.addEle(cEle);

        //呼叫
        String ss[] = root.interpret(c);
        System.out.println("c的值是="+ss[0]);


//      //想要獲取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]);
    }
}

理解直譯器模式

認識直譯器模式

1:直譯器模式的功能

直譯器模式使用直譯器物件來表示和處理相應的語法規則,一般一個解釋
器處理一條語法規則。理論上來說,只要能用直譯器物件把符合語法的表示式表
示出來,而且能夠構成抽象的語法樹,那都可以使用直譯器模式來處理。

2:語法規則和直譯器

語法規則和直譯器之間是有對應關係的,一般一個直譯器處理一條語法規
則,但是反過來並不成立,一條語法規則是可以有多種解釋和處理的,也就是一
條語法規則可以對應多個直譯器物件。

3:上下文的公用性

上下文在直譯器模式中起到非常重要的作用,由於上下文會被傳遞到所有
的直譯器中,因此可以在上下文中儲存和訪問直譯器的狀態,比如前面的直譯器
可以儲存一些資料在上下文中,後面的直譯器就可以獲取這些值。

另外還可以通過上下文傳遞一些在直譯器外部,但是直譯器需要的資料,
也可以是一些全域性的,公共的資料。

上下文還有一個功能,可以提供所有直譯器物件的公共功能,類似於物件
組合,而不是使用繼承來獲取公共功能,在每個直譯器物件裡面都可以呼叫。

4:誰來構建抽象語法樹

在前面的示例中,大家已經發現,自己在客戶端手工來構建抽象語法樹,是很
麻煩的,但是在直譯器模式中,並沒有涉及這部分功能,只是負責對構建好的抽象語
法樹進行解釋處理。前面的測試簡單,所以手工構