解析器模式

1  場景問題

1.1  讀取配置文件

       考慮這樣一個實際的應用,維護系統自定義的配置文件。html

幾乎每一個實際的應用系統都有與應用自身相關的配置文件,這個配置文件是由開發人員根據須要自定義的,系統運行時會根據配置的數據進行相應的功能處理。java

       系統現有的配置數據很簡單,主要是JDBC所須要的數據,還有默認讀取Spring的配置文件,目前系統只須要一個Spring的配置文件。示例以下:spring

<?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>

如今的功能需求是:如何可以靈活的讀取配置文件的內容?數據庫

1.2  不用模式的解決方案

       不就是讀取配置文件嗎?實現很簡單,直接讀取並解析xml就能夠了。讀取xml的應用包不少,這裏都不用,直接採用最基礎的Dom解析就能夠了。另外,讀取到xml中的值事後,後續如何處理,這裏也不去管,這裏只是實現把配置文件讀取並解析出來。設計模式

       按照這個思路,很快就寫出了實現的代碼,示例代碼以下:數組

/**
 * 讀取配置文件
 */
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);
    }
}

1.3  有何問題

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

請思考一個問題:若是配置文件的結構須要變更呢?仔細想一想,就會感受出問題來了。仍是先看例子,而後再來總結這個問題。服務器

隨着開發的深刻進行,愈來愈多可配置的數據被抽取出來,須要添加到配置文件中,好比與數據庫的鏈接配置:就加入了是否須要、是否使用DataSource等配置。除了這些還加入了一些其它須要配置的數據,例如:系統管理員、日誌記錄方式、緩存線程的間隔時長、默認讀取哪些Spring配置文件等等,示例以下:多線程

<?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>

有朋友可能會想,改變一下配置文件,值得大驚小怪嗎?對於應用系統開發來說,這不是常常發生的、很普通的一件事情嘛。併發

       的確是這樣,改變一下配置文件不是件大事情,可是帶來的一系列麻煩也不容忽視,好比:修改了配置文件的結構,那麼讀取配置文件的程序就須要作出相應的變動;用來封裝配置文件數據的數據對象也須要相應的修改;外部使用配置文件的地方,獲取數據的地方也會相應變更。

       固然在這一系列麻煩中,最讓人痛苦的莫過於修改讀取配置文件的程序了,有時候幾乎是重寫。好比在使用Dom讀取第一個配置文件,讀取默認的Spring配置文件的值的時候,可能的片段代碼示例以下:

//獲取application-xml
    NodeList applicationXmlNode =
doc.getElementsByTagName("application-xml");
    String applicationXml = applicationXmlNode.item(0)
.getFirstChild().getNodeValue();
    System.out.println("applicationXml=="+applicationXml);

可是若是配置文件改爲第二個,文件的結構發生了改變,須要讀取的配置文件變成了多個了,讀取的程序也發生了改變,並且application-xml節點也不是直接從doc下獲取了。幾乎是徹底重寫了,此時可能的片段代碼示例以下:

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

   仔細對比上面在xml變化先後讀取值的代碼,你會發現,因爲xml結構的變化,致使讀取xml文件內容的代碼,基本上徹底重寫了。

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

       確實經過使用不一樣的解析xml的方式是會讓程序變得簡單點,可是每次xml的結構發生變化事後,或多或少都是須要修改程序中解析xml部分的。

       有沒有辦法解決這個問題呢?也就是當xml的結構發生改變事後,可以很方便的獲取相應元素、或者是屬性的值,而不用再去修改解析xml的程序。

2  解決方案

2.1  解釋器模式來解決

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

(1)解釋器模式定義

wKiom1lm7UKwjqcIAABo80hsrz0304.png

 這裏的文法,簡單點說就是咱們俗稱的「語法規則」。


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

       要想解決當xml的結構發生改變後,不用修改解析部分的代碼,一個天然的思路就是要把解析部分的代碼寫成公共的,並且還要是通用的,可以知足各類xml取值的須要,好比:獲取單個元素的值,獲取多個相同名稱的元素的值,獲取單個元素的屬性的值,獲取多個相同名稱的元素的屬性的值,等等。

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

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

  • 這裏的解析器,指的是把描述客戶端調用要求的表達式,通過解析,造成一個抽象語法樹的程序,不是指xml的解析器。

  • 這裏的解釋器,指的是解釋抽象語法樹,並執行每一個節點對應的功能的程序。

       要解決通用解析xml的問題,第一步:須要先設計一個簡單的表達式語言,在客戶端調用解析程序的時候,傳入用這個表達式語言描述的一個表達式,而後把這個表達式經過解析器的解析,造成一個抽象的語法樹。

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

       這樣一來,每次當xml結構發生了更改,也就是在客戶端調用的時候,傳入不一樣的表達式便可,整個解析xml過程的代碼都不須要再修改了。

2.2  模式結構和說明

解釋器模式的結構如圖所示:

wKiom1lm7dqABLGjAACllqVdNxA073.png


AbstractExpression

       定義解釋器的接口,約定解釋器的解釋操做。

TerminalExpression

       終結符解釋器,用來實現語法規則中和終結符相關的操做,再也不包含其它的解釋器,若是用組合模式來構建抽象語法樹的話,就至關於組合模式中的葉子對象,能夠有多種終結符解釋器。

NonterminalExpression

       非終結符解釋器,用來實現語法規則中非終結符相關的操做,一般一個解釋器對應一個語法規則,能夠包含其它的解釋器,若是用組合模式來構建抽象語法樹的話,就至關於組合模式中的組合對象,能夠有多種非終結符解釋器。

Context

       上下文,一般包含各個解釋器須要的數據,或是公共的功能。

Client

       客戶端,指的是使用解釋器的客戶端,一般在這裏去把按照語言的語法作的表達式,轉換成爲使用解釋器對象描述的抽象語法樹,而後調用解釋操做。

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 {
    //主要按照語法規則對特定的句子構建抽象語法樹
    //而後調用解釋操做
}

看到這裏,可能有些朋友會以爲,上面的示例代碼裏面什麼都沒有啊。這主要是由於解釋器模式是跟具體的語法規則聯繫在一塊兒的,沒有相應的語法規則,天然寫不出對應的處理代碼來。

可是這些示例仍是有意義的,能夠經過它們看出解釋器模式實現的基本架子,只是沒有內部具體的處理罷了

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,對應成爲抽象語法樹,可能的結構以下圖所示:

wKioL1lm7umQ4llXAAB21FLwDgI872.png

3:具體示例

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

wKiom1lm7yaw2ud9AAF1RwIktO0116.png

(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中取什麼值的語法樹,而沒有看到如何從語言的表達式轉換成爲這種解釋器的表示,這個功能是屬於解析器的功能,沒有劃分在標準的解釋器模式中,因此這裏就先不演示,在後面會有示例來說解析器。

3  模式講解

3.1  認識解釋器模式

(1)解釋器模式的功能

       解釋器模式使用解釋器對象來表示和處理相應的語法規則,通常一個解釋器處理一條語法規則。理論上來講,只要能用解釋器對象把符合語法的表達式表示出來,並且可以構成抽象的語法樹,那均可以使用解釋器模式來處理。

(2)語法規則和解釋器

       語法規則和解釋器之間是有對應關係的,通常一個解釋器處理一條語法規則,可是反過來並不成立,一條語法規則是能夠有多種解釋和處理的,也就是一條語法規則能夠對應多個解釋器對象。

(3)上下文的公用性

       上下文在解釋器模式中起到很是重要的做用,因爲上下文會被傳遞到全部的解釋器中,所以能夠在上下文中存儲和訪問解釋器的狀態,好比前面的解釋器能夠存儲一些數據在上下文中,後面的解釋器就能夠獲取這些值。

       另外還能夠經過上下文傳遞一些在解釋器外部,可是解釋器須要的數據,也能夠是一些全局的,公共的數據。

       上下文還有一個功能,就是能夠提供全部解釋器對象的公共功能,相似於對象組合,而不是使用繼承來獲取公共功能,在每一個解釋器對象裏面均可以調用。

(4)誰來構建抽象語法樹

       在前面的示例中,你們已經發現,本身在客戶端手工來構建抽象語法樹,是很麻煩的,可是在解釋器模式中,並無涉及這部分功能,只是負責對構建好的抽象語法樹進行解釋處理。前面的測試簡單,因此手工構建抽象語法樹也不是特別困難的事,要是複雜了呢?若是仍是手工建立,那跟修改解析xml的代碼也差不了多少。後面會給你們講到,能夠提供解析器來實現把表達式轉換成爲抽象語法樹。

還有一個問題,就是一條語法規則是能夠對應多個解釋器對象的,也就是說同一個元素,是能夠轉換成多個解釋器對象的,這也就意味着一樣一個表達式,是能夠構成不一樣的抽象語法樹的,這也形成構建抽象語法樹變得很困難,並且工做量很大。

(5)誰負責解釋操做

       只要定義好了抽象語法樹,確定是解釋器來負責解釋執行。雖然有不一樣的語法規則,可是解釋器不負責選擇究竟用哪個解釋器對象來解釋執行語法規則,選擇解釋器的功能在構建抽象語法樹的時候就完成了。

       因此解釋器只要忠實的按照抽象語法樹解釋執行就行了。

(6)解釋器模式的調用順序示意圖

       解釋器模式的調用順序如圖所示:

wKioL1lnDr_wYa4zAACm3pQEpo4196.png

3.2  讀取多個元素或屬性的值

       前面看過了如何獲取單個元素的值和單個元素的屬性的值,下面應該來看看如何獲取多個元素的值,還有多個元素中相同名稱的屬性的值了。

       獲取多個值和前面獲取單個值的實現思路大體相同,只是在取值的時候須要循環整個NodelList,依次取值,而不是隻取出第一個來。固然,因爲語法發生了變更,因此對應的解釋器也須要發生改變。

       首先是有了一個表示多個元素做爲終結符的語法,好比「root/a/b/d$」中的「d$」;其次有了一個表示多個元素的屬性做爲終結符的語法,好比「root/a/b/d$.id$」中的「.id$」;最後還有一個表示多個元素,但不是終結符的語法,好比「root/a/b/d$.id$」中的「d$」。

       仍是看看代碼示例吧,會比較清楚。

(1)解釋器接口沒有變化,本來就定義的是數組,早作好準備了。

(2)讀取Xml的工具類XmlUtil也沒有任何變化

(3)上下文作了一點改變:

  • 把原來用來記錄上一次操做的元素,變成記錄上一次操做的多個元素的這麼一個集合,而後爲它提供相應的getter/setter方法

  • 另外,原來根據父元素和當前元素的名稱獲取當前元素的方法,變成了根據父元素和當前元素的名稱來獲取多個元素

  • 從新初始化上下文的方法裏面,初始化的就是記錄上一次操做的多個元素的這個集合了

具體的Context類的代碼示例以下:

/**
 *  上下文,用來包含解釋器須要的一些全局信息
 */
public class Context {
    /**
     * Dom解析Xml的Document對象
     */
    private Document document = null;
    /**
     * 上一次被處理的多個元素
     */
    private List<Element> preEles = new ArrayList<Element>();
    /**
     * 構造方法
     * @param filePathName 須要讀取的xml的路徑和名字
     * @throws Exception
     */
    public Context(String filePathName) throws Exception{
       //經過輔助的Xml工具類來獲取被解析的xml對應的Document對象
       this.document = XmlUtil.getRoot(filePathName);
    }
    /**
     * 從新初始化上下文
     */
    public void reInit(){
       preEles = new ArrayList<Element>();
    }
    /**
     * 各個Expression公共使用的方法,
     * 根據父元素和當前元素的名稱來獲取當前的多個元素的集合
     * @param pEle 父元素 
     * @param eleName 當前元素的名稱
     * @return 當前的多個元素的集合
     */
    public List<Element> getNowEles(Element pEle,String eleName){
       List<Element> elements = new ArrayList<Element>();
       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)){
                  elements.add(nowEle);
              }
           }
       }
       return elements;
    }
   
    public Document getDocument() {
       return document;
    }
    public List<Element> getPreEles() {
       return preEles;
    }
    public void setPreEles(List<Element> nowEles) {
       this.preEles = nowEles;
    }
}

(4)處理單個非終結符的對象ElementExpression,跟之前處理單個元素相比,主要是如今須要面向多個父元素,可是因爲是單個非終結符的處理,所以在多個父元素下面去查找符合要求的元素,找到一個就中止,示例代碼以下:

/**
 * 單個元素做爲非終結符的解釋器
 */
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) {
       //先取出上下文裏的父級元素
       List<Element> pEles = c.getPreEles();
       Element ele = null;
       //把當前獲取的元素放到上下文裏面
       List<Element> nowEles = new ArrayList<Element>();      
       if(pEles.size()==0){
           //說明如今獲取的是根元素
           ele = c.getDocument().getDocumentElement();
           pEles.add(ele);
           c.setPreEles(pEles);
       }else{
           for(Element tempEle : pEles){
              nowEles.addAll(c.getNowEles(tempEle, eleName));
              if(nowEles.size()>0){
                  //找到一個就中止
                  break;
              }
           }
           List<Element> tempList = new ArrayList<Element>();
           tempList.add(nowEles.get(0));
           c.setPreEles(tempList);
       }
      
       //循環調用子元素的interpret方法
       String [] ss = null;
       for(ReadXmlExpression tempEle : eles){
           ss = tempEle.interpret(c);
       }
       return ss;
    }
}

(5)用來處理單個元素做爲終結符的類,也發生了一點改變,主要是從多個父元素去獲取當前元素,若是當前元素是多個,就取第一個,示例代碼以下:

/**
 * 元素做爲終結符對應的解釋器
 */
public class ElementTerminalExpression  extends ReadXmlExpression{
    /**
     * 元素的名字
     */
    private String eleName = "";
    public ElementTerminalExpression(String name){
       this.eleName = name;
    }
   
    public String[] interpret(Context c) {
       //先取出上下文裏的當前元素做爲父級元素
       List<Element> pEles = c.getPreEles();
       //查找到當前元素名稱所對應的xml元素
       Element ele = null;
       if(pEles.size() == 0){
           //說明如今獲取的是根元素
           ele = c.getDocument().getDocumentElement();
       }else{
           //獲取當前的元素
           ele = c.getNowEles(pEles.get(0), eleName).get(0);
       }
 
       //而後須要去獲取這個元素的值
       String[] ss = new String[1];
       ss[0] = ele.getFirstChild().getNodeValue();
       return ss;
    }
}

(6)新添加一個解釋器,用來解釋處理以多個元素的屬性做爲終結符的狀況,它的實現比較簡單,只要獲取到最後的多個元素對象,而後循環這些元素,一個一個取出相應的屬性值就行了,示例代碼以下:

/**
 * 以多個元素的屬性作爲終結符的解釋處理對象
 */
public class PropertysTerminalExpression
                                             extends ReadXmlExpression{
    /**
     * 屬性名字
     */
    private String propName;
    public PropertysTerminalExpression(String propName){
       this.propName = propName;
    }
   
    public String[] interpret(Context c) {
       //獲取最後的多個元素
       List<Element> eles = c.getPreEles();
      
       String[] ss = new String[eles.size()];
       //循環多個元素,獲取每一個的屬性的值
       for(int i=0;i<ss.length;i++){
           ss[i] = eles.get(i).getAttribute(this.propName);
       }
       return ss;
    }
}

(7)新添加一個解釋器,用來解釋處理以多個元素做爲終結符的狀況,示例代碼以下:

/**
 * 以多個元素做爲終結符的解釋處理對象
 */
public class ElementsTerminalExpression  extends
                                                 ReadXmlExpression{
    /**
     * 元素的名稱
     */
    private String eleName = "";
    public ElementsTerminalExpression(String name){
       this.eleName = name;
    }
   
    public String[] interpret(Context c) {
       //先取出上下文裏的父級元素
       List<Element> pEles = c.getPreEles();
       //獲取當前的多個元素
       List<Element> nowEles = new ArrayList<Element>();
      
       for(Element ele : pEles){
           nowEles.addAll(c.getNowEles(ele, eleName));
       }
 
       //而後須要去獲取這些元素的值
       String[] ss = new String[nowEles.size()];
       for(int i=0;i<ss.length;i++){
           ss[i] = nowEles.get(i).getFirstChild().getNodeValue();
       }
       return ss;
    }
}

(8)新添加一個解釋器,用來解釋處理以多個元素做爲非終結符的狀況,它的實現相似於以單個元素做爲非終結符的狀況,只是此次處理的是多個,須要循環處理,一樣須要維護子對象,在咱們如今設計的語法中,多個元素後面是能夠再加子元素的,最起碼能夠加多個屬性的終結符對象,示例代碼以下:

/**
 * 多個元素作爲非終結符的解釋處理對象
 */
public class ElementsExpression extends ReadXmlExpression{
    /**
     * 用來記錄組合的ReadXmlExpression元素
     */
    private Collection<ReadXmlExpression> eles =
                                  new ArrayList<ReadXmlExpression>();
    /**
     * 元素名字
     */
    private String eleName = "";
    public ElementsExpression(String eleName){
       this.eleName = eleName;
    }
   
    public String[] interpret(Context c) {
       //先取出上下文裏的父級元素
       List<Element> pEles = c.getPreEles();
       //把當前獲取的元素放到上下文裏面,此次是獲取多個元素
       List<Element> nowEles = new ArrayList<Element>();
      
       for(Element ele : pEles){
           nowEles.addAll(c.getNowEles(ele, eleName));
       }
       c.setPreEles(nowEles);
      
       //循環調用子元素的interpret方法
       String [] ss = null;
       for(ReadXmlExpression ele : eles){
           ss = ele.interpret(c);
       }
       return ss;
    }
   
    public boolean addEle(ReadXmlExpression ele){
       this.eles.add(ele);
       return true;
    }
    public boolean removeEle(ReadXmlExpression ele){
       this.eles.remove(ele);
       return true;
    }
}

(9)終於能夠寫客戶端來測試一下了,看看是否能實現指望的功能。先測試獲取多個元素的值的狀況,示例代碼以下:

public class Client {
    public static void main(String[] args) throws Exception {
       //準備上下文
       Context c = new Context("InterpreterTest.xml");
       //想要獲取多個d元素的值,也就是以下表達式的值:"root/a/b/d$"
       //首先要構建解釋器的抽象語法樹
       ElementExpression root = new ElementExpression("root");
       ElementExpression aEle = new ElementExpression("a");
       ElementExpression bEle = new ElementExpression("b");
       ElementsTerminalExpression dEle =
new ElementsTerminalExpression("d");
       //組合起來
       root.addEle(aEle);
       aEle.addEle(bEle);
       bEle.addEle(dEle);      
       //調用
        String ss[] = root.interpret(c);
       for(String s : ss){
           System.out.println("d的值是="+s);  
       }
    }
}

測試結果以下:

d的值是=d1
d的值是=d2
d的值是=d3
d的值是=d4

接下來測試一下獲取多個屬性值的狀況,示例代碼以下:

public class Client {
    public static void main(String[] args) throws Exception {
       //準備上下文
       Context c = new Context("InterpreterTest.xml");
 
       //想要獲取d元素的id屬性,也就是以下表達式的值:"a/b/d$.id$"
//首先要構建解釋器的抽象語法樹
       ElementExpression root = new ElementExpression("root");
       ElementExpression aEle = new ElementExpression("a");
       ElementExpression bEle = new ElementExpression("b");
       ElementsExpression dEle = new ElementsExpression("d");
       PropertysTerminalExpression prop =
new PropertysTerminalExpression("id");
       //組合
       root.addEle(aEle);
       aEle.addEle(bEle);
       bEle.addEle(dEle);
       dEle.addEle(prop);
 
       //調用
       String ss[] = root.interpret(c);
       for (String s : ss) {
           System.out.println("d的屬性id值是=" + s);
       }
    }
}

測試結果以下:

d的屬性id值是=1
d的屬性id值是=2
d的屬性id值是=3
d的屬性id值是=4

也很簡單,是否是。只要學會了處理單個的值,處理多個值也就變得容易了,只要把原來獲取單個值的地方改爲循環操做便可。

固然,若是要使用同一個上下文,連續進行解析,是一樣須要從新初始化上下文對象的。你還能夠嘗試一下,若是是想要獲取多個元素下的,多個元素的同一個屬性的值,能實現嗎?你本身去測試,應該是能夠實現的。

3.3  解析器

       前面看完了解釋器部分的功能,看到只要構建好了抽象語法樹,解釋器就可以正確地解釋並執行它,可是該如何獲得這個抽象語法樹呢?前面的測試都是人工組合好抽象語法樹的,若是實際開發中還這樣作,基本上工做量跟修改解析xml的代碼差很少。

       這就須要解析器出場了,這個程序專門負責把按照語法表達的表達式,解析轉換成爲解釋器須要的抽象語法樹。固然解析器是跟表達式的語法,還有解釋器對象緊密關聯的。

       下面來示例一下解析器的實現,把符合前面定義的語法的表達式,轉換成爲前面實現的解釋器的抽象語法樹。解析器有不少種實現方式,沒有什麼定式,只要能完成相應的功能便可,好比表驅動、語法分析生成程序等等。這裏的示例採用本身來分解表達式以實現構建抽象語法樹的功能,沒有使用遞歸,是用循環實現的,固然也能夠用遞歸來作。

(1)實現思路

       要實現解析器也不復雜,大約有下面三個步驟:

       第一步:把客戶端傳遞來的表達式進行分解,分解成爲一個一個的元素,並用一個對應的解析模型來封裝這個元素的一些信息。

       第二步:根據每一個元素的信息,轉化成相對應的解析器對象

       第三步:按照前後順序,把這些解析器對象組合起來,就獲得抽象語法樹了。

       可能有朋友會說,爲何不把第一步和第二步合併,直接分解出一個元素就轉換成相應的解析器對象呢?緣由有兩個:

  • 其一是功能分離,不要讓一個方法的功能過於複雜;

  • 其二是爲了從此的修改和擴展,如今語法簡單,因此轉換成解析器對象須要考慮的東西少,直接轉換也不難,但要是語法複雜了,直接轉換就很雜亂了。

事實上,封裝解析屬性的數據模型充當了第一步和第二步操做間的接口,使第一步和第二步都變簡單了。

(2)先來看看用來封裝每個解析出來的元素對應的屬性對象,示例代碼以下:

/**
 * 用來封裝每個解析出來的元素對應的屬性
 */
public class ParserModel {
    /**
     * 是否單個值
     */
    private boolean singleVlaue;
    /**
     * 是否屬性,不是屬性就是元素
     */
    private boolean propertyValue;
    /**
     * 是否終結符
     */
    private boolean end;
    public boolean isEnd() {
       return end;
    }
    public void setEnd(boolean end) {
       this.end = end;
    }
    public boolean isSingleVlaue() {
       return singleVlaue;
    }
    public void setSingleVlaue(boolean oneVlaue) {
       this.singleVlaue = oneVlaue;
    }
    public boolean isPropertyValue() {
       return propertyValue;
    }
    public void setPropertyValue(boolean propertyValue) {
       this.propertyValue = propertyValue;
    }
}

(3)看看解析器的實現,代碼稍微複雜點,註釋很詳盡,爲了總體展現解析器,就不去分開每步單講了,不過要注意一點:下面這種實現沒有考慮併發處理的狀況,若是要用在多線程環境下,須要補充相應的處理,特別提示一下。示例代碼以下:

/**
 * 根據語法來解析表達式,轉換成爲相應的抽象語法樹
 */
public class Parser {
    /**
     * 私有化構造器,避免外部無謂的建立對象實例
     */
    private Parser(){
       //
    }
    //定義幾個常量,內部使用
    private final static String BACKLASH = "/";
    private final static String DOT = ".";
    private final static String DOLLAR = "$";
    /**
     * 按照分解的前後記錄須要解析的元素的名稱
     */
    private static List<String> listEle = null;
 
    /**
     * 傳入一個字符串表達式,經過解析,組合成爲一個抽象的語法樹
     * @param expr 描述要取值的字符串表達式
     * @return 對應的抽象語法樹
     */
    public static ReadXmlExpression parse(String expr){
//先初始化記錄需解析的元素的名稱的集 會
       listEle = new ArrayList<String>();
 
       //第一步:分解表達式,獲得須要解析的元素名稱和該元素對應的解析模型
       Map<String,ParserModel> mapPath = parseMapPath(expr);
      
       //第二步:根據節點的屬性轉換成爲相應的解釋器對象
       List<ReadXmlExpression> list = mapPath2Interpreter(
mapPath);
       //第三步:組合抽象語法樹,必定要按照前後順序來組合,
       //不然對象的包含關係就亂了
       ReadXmlExpression returnRe = buildTree(list);
   
       return returnRe;        
    }
/*----------------------開始實現第一步-----------------------*/
    /**
     * 按照從左到右順序來分解表達式,獲得須要解析的元素名稱,
     * 還有該元素對應的解析模型
     * @param expr 須要分解的表達式
     * @return 獲得須要解析的元素名稱,還有該元素對應的解析模型
     */
    private static Map<String,ParserModel> parseMapPath(
String expr){
       //先按照/分割字符串
       StringTokenizer tokenizer = new StringTokenizer(
expr, BACKLASH);
       //初始化一個map用來存放分解出來的值
       Map<String,ParserModel> mapPath =
new HashMap<String,ParserModel>();
       while (tokenizer.hasMoreTokens()) {
           String onePath = tokenizer.nextToken();
           if (tokenizer.hasMoreTokens()) {
              //還有下一個值,說明這不是最後一個元素
              //按照如今的語法,屬性必然在最後,所以也不是屬性
              setParsePath(false,onePath,false,mapPath);
           } else {
              //說明到最後了
              int dotIndex = onePath.indexOf(DOT);
              if (dotIndex > 0) {
                  //說明是要獲取屬性的值,那就按照"."來分割,
                  //前面的就是元素名字,後面的是屬性的名字
                  String eleName = onePath.substring(0, dotIndex);
                  String propName = onePath.substring(dotIndex + 1);
                  //設置屬性前面的那個元素,天然不是最後一個,也不是屬性
                  setParsePath(false,eleName,false,mapPath);
                  //設置屬性,按照如今的語法定義,屬性只能是最後一個
                  setParsePath(true,propName,true,mapPath);
              } else {
                  //說明是取元素的值,並且是最後一個元素的值
                  setParsePath(true,onePath,false,mapPath);
              }
              break;
           }
       }
       return mapPath;
    }
    /**
     * 按照分解出來的位置和名稱來設置須要解析的元素名稱,
     * 還有該元素對應的解析模型
     * @param end 是不是最後一個
     * @param ele 元素名稱
     * @param propertyValue 是不是取屬性
     * @param mapPath 設置須要解析的元素名稱,還有該元素對應的解析模型的Map
     */
    private static void setParsePath(boolean end,String ele
          ,boolean propertyValue,Map<String,ParserModel> mapPath){
       ParserModel pm = new ParserModel();
       pm.setEnd(end);
       //若是帶有$符號就說明不是一個值
       pm.setSingleVlaue(!(ele.indexOf(DOLLAR)>0));
       pm.setPropertyValue(propertyValue);             
       //去掉$
       ele = ele.replace(DOLLAR, "");
       mapPath.put(ele,pm);
       listEle.add(ele);
    }
/*----------------------第一步實現結束-----------------------*/
 
/*----------------------開始實現第二步-----------------------*/  
    /**
     * 把分解出來的元素名稱,根據對應的解析模型轉換成爲相應的解釋器對象
     * @param mapPath 分解出來的需解析的元素名稱,還有該元素對應的解析模型
     * @return 把每一個元素轉換成爲相應的解釋器對象後的集合
     */
    private static List<ReadXmlExpression> mapPath2Interpreter(
Map<String,ParserModel> mapPath){
       List<ReadXmlExpression> list =
new ArrayList<ReadXmlExpression>();
       //必定要按照分解的前後順序來轉換成解釋器對象
       for(String key : listEle){
           ParserModel pm = mapPath.get(key);
           ReadXmlExpression obj = null;
           if(!pm.isEnd()){
              if(pm.isSingleVlaue()){
                  //不是最後一個,是一個值,轉化爲
                  obj = new ElementExpression(key);            
              }else{
                  //不是最後一個,是多個值,轉化爲
                  obj = new ElementsExpression(key);
              }
           }else{
              if(pm.isPropertyValue()){
                  if(pm.isSingleVlaue()){
                     //是最後一個,是一個值,取屬性的值,轉化爲
                     obj = new PropertyTerminalExpression(key);
                  }else{
                     //是最後一個,是多個值,取屬性的值,轉化爲
                     obj = new PropertysTerminalExpression(key);
                  }
              }else{
                  if(pm.isSingleVlaue()){
                     //是最後一個,是一個值,取元素的值,轉化爲
                     obj = new ElementTerminalExpression(key);
                  }else{
                     //是最後一個,是多個值,取元素的值,轉化爲
                     obj = new ElementsTerminalExpression(key);
                  }
              }
           }
           //把轉換後的對象添加到集合中
           list.add(obj);
       }
        return list;
    }
/*----------------------第二步實現結束-----------------------*/  
   
/*----------------------開始實現第三步-----------------------*/  
    private static ReadXmlExpression buildTree(
List<ReadXmlExpression> list){
       //第一個對象,也是返回去的對象,就是抽象語法樹的根
       ReadXmlExpression returnRe = null;
       //定義上一個對象
       ReadXmlExpression preRe = null;
       for(ReadXmlExpression re : list){        
           if(preRe==null){
              //說明是第一個元素
              preRe = re;
              returnRe = re;
           }else{
              //把元素添加到上一個對象下面,同時把本對象設置成爲oldRe,
              //做爲下一個對象的父結點
              if(preRe instanceof ElementExpression){
                  ElementExpression ele =
 (ElementExpression)preRe;
                  ele.addEle(re);
                  preRe = re;
              }else if(preRe instanceof ElementsExpression){
                  ElementsExpression eles =
 (ElementsExpression)preRe;
                  eles.addEle(re);
                  preRe = re;
              }
           }
       }
       return returnRe;
    }
/*----------------------第三步實現結束-----------------------*/
}

(4)看完這個稍長點的解析器程序,該來體會一下,有了它對咱們的開發有什麼好處,寫個客戶端來測試看看。如今的客戶端就很是簡單了,主要三步:

  • 首先是設計好想要取值的表達式

  • 而後是經過解析器解析獲取抽象語法樹

  • 最後就是請求解釋器解釋並執行這個抽象語法樹,就獲得最後的結果了

客戶端測試的示例代碼以下:

public class Client {
    public static void main(String[] args) throws Exception {
       //準備上下文
       Context c = new Context("InterpreterTest.xml");
       //經過解析器獲取抽象語法樹
       ReadXmlExpression re = Parser.parse("root/a/b/d$.id$");
       //請求解析,獲取返回值
       String ss[] = re.interpret(c);
 
       for (String s : ss) {
           System.out.println("d的屬性id值是=" + s);
       }
     
         //若是要使用同一個上下文,連續進行解析,須要從新初始化上下文對象
       c.reInit();
       ReadXmlExpression re2 = Parser.parse("root/a/b/d$");
       //請求解析,獲取返回值
       String ss2[] = re2.interpret(c);
       for (String s : ss2) {
           System.out.println("d的值是=" + s);
       }
    }
}

簡單多了吧!經過使用解釋器模式,自行設計一種簡單的語法,就能夠用很簡單的表達式來獲取你想要的xml中的值了。有的朋友可能會想到XPath,沒錯,本章示例實現的功能就是相似於XPath的部分功能。

若是從此xml的結構要是發生了變化,或者是想要獲取不一樣的值,基本上就是修改那個表達式而已,你能夠試試看,可否完成前面實現過的功能。好比:

  • 想要獲取c元素的值,表達式爲:「root/a/b/c」

  • 想要獲取c元素的name屬性值,表達式爲:「root/a/b/c.name」

  • 想要獲取d元素的值,表達式爲:「root/a/b/d$」,獲取d的屬性上面已經測試了

3.4  解釋器模式的優缺點

l          易於實現語法
    在解釋器模式中,一條語法規則用一個解釋器對象來解釋執行,對於解釋器的實現來說,功能就變得比較簡單,只須要考慮這一條語法規則的實現就行了,其它的都不用管。

l          易於擴展新的語法
    正是因爲採用一個解釋器對象負責一條語法規則的方式,使得擴展新的語法很是容易,擴展了新的語法,只須要建立相應的解釋器對象,在建立抽象語法樹的時候使用這個新的解釋器對象就能夠了。

l          不適合複雜的語法
    若是語法特別複雜,構建解釋器模式須要的抽象語法樹的工做是很是艱鉅的,再加上有可能會須要構建多個抽象語法樹。因此解釋器模式不太適合於複雜的語法,對於複雜的語法,使用語法分析程序或編譯器生成器可能會更好。

3.5  思考解釋器模式

1:解釋器模式的本質

解釋器模式的本質:分離實現,解釋執行

       解釋器模式經過一個解釋器對象處理一個語法規則的方式,把複雜的功能分離開;而後選擇須要被執行的功能,並把這些功能組合成爲須要被解釋執行的抽象語法樹;而後再按照抽象語法樹來解釋執行,實現相應的功能。

       認識這個本質對於識別和變形使用解釋器模式是頗有做用的。從表面上看,解釋器模式是關注的咱們平時不太用到的自定義語法的處理,可是從實質上看,解釋器模式的思路仍然是分離、封裝、簡化,跟不少模式是同樣的。

       好比可使用解釋器模式模擬狀態模式的功能。若是把解釋器模式要處理的語法簡化到只有一個狀態標記,把解釋器當作是對狀態的處理對象,對同一個表示狀態的語法,能夠有不少不一樣的解釋器,也就是有不少不一樣的處理狀態的對象,而後在建立抽象語法樹的時候,簡化成根據狀態的標記來建立相應的解釋器,不用再構建樹了。你看看這麼簡化下來,是否是能夠用解釋器模擬出狀態模式的功能呢?

       同理,解釋器模式能夠模擬實現策略模式的功能,裝飾器模式的功能等等,尤爲是模擬裝飾器模式的功能,構建抽象語法樹的過程,天然就對應成爲組合裝飾器的過程。

2:什麼時候選用解釋器模式

       建議在以下狀況中,選用解釋器模式:

  • 當有一個語言須要解釋執行,而且能夠將該語言中的句子表示爲一個抽象語法樹的時候,能夠考慮使用解釋器模式。
        在使用解釋器模式的時候,還有兩個特色須要考慮,一個是語法相對應該比較簡單,太複雜的語法不合適使用解釋器模式;另外一個是效率要求不是很高,對效率要求很高的狀況下,不適合使用解釋器模式。

3.6  相關模式

l          解釋器模式和組合模式
    這兩個模式能夠組合使用。
    一般解釋器模式都會使用組合模式來實現,這樣可以方便的構建抽象語法樹。通常非終結符解釋器就至關於組合模式中的組合對象,終結符解釋器就至關於葉子對象。

l          解釋器模式和迭代器模式
    這兩個模式能夠組合使用。
    因爲解釋器模式一般使用組合模式來實現,所以在遍歷整個對象結構的時候,天然可使用迭代器模式。

l          解釋器模式和享元模式
    這兩個模式能夠組合使用。
    在使用解釋器模式的時候,可能會形成多個細粒度對象,好比會有各類各樣的終結符解釋器,而這些終結符解釋器對不一樣的表達式來講是同樣的,是能夠共用的,所以能夠引入享元模式來共享這些對象。

l          解釋器模式和訪問者模式
    這兩個模式能夠組合使用。
    在解釋器模式中,語法規則和解釋器對象是有對應關係的。語法規則的變更意味着功能的變化,天然會致使使用不一樣的解釋器對象;並且一個語法規則能夠被不一樣的解釋器解釋執行。
    所以在構建抽象語法樹的時候,若是每一個節點所對應的解釋器對象是固定的,這就意味着這個節點對應的功能是固定的,那麼就不得不根據須要來構建不一樣的抽象語法樹。
    爲了讓構建的抽象語法樹較爲通用,那就要求解釋器的功能不要那麼固定,要能很方便的改變解釋器的功能,這個時候問題就變成了,如何可以很方便的更改樹形結構中節點對象的功能了,訪問者模式能夠很好的實現這個功能。


轉載至:http://sishuok.com/forum/blogPost/list/5667.html cc老師的設計模式是我目前看過最詳細最有實踐的教程。

相關文章
相關標籤/搜索