研磨設計模式 之 直譯器模式(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】
---------------------------------------------------------------------------