设计模式(二十四)解释器模式
#1 場景問題# ##1.1 讀取配置文件## 考慮這樣一個實際的應(yīng)用,維護系統(tǒng)自定義的配置文件。
幾乎每個實際的應(yīng)用系統(tǒng)都有與應(yīng)用自身相關(guān)的配置文件,這個配置文件是由開發(fā)人員根據(jù)需要自定義的,系統(tǒng)運行時會根據(jù)配置的數(shù)據(jù)進行相應(yīng)的功能處理。
系統(tǒng)現(xiàn)有的配置數(shù)據(jù)很簡單,主要是JDBC所需要的數(shù)據(jù),還有默認讀取Spring的配置文件,目前系統(tǒng)只需要一個Spring的配置文件。示例如下:
<?xml version="1.0" encoding="UTF-8"?> <root><jdbc><driver-class>驅(qū)動類名</driver-class><url>連接數(shù)據(jù)庫的URL</url><user>連接數(shù)據(jù)庫的用戶名</user><password>連接數(shù)據(jù)庫的密碼</password></jdbc><application-xml>缺省讀取的Spring配置的文件名稱</application-xml> </root> 復(fù)制代碼現(xiàn)在的功能需求是:如何能夠靈活的讀取配置文件的內(nèi)容?
##1.2 不用模式的解決方案## 不就是讀取配置文件嗎?實現(xiàn)很簡單,直接讀取并解析xml就可以了。讀取xml的應(yīng)用包很多,這里都不用,直接采用最基礎(chǔ)的Dom解析就可以了。另外,讀取到xml中的值過后,后續(xù)如何處理,這里也不去管,這里只是實現(xiàn)把配置文件讀取并解析出來。
按照這個思路,很快就寫出了實現(xiàn)的代碼,示例代碼如下:
/*** 讀取配置文件*/ public class ReadAppXml {/*** 讀取配置文件內(nèi)容* @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中作為格式化內(nèi)容的空白而映射在DOM樹中的Text Node對象doc.normalize();//獲取jdbc的配置值NodeList jdbc = doc.getElementsByTagName("jdbc");//只有一個jdbc,獲取jdbc中的驅(qū)動類的名稱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-xmlNodeList applicationXmlNode =doc.getElementsByTagName("application-xml");String applicationXml = applicationXmlNode.item(0).getFirstChild().getNodeValue();System.out.println("applicationXml=="+applicationXml);} } 復(fù)制代碼##1.3 有何問題## 看了上面的實現(xiàn),多簡單啊,就是最基本的Dom解析嘛,要是采用其它的開源工具包,比如dom4j、jDom之類的來處理,會更簡單,這好像不值得一提呀,真的是這樣嗎?
請思考一個問題:如果配置文件的結(jié)構(gòu)需要變動呢?仔細想想,就會感覺出問題來了。還是先看例子,然后再來總結(jié)這個問題。
隨著開發(fā)的深入進行,越來越多可配置的數(shù)據(jù)被抽取出來,需要添加到配置文件中,比如與數(shù)據(jù)庫的連接配置:就加入了是否需要、是否使用DataSource等配置。除了這些還加入了一些其它需要配置的數(shù)據(jù),例如:系統(tǒng)管理員、日志記錄方式、緩存線程的間隔時長、默認讀取哪些Spring配置文件等等,示例如下:
<?xml version="1.0" encoding="UTF-8"?> <root><database-connection><connection-type>連接數(shù)據(jù)庫的類型,1-用Spring集成的方式(也就是不用下面兩種方式了),2-DataSource(就是使用JNDI),3-使用JDBC自己來連接數(shù)據(jù)庫</connection-type><jndi>DataSource的方式用,服務(wù)器數(shù)據(jù)源的JNDI名稱</jndi><jdbc>跟上面一樣,省略了</jdbc></database-connection><system-operator>系統(tǒng)管理員ID</system-operator><log><operate-type>記錄日志的方式,1-數(shù)據(jù)庫,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> 復(fù)制代碼有朋友可能會想,改變一下配置文件,值得大驚小怪嗎?對于應(yīng)用系統(tǒng)開發(fā)來講,這不是經(jīng)常發(fā)生的、很普通的一件事情嘛。
的確是這樣,改變一下配置文件不是件大事情,但是帶來的一系列麻煩也不容忽視,比如:修改了配置文件的結(jié)構(gòu),那么讀取配置文件的程序就需要做出相應(yīng)的變更;用來封裝配置文件數(shù)據(jù)的數(shù)據(jù)對象也需要相應(yīng)的修改;外部使用配置文件的地方,獲取數(shù)據(jù)的地方也會相應(yīng)變動。
當(dāng)然在這一系列麻煩中,最讓人痛苦的莫過于修改讀取配置文件的程序了,有時候幾乎是重寫。比如在使用Dom讀取第一個配置文件,讀取默認的Spring配置文件的值的時候,可能的片斷代碼示例如下:
// 獲取application-xml NodeList applicationXmlNode = doc.getElementsByTagName("application-xml"); String applicationXml = applicationXmlNode.item(0).getFirstChild().getNodeValue(); System.out.println("applicationXml=="+applicationXml); 復(fù)制代碼但是如果配置文件改成第二個,文件的結(jié)構(gòu)發(fā)生了改變,需要讀取的配置文件變成了多個了,讀取的程序也發(fā)生了改變,而且application-xml節(jié)點也不是直接從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"); //循環(huán)獲取每個application-xml元素的值 for(int i=0;i<appXmlNode.getLength();i++) {String applicationXml = appXmlNode.item(i).getFirstChild().getNodeValue();System.out.println("applicationXml=="+applicationXml); } 復(fù)制代碼仔細對比上面在xml變化前后讀取值的代碼,你會發(fā)現(xiàn),由于xml結(jié)構(gòu)的變化,導(dǎo)致讀取xml文件內(nèi)容的代碼,基本上完全重寫了。
問題還不僅僅限于讀取元素的值,同樣體現(xiàn)在讀取屬性上。可能有些朋友說可以換不同的xml解析方式來簡化,不是還有Sax解析,實在不行換用其它開源的解決方案。
確實通過使用不同的解析xml的方式是會讓程序變得簡單點,但是每次xml的結(jié)構(gòu)發(fā)生變化過后,或多或少都是需要修改程序中解析xml部分的。
有沒有辦法解決這個問題呢?也就是當(dāng)xml的結(jié)構(gòu)發(fā)生改變過后,能夠很方便的獲取相應(yīng)元素、或者是屬性的值,而不用再去修改解析xml的程序。
#2 解決方案# ##2.1 解釋器模式來解決## 用來解決上述問題的一個合理的解決方案,就是使用解釋器模式。那么什么是解釋器模式呢?
解釋器模式定義
這里的文法,簡單點說就是我們俗稱的“語法規(guī)則”。
應(yīng)用解釋器模式來解決的思路
要想解決當(dāng)xml的結(jié)構(gòu)發(fā)生改變后,不用修改解析部分的代碼,一個自然的思路就是要把解析部分的代碼寫成公共的,而且還要是通用的,能夠滿足各種xml取值的需要,比如:獲取單個元素的值,獲取多個相同名稱的元素的值,獲取單個元素的屬性的值,獲取多個相同名稱的元素的屬性的值,等等。
要寫成通用的代碼,又有幾個問題要解決,如何組織這些通用的代碼?如何調(diào)用這些通用的代碼?以何種方式來告訴這些通用代碼,客戶端的需要?
要解決這些問題,其中的一個解決方案就是解釋器模式。在描述這個模式的解決思路之前,先解釋兩個概念,一個是解析器(不是指xml的解析器),一個是解釋器。
這里的解析器,指的是把描述客戶端調(diào)用要求的表達式,經(jīng)過解析,形成一個抽象語法樹的程序,不是指xml的解析器。
這里的解釋器,指的是解釋抽象語法樹,并執(zhí)行每個節(jié)點對應(yīng)的功能的程序。
要解決通用解析xml的問題:
第一步:需要先設(shè)計一個簡單的表達式語言,在客戶端調(diào)用解析程序的時候,傳入用這個表達式語言描述的一個表達式,然后把這個表達式通過解析器的解析,形成一個抽象的語法樹。
第二步:解析完成后,自動調(diào)用解釋器來解釋抽象語法樹,并執(zhí)行每個節(jié)點所對應(yīng)的功能,從而完成通用的xml解析。
這樣一來,每次當(dāng)xml結(jié)構(gòu)發(fā)生了更改,也就是在客戶端調(diào)用的時候,傳入不同的表達式即可,整個解析xml過程的代碼都不需要再修改了。
##2.2 模式結(jié)構(gòu)和說明## 解釋器模式的結(jié)構(gòu)如圖21.1所示:
AbstractExpression:定義解釋器的接口,約定解釋器的解釋操作。
TerminalExpression:終結(jié)符解釋器,用來實現(xiàn)語法規(guī)則中和終結(jié)符相關(guān)的操作,不再包含其它的解釋器,如果用組合模式來構(gòu)建抽象語法樹的話,就相當(dāng)于組合模式中的葉子對象,可以有多種終結(jié)符解釋器。
NonterminalExpression:非終結(jié)符解釋器,用來實現(xiàn)語法規(guī)則中非終結(jié)符相關(guān)的操作,通常一個解釋器對應(yīng)一個語法規(guī)則,可以包含其它的解釋器,如果用組合模式來構(gòu)建抽象語法樹的話,就相當(dāng)于組合模式中的組合對象,可以有多種非終結(jié)符解釋器。
Context:上下文,通常包含各個解釋器需要的數(shù)據(jù),或是公共的功能。
Client:客戶端,指的是使用解釋器的客戶端,通常在這里去把按照語言的語法做的表達式,轉(zhuǎn)換成為使用解釋器對象描述的抽象語法樹,然后調(diào)用解釋操作。
##2.3 解釋器模式示例代碼##
先看看抽象表達式的定義,非常簡單,定義一個執(zhí)行解釋的方法,示例代碼如下:
/*** 抽象表達式*/ public abstract class AbstractExpression {/*** 解釋的操作* @param ctx 上下文對象*/public abstract void interpret(Context ctx); } 復(fù)制代碼再來看看終結(jié)符表達式的定義,示例代碼如下:
/*** 終結(jié)符表達式*/ public class TerminalExpression extends AbstractExpression{public void interpret(Context ctx) {//實現(xiàn)與語法規(guī)則中的終結(jié)符相關(guān)聯(lián)的解釋操作} } 復(fù)制代碼接下來該看看非終結(jié)符表達式的定義了,示例代碼如下:
/*** 非終結(jié)符表達式*/ public class NonterminalExpression extends AbstractExpression{public void interpret(Context ctx) {//實現(xiàn)與語法規(guī)則中的非終結(jié)符相關(guān)聯(lián)的解釋操作} } 復(fù)制代碼上下文的定義,示例代碼如下:
/*** 上下文,包含解釋器之外的一些全局信息*/ public class Context { } 復(fù)制代碼最后來看看客戶端的定義,示例代碼如下:
/*** 使用解釋器的客戶*/ public class Client {//主要按照語法規(guī)則對特定的句子構(gòu)建抽象語法樹//然后調(diào)用解釋操作 } 復(fù)制代碼看到這里,可能有些朋友會覺得,上面的示例代碼里面什么都沒有啊。這主要是因為解釋器模式是跟具體的語法規(guī)則聯(lián)系在一起的,沒有相應(yīng)的語法規(guī)則,自然寫不出對應(yīng)的處理代碼來。
但是這些示例還是有意義的,可以通過它們看出解釋器模式實現(xiàn)的基本架子,只是沒有內(nèi)部具體的處理罷了。
##2.4 使用解釋器模式重寫示例## 通過上面的講述可以看出,要使用解釋器模式,一個重要的前提就是要定義一套語法規(guī)則,也稱為文法。不管這套文法的規(guī)則是簡單還是復(fù)雜,必須有這么個東西,因為解釋器模式就是來按照這些規(guī)則進行解析并執(zhí)行相應(yīng)的功能的。
為表達式設(shè)計簡單的文法
為了通用,用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> 復(fù)制代碼約定表達式的文法如下:
**獲取單個元素的值:**從根元素開始,一直到想要獲取值的元素,元素中間用“/”分隔,根元素前不加“/”。比如表達式“root/a/b/c”就表示獲取根元素下、a元素下、b元素下的c元素的值;
**獲取單個元素的屬性的值:**要獲取值的屬性一定是表達式的最后一個元素的屬性,在最后一個元素后面添加“.”然后再加上屬性的名稱。比如表達式“root/a/b/c.name”就表示獲取根元素下、a元素下、b元素下、c元素的name屬性的值;
**獲取相同元素名稱的值,當(dāng)然是多個:**要獲取值的元素一定是表達式的最后一個元素,在最后一個元素后面添加“”就表示獲取根元素下、a元素下、b元素下的多個d元素的值的集合;
**獲取相同元素名稱的屬性的值,當(dāng)然也是多個:**要獲取屬性值的元素一定是表達式的最后一個元素,在最后一個元素后面添加“”。比如表達式“root/a/b/d”就表示獲取根元素下、a元素下、b元素下的多個d元素的id屬性的值的集合;
示例說明
為了示例的通用性
為了示例的通用性,就使用上面這個xml來實現(xiàn)功能,不去使用前面定義的具體的xml了,解決的方法是一樣的。
另外一個問題,解釋器模式主要解決的是“解釋抽象語法樹,并執(zhí)行每個節(jié)點所對應(yīng)的功能”,并不包含如何從一個表達式轉(zhuǎn)換成為抽象的語法樹。因此下面的范例就先來實現(xiàn)解釋器模式所要求的功能。至于如何從一個表達式轉(zhuǎn)換成為相應(yīng)的抽象語法樹,后面會給出一個示例。
對于抽象的語法樹這個樹狀結(jié)構(gòu),很明顯可以使用組合模式來構(gòu)建。解釋器模式把需要解釋的對象分成了兩大類,一類是節(jié)點元素,就是可以包含其它元素的組合元素,比如非終結(jié)符元素,對應(yīng)成為組合模式的Composite;另一類是終結(jié)符元素,相當(dāng)于組合模式的葉子對象。解釋整個抽象語法樹的過程,也就是執(zhí)行相應(yīng)對象的功能的過程。
比如上面的xml,對應(yīng)成為抽象語法樹,可能的結(jié)構(gòu)如下圖21.2所示:
具體示例從簡單的開始,先來演示獲取單個元素的值和單個元素的屬性的值。在看具體代碼前,先來看看此時系統(tǒng)的整體結(jié)構(gòu),如圖21.3所示:
(1)定義抽象的解釋器
要實現(xiàn)解釋器的功能,首先定義一個抽象的解釋器,來約束所有被解釋的語法對象,也就是節(jié)點元素和終結(jié)符元素都要實現(xiàn)的功能。示例代碼如下:
/*** 用于處理自定義Xml取值表達式的接口*/ public abstract class ReadXmlExpression {/*** 解釋表達式* @param c 上下文* @return 解析過后的值,為了通用,可能是單個值,也可能是多個值,* 因此就返回一個數(shù)組*/public abstract String[] interpret(Context c); } 復(fù)制代碼(2)定義上下文
上下文是用來封裝解釋器需要的一些全局?jǐn)?shù)據(jù),也可以在里面封裝一些解釋器的公共功能,可以相當(dāng)于各個解釋器的公共對象,示例代碼如下:
/*** 上下文,用來包含解釋器需要的一些全局信息*/ public class Context {/*** 上一個被處理的元素*/private Element preEle = null;/*** Dom解析Xml的Document對象*/private Document document = null;/*** 構(gòu)造方法* @param filePathName 需要讀取的xml的路徑和名字* @throws Exception*/public Context(String filePathName) throws Exception{//通過輔助的Xml工具類來獲取被解析的xml對應(yīng)的Document對象this.document = XmlUtil.getRoot(filePathName);}/*** 重新初始化上下文*/public void reInit(){preEle = null;}/*** 各個Expression公共使用的方法,* 根據(jù)父元素和當(dāng)前元素的名稱來獲取當(dāng)前的元素* @param pEle 父元素* @param eleName 當(dāng)前元素的名稱* @return 找到的當(dāng)前元素*/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;} } 復(fù)制代碼在上下文中使用了一個工具對象XmlUtil來獲取Document對象,就是Dom解析xml,獲取相應(yīng)的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文檔中作為格式化內(nèi)容的空白而映射在DOM樹中的TextNode對象doc.normalize();return doc;} } 復(fù)制代碼(3)定義元素作為非終結(jié)符對應(yīng)的解釋器
接下來該看看如何解釋執(zhí)行中間元素了,首先這個元素相當(dāng)于組合模式的Composite對象,因此需要對子元素進行維護,另外這個元素的解釋處理,只是需要把自己找到,作為下一個元素的父元素就好了。示例代碼如下:
/*** 元素作為非終結(jié)符對應(yīng)的解釋器,解釋并執(zhí)行中間元素*/ 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) {//先取出上下文里的當(dāng)前元素作為父級元素//查找到當(dāng)前元素名稱所對應(yīng)的xml元素,并設(shè)置回到上下文中Element pEle = c.getPreEle();if(pEle==null){//說明現(xiàn)在獲取的是根元素c.setPreEle(c.getDocument().getDocumentElement());}else{//根據(jù)父級元素和要查找的元素的名稱來獲取當(dāng)前的元素Element nowEle = c.getNowEle(pEle, eleName);//把當(dāng)前獲取的元素放到上下文里面c.setPreEle(nowEle);}//循環(huán)調(diào)用子元素的interpret方法String [] ss = null;for(ReadXmlExpression ele : eles){ss = ele.interpret(c);}return ss;} } 復(fù)制代碼(4)定義元素作為終結(jié)符對應(yīng)的解釋器
對于單個元素的處理,終結(jié)符有兩種,一個是元素終結(jié),一個是屬性終結(jié)。如果是元素終結(jié),就是要獲取元素的值;如果是屬性終結(jié),就是要獲取屬性的值。
分別來看看如何實現(xiàn)的,先看元素作為終結(jié)的解釋器,示例代碼如下:
/*** 元素作為終結(jié)符對應(yīng)的解釋器*/ public class ElementTerminalExpression extends ReadXmlExpression{/*** 元素的名字*/private String eleName = "";public ElementTerminalExpression(String name){this.eleName = name;} public String[] interpret(Context c) {//先取出上下文里的當(dāng)前元素作為父級元素Element pEle = c.getPreEle();//查找到當(dāng)前元素名稱所對應(yīng)的xml元素Element ele = null;if(pEle==null){//說明現(xiàn)在獲取的是根元素ele = c.getDocument().getDocumentElement();c.setPreEle(ele);}else{//根據(jù)父級元素和要查找的元素的名稱來獲取當(dāng)前的元素ele = c.getNowEle(pEle, eleName);//把當(dāng)前獲取的元素放到上下文里面c.setPreEle(ele);}//然后需要去獲取這個元素的值String[] ss = new String[1];ss[0] = ele.getFirstChild().getNodeValue();return ss;} } 復(fù)制代碼(5)定義屬性作為終結(jié)符對應(yīng)的解釋器
接下來看看屬性終結(jié)符的實現(xiàn),就會比較簡單,直接獲取最后的元素對象,然后獲取相應(yīng)的屬性的值,示例代碼如下:
/*** 屬性作為終結(jié)符對應(yīng)的解釋器*/ 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;} } 復(fù)制代碼(6)使用解釋器
定義好了各個解釋器的實現(xiàn),可以寫個客戶端來測試一下這些解釋器對象的功能了。使用解釋器的客戶端的工作會比較多,最主要的就是要組裝抽象的語法樹。
先來看看如何使用解釋器獲取單個元素的值,示例代碼如下:
public class Client {public static void main(String[] args) throws Exception {//準(zhǔn)備上下文Context c = new Context("InterpreterTest.xml"); //想要獲取c元素的值,也就是如下表達式的值:"root/a/b/c"//首先要構(gòu)建解釋器的抽象語法樹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);//調(diào)用String ss[] = root.interpret(c);System.out.println("c的值是="+ss[0]);} } 復(fù)制代碼把前面定義的xml取名叫作“InterpreterTest.xml”,放到當(dāng)前工程的根下面,運行看看,能正確獲取值嗎,運行的結(jié)果如下:
c的值是=12345 復(fù)制代碼再來測試一下獲取單個元素的屬性的值,示例代碼如下:
public class Client {public static void main(String[] args) throws Exception {//準(zhǔn)備上下文Context c = new Context("InterpreterTest.xml");//想要獲取c元素的name屬性,也就是如下表達式的值:"root/a/b/c.name"//這個時候c不是終結(jié)了,需要把c修改成ElementExpressioinElementExpression 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);//調(diào)用String ss[] = root.interpret(c);System.out.println("c的屬性name的值是="+ss[0]);//如果要使用同一個上下文,連續(xù)進行解析,需要重新初始化上下文對象//比如要連續(xù)的重新再獲取一次屬性name的值,當(dāng)然你可以重新組合元素,//重新解析,只要是在使用同一個上下文,就需要重新初始化上下文對象c.reInit();String ss2[] = root.interpret(c);System.out.println("重新獲取c的屬性name的值是="+ss2[0]);} } 復(fù)制代碼運行的結(jié)果如下:
c的屬性name的值是=testC 重新獲取c的屬性name的值是=testC 復(fù)制代碼就像前面講述的那樣,制定一種簡單的語言,讓客戶端用來表達從xml中取值的表達式的語言,然后為它們定義一種文法的表示,也就是語法規(guī)則,然后用解釋器對象來表示那些表達式,接下來通過運行解釋器來解釋并執(zhí)行這些功能。
但是從前面的示例中,我們只能看到客戶端直接使用解釋器對象,來表示客戶要從xml中取什么值的語法樹,而沒有看到如何從語言的表達式轉(zhuǎn)換成為這種解釋器的表示,這個功能是屬于解析器的功能,沒有劃分在標(biāo)準(zhǔn)的解釋器模式中,所以這里就先不演示,在后面會有示例來講解析器。
#3 模式講解# ##3.1 認識解釋器模式##
解釋器模式的功能 解釋器模式使用解釋器對象來表示和處理相應(yīng)的語法規(guī)則,一般一個解釋器處理一條語法規(guī)則。理論上來說,只要能用解釋器對象把符合語法的表達式表示出來,而且能夠構(gòu)成抽象的語法樹,那都可以使用解釋器模式來處理。
語法規(guī)則和解釋器 語法規(guī)則和解釋器之間是有對應(yīng)關(guān)系的,一般一個解釋器處理一條語法規(guī)則,但是反過來并不成立,一條語法規(guī)則是可以有多種解釋和處理的,也就是一條語法規(guī)則可以對應(yīng)多個解釋器對象。
上下文的公用性 上下文在解釋器模式中起到非常重要的作用,由于上下文會被傳遞到所有的解釋器中,因此可以在上下文中存儲和訪問解釋器的狀態(tài),比如前面的解釋器可以存儲一些數(shù)據(jù)在上下文中,后面的解釋器就可以獲取這些值。
另外還可以通過上下文傳遞一些在解釋器外部,但是解釋器需要的數(shù)據(jù),也可以是一些全局的,公共的數(shù)據(jù)。
上下文還有一個功能,就是可以提供所有解釋器對象的公共功能,類似于對象組合,而不是使用繼承來獲取公共功能,在每個解釋器對象里面都可以調(diào)用。
誰來構(gòu)建抽象語法樹 在前面的示例中,大家已經(jīng)發(fā)現(xiàn),自己在客戶端手工來構(gòu)建抽象語法樹,是很麻煩的,但是在解釋器模式中,并沒有涉及這部分功能,只是負責(zé)對構(gòu)建好的抽象語法樹進行解釋處理。前面的測試簡單,所以手工構(gòu)建抽象語法樹也不是特別困難的事,要是復(fù)雜了呢?如果還是手工創(chuàng)建,那跟修改解析xml的代碼也差不了多少。后面會給大家講到,可以提供解析器來實現(xiàn)把表達式轉(zhuǎn)換成為抽象語法樹。
還有一個問題,就是一條語法規(guī)則是可以對應(yīng)多個解釋器對象的,也就是說同一個元素,是可以轉(zhuǎn)換成多個解釋器對象的,這也就意味著同樣一個表達式,是可以構(gòu)成不同的抽象語法樹的,這也造成構(gòu)建抽象語法樹變得很困難,而且工作量很大。
誰負責(zé)解釋操作 只要定義好了抽象語法樹,肯定是解釋器來負責(zé)解釋執(zhí)行。雖然有不同的語法規(guī)則,但是解釋器不負責(zé)選擇究竟用哪一個解釋器對象來解釋執(zhí)行語法規(guī)則,選擇解釋器的功能在構(gòu)建抽象語法樹的時候就完成了。
所以解釋器只要忠實的按照抽象語法樹解釋執(zhí)行就好了。
解釋器模式的調(diào)用順序示意圖
解釋器模式的調(diào)用順序如圖21.4所示:
##3.2 讀取多個元素或?qū)傩缘闹?# 前面看過了如何獲取單個元素的值和單個元素的屬性的值,下面應(yīng)該來看看如何獲取多個元素的值,還有多個元素中相同名稱的屬性的值了。獲取多個值和前面獲取單個值的實現(xiàn)思路大致相同,只是在取值的時候需要循環(huán)整個NodelList,依次取值,而不是只取出第一個來。當(dāng)然,由于語法發(fā)生了變動,所以對應(yīng)的解釋器也需要發(fā)生改變。
首先是有了一個表示多個元素作為終結(jié)符的語法,比如“root/a/b/d”;其次有了一個表示多個元素的屬性作為終結(jié)符的語法,比如“root/a/b/d”中的“.id.id”。
還是看看代碼示例吧,會比較清楚。
-
解釋器接口沒有變化,原本就定義的是數(shù)組,早做好準(zhǔn)備了。
-
讀取Xml的工具類XmlUtil也沒有任何變化。
-
上下文做了一點改變。
把原來用來記錄上一次操作的元素,變成記錄上一次操作的多個元素的這么一個集合,然后為它提供相應(yīng)的getter/setter方法;
另外,原來根據(jù)父元素和當(dāng)前元素的名稱獲取當(dāng)前元素的方法,變成了根據(jù)父元素和當(dāng)前元素的名稱來獲取多個元素;
重新初始化上下文的方法里面,初始化的就是記錄上一次操作的多個元素的這個集合了;
具體的Context類的代碼示例如下:
/*** 上下文,用來包含解釋器需要的一些全局信息*/ public class Context {/*** Dom解析Xml的Document對象*/private Document document = null;/*** 上一次被處理的多個元素*/private List<Element> preEles = new ArrayList<Element>();/*** 構(gòu)造方法* @param filePathName 需要讀取的xml的路徑和名字* @throws Exception*/public Context(String filePathName) throws Exception{//通過輔助的Xml工具類來獲取被解析的xml對應(yīng)的Document對象this.document = XmlUtil.getRoot(filePathName);}/*** 重新初始化上下文*/public void reInit(){preEles = new ArrayList<Element>();}/*** 各個Expression公共使用的方法,* 根據(jù)父元素和當(dāng)前元素的名稱來獲取當(dāng)前的多個元素的集合* @param pEle 父元素* @param eleName 當(dāng)前元素的名稱* @return 當(dāng)前的多個元素的集合*/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;} } 復(fù)制代碼處理單個非終結(jié)符的對象ElementExpression,跟以前處理單個元素相比,主要是現(xiàn)在需要面向多個父元素,但是由于是單個非終結(jié)符的處理,因此在多個父元素下面去查找符合要求的元素,找到一個就停止,示例代碼如下:
/*** 單個元素作為非終結(jié)符的解釋器*/ 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;//把當(dāng)前獲取的元素放到上下文里面List<Element> nowEles = new ArrayList<Element>(); if(pEles.size()==0){//說明現(xiàn)在獲取的是根元素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);}//循環(huán)調(diào)用子元素的interpret方法String [] ss = null;for(ReadXmlExpression tempEle : eles){ss = tempEle.interpret(c);}return ss;} } 復(fù)制代碼用來處理單個元素作為終結(jié)符的類,也發(fā)生了一點改變,主要是從多個父元素去獲取當(dāng)前元素,如果當(dāng)前元素是多個,就取第一個,示例代碼如下:
/*** 元素作為終結(jié)符對應(yīng)的解釋器*/ public class ElementTerminalExpression extends ReadXmlExpression{/*** 元素的名字*/private String eleName = "";public ElementTerminalExpression(String name){this.eleName = name;}public String[] interpret(Context c) {//先取出上下文里的當(dāng)前元素作為父級元素List<Element> pEles = c.getPreEles();//查找到當(dāng)前元素名稱所對應(yīng)的xml元素Element ele = null;if(pEles.size() == 0){//說明現(xiàn)在獲取的是根元素ele = c.getDocument().getDocumentElement();}else{//獲取當(dāng)前的元素ele = c.getNowEles(pEles.get(0), eleName).get(0);}//然后需要去獲取這個元素的值String[] ss = new String[1];ss[0] = ele.getFirstChild().getNodeValue();return ss;} } 復(fù)制代碼新添加一個解釋器,用來解釋處理以多個元素的屬性作為終結(jié)符的情況,它的實現(xiàn)比較簡單,只要獲取到最后的多個元素對象,然后循環(huán)這些元素,一個一個取出相應(yīng)的屬性值就好了,示例代碼如下:
/*** 以多個元素的屬性做為終結(jié)符的解釋處理對象*/ 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()];//循環(huán)多個元素,獲取每個的屬性的值for(int i=0;i<ss.length;i++){ss[i] = eles.get(i).getAttribute(this.propName);}return ss;} } 復(fù)制代碼新添加一個解釋器,用來解釋處理以多個元素作為終結(jié)符的情況,示例代碼如下:
/*** 以多個元素作為終結(jié)符的解釋處理對象*/ 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();//獲取當(dāng)前的多個元素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;} } 復(fù)制代碼新添加一個解釋器,用來解釋處理以多個元素作為非終結(jié)符的情況,它的實現(xiàn)類似于以單個元素作為非終結(jié)符的情況,只是這次處理的是多個,需要循環(huán)處理,同樣需要維護子對象,在我們現(xiàn)在設(shè)計的語法中,多個元素后面是可以再加子元素的,最起碼可以加多個屬性的終結(jié)符對象,示例代碼如下:
/*** 多個元素做為非終結(jié)符的解釋處理對象*/ 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();//把當(dāng)前獲取的元素放到上下文里面,這次是獲取多個元素List<Element> nowEles = new ArrayList<Element>();for(Element ele : pEles){nowEles.addAll(c.getNowEles(ele, eleName));}c.setPreEles(nowEles);//循環(huán)調(diào)用子元素的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;} } 復(fù)制代碼終于可以寫客戶端來測試一下了,看看是否能實現(xiàn)期望的功能。先測試獲取多個元素的值的情況,示例代碼如下:
public class Client {public static void main(String[] args) throws Exception {//準(zhǔn)備上下文Context c = new Context("InterpreterTest.xml");//想要獲取多個d元素的值,也就是如下表達式的值:"root/a/b/d$"//首先要構(gòu)建解釋器的抽象語法樹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); //調(diào)用String ss[] = root.interpret(c);for(String s : ss){System.out.println("d的值是="+s); }} } 復(fù)制代碼運行結(jié)果如下:
d的值是=d1 d的值是=d2 d的值是=d3 d的值是=d4 復(fù)制代碼接下來測試一下獲取多個屬性值的情況,示例代碼如下:
public class Client {public static void main(String[] args) throws Exception {//準(zhǔn)備上下文Context c = new Context("InterpreterTest.xml");//想要獲取d元素的id屬性,也就是如下表達式的值:"a/b/d$.id$"//首先要構(gòu)建解釋器的抽象語法樹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);//調(diào)用String ss[] = root.interpret(c);for (String s : ss) {System.out.println("d的屬性id值是=" + s);}} } 復(fù)制代碼運行結(jié)果如下:
d的屬性id值是=1 d的屬性id值是=2 d的屬性id值是=3 d的屬性id值是=4 復(fù)制代碼也很簡單,是不是。只要學(xué)會了處理單個的值,處理多個值也就變得容易了,只要把原來獲取單個值的地方改成循環(huán)操作即可。
當(dāng)然,如果要使用同一個上下文,連續(xù)進行解析,是同樣需要重新初始化上下文對象的。你還可以嘗試一下,如果是想要獲取多個元素下的,多個元素的同一個屬性的值,能實現(xiàn)嗎?你自己去測試,應(yīng)該是可以實現(xiàn)的。
##3.3 解析器## 前面看完了解釋器部分的功能,看到只要構(gòu)建好了抽象語法樹,解釋器就能夠正確地解釋并執(zhí)行它,但是該如何得到這個抽象語法樹呢?前面的測試都是人工組合好抽象語法樹的,如果實際開發(fā)中還這樣做,基本上工作量跟修改解析xml的代碼差不多。
這就需要解析器出場了,這個程序?qū)iT負責(zé)把按照語法表達的表達式,解析轉(zhuǎn)換成為解釋器需要的抽象語法樹。當(dāng)然解析器是跟表達式的語法,還有解釋器對象緊密關(guān)聯(lián)的。
下面來示例一下解析器的實現(xiàn),把符合前面定義的語法的表達式,轉(zhuǎn)換成為前面實現(xiàn)的解釋器的抽象語法樹。解析器有很多種實現(xiàn)方式,沒有什么定式,只要能完成相應(yīng)的功能即可,比如表驅(qū)動、語法分析生成程序等等。這里的示例采用自己來分解表達式以實現(xiàn)構(gòu)建抽象語法樹的功能,沒有使用遞歸,是用循環(huán)實現(xiàn)的,當(dāng)然也可以用遞歸來做。
實現(xiàn)思路
要實現(xiàn)解析器也不復(fù)雜,大約有下面三個步驟:
第一步:把客戶端傳遞來的表達式進行分解,分解成為一個一個的元素,并用一個對應(yīng)的解析模型來封裝這個元素的一些信息;
第二步:根據(jù)每個元素的信息,轉(zhuǎn)化成相對應(yīng)的解析器對象;
第三步:按照先后順序,把這些解析器對象組合起來,就得到抽象語法樹了;
可能有朋友會說,為什么不把第一步和第二步合并,直接分解出一個元素就轉(zhuǎn)換成相應(yīng)的解析器對象呢?原因有兩個:
其一是功能分離,不要讓一個方法的功能過于復(fù)雜;
其二是為了今后的修改和擴展,現(xiàn)在語法簡單,所以轉(zhuǎn)換成解析器對象需要考慮的東西少,直接轉(zhuǎn)換也不難,但要是語法復(fù)雜了,直接轉(zhuǎn)換就很雜亂了;
事實上,封裝解析屬性的數(shù)據(jù)模型充當(dāng)了第一步和第二步操作間的接口,使第一步和第二步都變簡單了。
先來看看用來封裝每一個解析出來的元素對應(yīng)的屬性對象,示例代碼如下:
/*** 用來封裝每一個解析出來的元素對應(yīng)的屬性*/ public class ParserModel {/*** 是否單個值*/private boolean singleVlaue;/*** 是否屬性,不是屬性就是元素*/private boolean propertyValue;/*** 是否終結(jié)符*/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;} } 復(fù)制代碼看看解析器的實現(xiàn),代碼稍微復(fù)雜點,注釋很詳盡,為了整體展示解析器,就不去分開每步單講了,不過要注意一點:下面這種實現(xiàn)沒有考慮并發(fā)處理的情況,如果要用在多線程環(huán)境下,需要補充相應(yīng)的處理,特別提示一下。示例代碼如下:
/*** 根據(jù)語法來解析表達式,轉(zhuǎn)換成為相應(yīng)的抽象語法樹*/ public class Parser {/*** 私有化構(gòu)造器,避免外部無謂的創(chuàng)建對象實例*/private Parser(){//}//定義幾個常量,內(nèi)部使用private final static String BACKLASH = "/";private final static String DOT = ".";private final static String DOLLAR = "$";/*** 按照分解的先后記錄需要解析的元素的名稱*/private static List<String> listEle = null;/*** 傳入一個字符串表達式,通過解析,組合成為一個抽象的語法樹* @param expr 描述要取值的字符串表達式* @return 對應(yīng)的抽象語法樹*/public static ReadXmlExpression parse(String expr){//先初始化記錄需解析的元素的名稱的集 會listEle = new ArrayList<String>();//第一步:分解表達式,得到需要解析的元素名稱和該元素對應(yīng)的解析模型Map<String,ParserModel> mapPath = parseMapPath(expr);//第二步:根據(jù)節(jié)點的屬性轉(zhuǎn)換成為相應(yīng)的解釋器對象List<ReadXmlExpression> list = mapPath2Interpreter(mapPath);//第三步:組合抽象語法樹,一定要按照先后順序來組合,//否則對象的包含關(guān)系就亂了ReadXmlExpression returnRe = buildTree(list);return returnRe; }/*----------------------開始實現(xiàn)第一步-----------------------*//*** 按照從左到右順序來分解表達式,得到需要解析的元素名稱,* 還有該元素對應(yīng)的解析模型* @param expr 需要分解的表達式* @return 得到需要解析的元素名稱,還有該元素對應(yīng)的解析模型*/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()) {//還有下一個值,說明這不是最后一個元素//按照現(xiàn)在的語法,屬性必然在最后,因此也不是屬性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);//設(shè)置屬性前面的那個元素,自然不是最后一個,也不是屬性setParsePath(false,eleName,false,mapPath);//設(shè)置屬性,按照現(xiàn)在的語法定義,屬性只能是最后一個setParsePath(true,propName,true,mapPath);} else {//說明是取元素的值,而且是最后一個元素的值setParsePath(true,onePath,false,mapPath);}break;}}return mapPath;}/*** 按照分解出來的位置和名稱來設(shè)置需要解析的元素名稱,* 還有該元素對應(yīng)的解析模型* @param end 是否是最后一個* @param ele 元素名稱* @param propertyValue 是否是取屬性* @param mapPath 設(shè)置需要解析的元素名稱,還有該元素對應(yīng)的解析模型的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);}/*----------------------第一步實現(xiàn)結(jié)束-----------------------*//*----------------------開始實現(xiàn)第二步-----------------------*/ /*** 把分解出來的元素名稱,根據(jù)對應(yīng)的解析模型轉(zhuǎn)換成為相應(yīng)的解釋器對象* @param mapPath 分解出來的需解析的元素名稱,還有該元素對應(yīng)的解析模型* @return 把每個元素轉(zhuǎn)換成為相應(yīng)的解釋器對象后的集合*/private static List<ReadXmlExpression> mapPath2Interpreter(Map<String,ParserModel> mapPath){List<ReadXmlExpression> list = new ArrayList<ReadXmlExpression>();//一定要按照分解的先后順序來轉(zhuǎn)換成解釋器對象for(String key : listEle){ParserModel pm = mapPath.get(key);ReadXmlExpression obj = null;if(!pm.isEnd()){if(pm.isSingleVlaue()){//不是最后一個,是一個值,轉(zhuǎn)化為obj = new ElementExpression(key); }else{//不是最后一個,是多個值,轉(zhuǎn)化為obj = new ElementsExpression(key);}}else{if(pm.isPropertyValue()){if(pm.isSingleVlaue()){//是最后一個,是一個值,取屬性的值,轉(zhuǎn)化為obj = new PropertyTerminalExpression(key);}else{//是最后一個,是多個值,取屬性的值,轉(zhuǎn)化為obj = new PropertysTerminalExpression(key);}}else{if(pm.isSingleVlaue()){//是最后一個,是一個值,取元素的值,轉(zhuǎn)化為obj = new ElementTerminalExpression(key);}else{//是最后一個,是多個值,取元素的值,轉(zhuǎn)化為obj = new ElementsTerminalExpression(key);}}}//把轉(zhuǎn)換后的對象添加到集合中l(wèi)ist.add(obj);}return list;}/*----------------------第二步實現(xiàn)結(jié)束-----------------------*/ /*----------------------開始實現(xiàn)第三步-----------------------*/ 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{//把元素添加到上一個對象下面,同時把本對象設(shè)置成為oldRe,//作為下一個對象的父結(jié)點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;}/*----------------------第三步實現(xiàn)結(jié)束-----------------------*/ } 復(fù)制代碼看完這個稍長點的解析器程序,該來體會一下,有了它對我們的開發(fā)有什么好處,寫個客戶端來測試看看。現(xiàn)在的客戶端就非常簡單了,主要三步: 首先是設(shè)計好想要取值的表達式;
然后是通過解析器解析獲取抽象語法樹;
最后就是請求解釋器解釋并執(zhí)行這個抽象語法樹,就得到最后的結(jié)果了;
客戶端測試的示例代碼如下:
public class Client {public static void main(String[] args) throws Exception {//準(zhǔn)備上下文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);}//如果要使用同一個上下文,連續(xù)進行解析,需要重新初始化上下文對象c.reInit();ReadXmlExpression re2 = Parser.parse("root/a/b/d$");//請求解析,獲取返回值String ss2[] = re2.interpret(c);for (String s : ss2) {System.out.println("d的值是=" + s);}} } 復(fù)制代碼簡單多了吧!通過使用解釋器模式,自行設(shè)計一種簡單的語法,就可以用很簡單的表達式來獲取你想要的xml中的值了。有的朋友可能會想到XPath,沒錯,本章示例實現(xiàn)的功能就是類似于XPath的部分功能。
如果今后xml的結(jié)構(gòu)要是發(fā)生了變化,或者是想要獲取不同的值,基本上就是修改那個表達式而已,你可以試試看,能否完成前面實現(xiàn)過的功能。比如:
想要獲取c元素的值,表達式為:“root/a/b/c”;
想要獲取c元素的name屬性值,表達式為:“root/a/b/c.name”;
想要獲取d元素的值,表達式為:“root/a/b/d$”,獲取d的屬性上面已經(jīng)測試了;
##3.4 解釋器模式的優(yōu)缺點##
易于實現(xiàn)語法 在解釋器模式中,一條語法規(guī)則用一個解釋器對象來解釋執(zhí)行,對于解釋器的實現(xiàn)來講,功能就變得比較簡單,只需要考慮這一條語法規(guī)則的實現(xiàn)就好了,其它的都不用管。
易于擴展新的語法 正是由于采用一個解釋器對象負責(zé)一條語法規(guī)則的方式,使得擴展新的語法非常容易,擴展了新的語法,只需要創(chuàng)建相應(yīng)的解釋器對象,在創(chuàng)建抽象語法樹的時候使用這個新的解釋器對象就可以了。
不適合復(fù)雜的語法 如果語法特別復(fù)雜,構(gòu)建解釋器模式需要的抽象語法樹的工作是非常艱巨的,再加上有可能會需要構(gòu)建多個抽象語法樹。所以解釋器模式不太適合于復(fù)雜的語法,對于復(fù)雜的語法,使用語法分析程序或編譯器生成器可能會更好。
##3.5 思考解釋器模式##
解釋器模式的本質(zhì) 解釋器模式的本質(zhì):分離實現(xiàn),解釋執(zhí)行。
解釋器模式通過一個解釋器對象處理一個語法規(guī)則的方式,把復(fù)雜的功能分離開;然后選擇需要被執(zhí)行的功能,并把這些功能組合成為需要被解釋執(zhí)行的抽象語法樹;然后再按照抽象語法樹來解釋執(zhí)行,實現(xiàn)相應(yīng)的功能。
認識這個本質(zhì)對于識別和變形使用解釋器模式是很有作用的。從表面上看,解釋器模式是關(guān)注的我們平時不太用到的自定義語法的處理,但是從實質(zhì)上看,解釋器模式的思路仍然是分離、封裝、簡化,跟很多模式是一樣的。
比如可以使用解釋器模式模擬狀態(tài)模式的功能。如果把解釋器模式要處理的語法簡化到只有一個狀態(tài)標(biāo)記,把解釋器看成是對狀態(tài)的處理對象,對同一個表示狀態(tài)的語法,可以有很多不同的解釋器,也就是有很多不同的處理狀態(tài)的對象,然后在創(chuàng)建抽象語法樹的時候,簡化成根據(jù)狀態(tài)的標(biāo)記來創(chuàng)建相應(yīng)的解釋器,不用再構(gòu)建樹了。你看看這么簡化下來,是不是可以用解釋器模擬出狀態(tài)模式的功能呢?
同理,解釋器模式可以模擬實現(xiàn)策略模式的功能,裝飾器模式的功能等等,尤其是模擬裝飾器模式的功能,構(gòu)建抽象語法樹的過程,自然就對應(yīng)成為組合裝飾器的過程。
何時選用解釋器模式 建議在如下情況中,選用解釋器模式:
當(dāng)有一個語言需要解釋執(zhí)行,并且可以將該語言中的句子表示為一個抽象語法樹的時候,可以考慮使用解釋器模式。
在使用解釋器模式的時候,還有兩個特點需要考慮,一個是語法相對應(yīng)該比較簡單,太復(fù)雜的語法不合適使用解釋器模式;另一個是效率要求不是很高,對效率要求很高的情況下,不適合使用解釋器模式。
##3.6 相關(guān)模式##
解釋器模式和組合模式 這兩個模式可以組合使用。
通常解釋器模式都會使用組合模式來實現(xiàn),這樣能夠方便的構(gòu)建抽象語法樹。一般非終結(jié)符解釋器就相當(dāng)于組合模式中的組合對象,終結(jié)符解釋器就相當(dāng)于葉子對象。
解釋器模式和迭代器模式 這兩個模式可以組合使用。
由于解釋器模式通常使用組合模式來實現(xiàn),因此在遍歷整個對象結(jié)構(gòu)的時候,自然可以使用迭代器模式。
解釋器模式和享元模式 這兩個模式可以組合使用。
在使用解釋器模式的時候,可能會造成多個細粒度對象,比如會有各種各樣的終結(jié)符解釋器,而這些終結(jié)符解釋器對不同的表達式來說是一樣的,是可以共用的,因此可以引入享元模式來共享這些對象。
解釋器模式和訪問者模式 這兩個模式可以組合使用。
在解釋器模式中,語法規(guī)則和解釋器對象是有對應(yīng)關(guān)系的。語法規(guī)則的變動意味著功能的變化,自然會導(dǎo)致使用不同的解釋器對象;而且一個語法規(guī)則可以被不同的解釋器解釋執(zhí)行。
因此在構(gòu)建抽象語法樹的時候,如果每個節(jié)點所對應(yīng)的解釋器對象是固定的,這就意味著這個節(jié)點對應(yīng)的功能是固定的,那么就不得不根據(jù)需要來構(gòu)建不同的抽象語法樹。
為了讓構(gòu)建的抽象語法樹較為通用,那就要求解釋器的功能不要那么固定,要能很方便的改變解釋器的功能,這個時候問題就變成了,如何能夠很方便的更改樹形結(jié)構(gòu)中節(jié)點對象的功能了,訪問者模式可以很好的實現(xiàn)這個功能。
note:成年人的世界沒有容易
轉(zhuǎn)載于:https://juejin.im/post/5ce56e826fb9a07eee5ea5cd
總結(jié)
以上是生活随笔為你收集整理的设计模式(二十四)解释器模式的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微信网页授权登录(公众号)
- 下一篇: flutter进行自动编译操作步骤