设计模式(二十四)解释器模式
#1 場景問題# ##1.1 讀取配置文件## 考慮這樣一個實際的應用,維護系統自定義的配置文件。
幾乎每個實際的應用系統都有與應用自身相關的配置文件,這個配置文件是由開發人員根據需要自定義的,系統運行時會根據配置的數據進行相應的功能處理。
系統現有的配置數據很簡單,主要是JDBC所需要的數據,還有默認讀取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-xmlNodeList 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 解釋器模式來解決## 用來解決上述問題的一個合理的解決方案,就是使用解釋器模式。那么什么是解釋器模式呢?
解釋器模式定義
這里的文法,簡單點說就是我們俗稱的“語法規則”。
應用解釋器模式來解決的思路
要想解決當xml的結構發生改變后,不用修改解析部分的代碼,一個自然的思路就是要把解析部分的代碼寫成公共的,而且還要是通用的,能夠滿足各種xml取值的需要,比如:獲取單個元素的值,獲取多個相同名稱的元素的值,獲取單個元素的屬性的值,獲取多個相同名稱的元素的屬性的值,等等。
要寫成通用的代碼,又有幾個問題要解決,如何組織這些通用的代碼?如何調用這些通用的代碼?以何種方式來告訴這些通用代碼,客戶端的需要?
要解決這些問題,其中的一個解決方案就是解釋器模式。在描述這個模式的解決思路之前,先解釋兩個概念,一個是解析器(不是指xml的解析器),一個是解釋器。
這里的解析器,指的是把描述客戶端調用要求的表達式,經過解析,形成一個抽象語法樹的程序,不是指xml的解析器。
這里的解釋器,指的是解釋抽象語法樹,并執行每個節點對應的功能的程序。
要解決通用解析xml的問題:
第一步:需要先設計一個簡單的表達式語言,在客戶端調用解析程序的時候,傳入用這個表達式語言描述的一個表達式,然后把這個表達式通過解析器的解析,形成一個抽象的語法樹。
第二步:解析完成后,自動調用解釋器來解釋抽象語法樹,并執行每個節點所對應的功能,從而完成通用的xml解析。
這樣一來,每次當xml結構發生了更改,也就是在客戶端調用的時候,傳入不同的表達式即可,整個解析xml過程的代碼都不需要再修改了。
##2.2 模式結構和說明## 解釋器模式的結構如圖21.1所示:
AbstractExpression:定義解釋器的接口,約定解釋器的解釋操作。
TerminalExpression:終結符解釋器,用來實現語法規則中和終結符相關的操作,不再包含其它的解釋器,如果用組合模式來構建抽象語法樹的話,就相當于組合模式中的葉子對象,可以有多種終結符解釋器。
NonterminalExpression:非終結符解釋器,用來實現語法規則中非終結符相關的操作,通常一個解釋器對應一個語法規則,可以包含其它的解釋器,如果用組合模式來構建抽象語法樹的話,就相當于組合模式中的組合對象,可以有多種非終結符解釋器。
Context:上下文,通常包含各個解釋器需要的數據,或是公共的功能。
Client:客戶端,指的是使用解釋器的客戶端,通常在這里去把按照語言的語法做的表達式,轉換成為使用解釋器對象描述的抽象語法樹,然后調用解釋操作。
##2.3 解釋器模式示例代碼##
先看看抽象表達式的定義,非常簡單,定義一個執行解釋的方法,示例代碼如下:
/*** 抽象表達式*/ public abstract class AbstractExpression {/*** 解釋的操作* @param ctx 上下文對象*/public abstract void interpret(Context ctx); } 復制代碼再來看看終結符表達式的定義,示例代碼如下:
/*** 終結符表達式*/ public class TerminalExpression extends AbstractExpression{public void interpret(Context ctx) {//實現與語法規則中的終結符相關聯的解釋操作} } 復制代碼接下來該看看非終結符表達式的定義了,示例代碼如下:
/*** 非終結符表達式*/ public class NonterminalExpression extends AbstractExpression{public void interpret(Context ctx) {//實現與語法規則中的非終結符相關聯的解釋操作} } 復制代碼上下文的定義,示例代碼如下:
/*** 上下文,包含解釋器之外的一些全局信息*/ public class Context { } 復制代碼最后來看看客戶端的定義,示例代碼如下:
/*** 使用解釋器的客戶*/ public class Client {//主要按照語法規則對特定的句子構建抽象語法樹//然后調用解釋操作 } 復制代碼看到這里,可能有些朋友會覺得,上面的示例代碼里面什么都沒有啊。這主要是因為解釋器模式是跟具體的語法規則聯系在一起的,沒有相應的語法規則,自然寫不出對應的處理代碼來。
但是這些示例還是有意義的,可以通過它們看出解釋器模式實現的基本架子,只是沒有內部具體的處理罷了。
##2.4 使用解釋器模式重寫示例## 通過上面的講述可以看出,要使用解釋器模式,一個重要的前提就是要定義一套語法規則,也稱為文法。不管這套文法的規則是簡單還是復雜,必須有這么個東西,因為解釋器模式就是來按照這些規則進行解析并執行相應的功能的。
為表達式設計簡單的文法
為了通用,用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屬性的值;
**獲取相同元素名稱的值,當然是多個:**要獲取值的元素一定是表達式的最后一個元素,在最后一個元素后面添加“”就表示獲取根元素下、a元素下、b元素下的多個d元素的值的集合;
**獲取相同元素名稱的屬性的值,當然也是多個:**要獲取屬性值的元素一定是表達式的最后一個元素,在最后一個元素后面添加“”。比如表達式“root/a/b/d”就表示獲取根元素下、a元素下、b元素下的多個d元素的id屬性的值的集合;
示例說明
為了示例的通用性
為了示例的通用性,就使用上面這個xml來實現功能,不去使用前面定義的具體的xml了,解決的方法是一樣的。
另外一個問題,解釋器模式主要解決的是“解釋抽象語法樹,并執行每個節點所對應的功能”,并不包含如何從一個表達式轉換成為抽象的語法樹。因此下面的范例就先來實現解釋器模式所要求的功能。至于如何從一個表達式轉換成為相應的抽象語法樹,后面會給出一個示例。
對于抽象的語法樹這個樹狀結構,很明顯可以使用組合模式來構建。解釋器模式把需要解釋的對象分成了兩大類,一類是節點元素,就是可以包含其它元素的組合元素,比如非終結符元素,對應成為組合模式的Composite;另一類是終結符元素,相當于組合模式的葉子對象。解釋整個抽象語法樹的過程,也就是執行相應對象的功能的過程。
比如上面的xml,對應成為抽象語法樹,可能的結構如下圖21.2所示:
具體示例從簡單的開始,先來演示獲取單個元素的值和單個元素的屬性的值。在看具體代碼前,先來看看此時系統的整體結構,如圖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修改成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);//調用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 認識解釋器模式##
解釋器模式的功能 解釋器模式使用解釋器對象來表示和處理相應的語法規則,一般一個解釋器處理一條語法規則。理論上來說,只要能用解釋器對象把符合語法的表達式表示出來,而且能夠構成抽象的語法樹,那都可以使用解釋器模式來處理。
語法規則和解釋器 語法規則和解釋器之間是有對應關系的,一般一個解釋器處理一條語法規則,但是反過來并不成立,一條語法規則是可以有多種解釋和處理的,也就是一條語法規則可以對應多個解釋器對象。
上下文的公用性 上下文在解釋器模式中起到非常重要的作用,由于上下文會被傳遞到所有的解釋器中,因此可以在上下文中存儲和訪問解釋器的狀態,比如前面的解釋器可以存儲一些數據在上下文中,后面的解釋器就可以獲取這些值。
另外還可以通過上下文傳遞一些在解釋器外部,但是解釋器需要的數據,也可以是一些全局的,公共的數據。
上下文還有一個功能,就是可以提供所有解釋器對象的公共功能,類似于對象組合,而不是使用繼承來獲取公共功能,在每個解釋器對象里面都可以調用。
誰來構建抽象語法樹 在前面的示例中,大家已經發現,自己在客戶端手工來構建抽象語法樹,是很麻煩的,但是在解釋器模式中,并沒有涉及這部分功能,只是負責對構建好的抽象語法樹進行解釋處理。前面的測試簡單,所以手工構建抽象語法樹也不是特別困難的事,要是復雜了呢?如果還是手工創建,那跟修改解析xml的代碼也差不了多少。后面會給大家講到,可以提供解析器來實現把表達式轉換成為抽象語法樹。
還有一個問題,就是一條語法規則是可以對應多個解釋器對象的,也就是說同一個元素,是可以轉換成多個解釋器對象的,這也就意味著同樣一個表達式,是可以構成不同的抽象語法樹的,這也造成構建抽象語法樹變得很困難,而且工作量很大。
誰負責解釋操作 只要定義好了抽象語法樹,肯定是解釋器來負責解釋執行。雖然有不同的語法規則,但是解釋器不負責選擇究竟用哪一個解釋器對象來解釋執行語法規則,選擇解釋器的功能在構建抽象語法樹的時候就完成了。
所以解釋器只要忠實的按照抽象語法樹解釋執行就好了。
解釋器模式的調用順序示意圖
解釋器模式的調用順序如圖21.4所示:
##3.2 讀取多個元素或屬性的值## 前面看過了如何獲取單個元素的值和單個元素的屬性的值,下面應該來看看如何獲取多個元素的值,還有多個元素中相同名稱的屬性的值了。獲取多個值和前面獲取單個值的實現思路大致相同,只是在取值的時候需要循環整個NodelList,依次取值,而不是只取出第一個來。當然,由于語法發生了變動,所以對應的解釋器也需要發生改變。
首先是有了一個表示多個元素作為終結符的語法,比如“root/a/b/d”;其次有了一個表示多個元素的屬性作為終結符的語法,比如“root/a/b/d”中的“.id.id”。
還是看看代碼示例吧,會比較清楚。
-
解釋器接口沒有變化,原本就定義的是數組,早做好準備了。
-
讀取Xml的工具類XmlUtil也沒有任何變化。
-
上下文做了一點改變。
把原來用來記錄上一次操作的元素,變成記錄上一次操作的多個元素的這么一個集合,然后為它提供相應的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;} } 復制代碼處理單個非終結符的對象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;} } 復制代碼用來處理單個元素作為終結符的類,也發生了一點改變,主要是從多個父元素去獲取當前元素,如果當前元素是多個,就取第一個,示例代碼如下:
/*** 元素作為終結符對應的解釋器*/ 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;} } 復制代碼新添加一個解釋器,用來解釋處理以多個元素的屬性作為終結符的情況,它的實現比較簡單,只要獲取到最后的多個元素對象,然后循環這些元素,一個一個取出相應的屬性值就好了,示例代碼如下:
/*** 以多個元素的屬性做為終結符的解釋處理對象*/ 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;} } 復制代碼新添加一個解釋器,用來解釋處理以多個元素作為終結符的情況,示例代碼如下:
/*** 以多個元素作為終結符的解釋處理對象*/ 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;} } 復制代碼新添加一個解釋器,用來解釋處理以多個元素作為非終結符的情況,它的實現類似于以單個元素作為非終結符的情況,只是這次處理的是多個,需要循環處理,同樣需要維護子對象,在我們現在設計的語法中,多個元素后面是可以再加子元素的,最起碼可以加多個屬性的終結符對象,示例代碼如下:
/*** 多個元素做為非終結符的解釋處理對象*/ 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;} } 復制代碼終于可以寫客戶端來測試一下了,看看是否能實現期望的功能。先測試獲取多個元素的值的情況,示例代碼如下:
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的代碼差不多。
這就需要解析器出場了,這個程序專門負責把按照語法表達的表達式,解析轉換成為解釋器需要的抽象語法樹。當然解析器是跟表達式的語法,還有解釋器對象緊密關聯的。
下面來示例一下解析器的實現,把符合前面定義的語法的表達式,轉換成為前面實現的解釋器的抽象語法樹。解析器有很多種實現方式,沒有什么定式,只要能完成相應的功能即可,比如表驅動、語法分析生成程序等等。這里的示例采用自己來分解表達式以實現構建抽象語法樹的功能,沒有使用遞歸,是用循環實現的,當然也可以用遞歸來做。
實現思路
要實現解析器也不復雜,大約有下面三個步驟:
第一步:把客戶端傳遞來的表達式進行分解,分解成為一個一個的元素,并用一個對應的解析模型來封裝這個元素的一些信息;
第二步:根據每個元素的信息,轉化成相對應的解析器對象;
第三步:按照先后順序,把這些解析器對象組合起來,就得到抽象語法樹了;
可能有朋友會說,為什么不把第一步和第二步合并,直接分解出一個元素就轉換成相應的解析器對象呢?原因有兩個:
其一是功能分離,不要讓一個方法的功能過于復雜;
其二是為了今后的修改和擴展,現在語法簡單,所以轉換成解析器對象需要考慮的東西少,直接轉換也不難,但要是語法復雜了,直接轉換就很雜亂了;
事實上,封裝解析屬性的數據模型充當了第一步和第二步操作間的接口,使第一步和第二步都變簡單了。
先來看看用來封裝每一個解析出來的元素對應的屬性對象,示例代碼如下:
/*** 用來封裝每一個解析出來的元素對應的屬性*/ 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;} } 復制代碼看看解析器的實現,代碼稍微復雜點,注釋很詳盡,為了整體展示解析器,就不去分開每步單講了,不過要注意一點:下面這種實現沒有考慮并發處理的情況,如果要用在多線程環境下,需要補充相應的處理,特別提示一下。示例代碼如下:
/*** 根據語法來解析表達式,轉換成為相應的抽象語法樹*/ 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;}/*----------------------第三步實現結束-----------------------*/ } 復制代碼看完這個稍長點的解析器程序,該來體會一下,有了它對我們的開發有什么好處,寫個客戶端來測試看看。現在的客戶端就非常簡單了,主要三步: 首先是設計好想要取值的表達式;
然后是通過解析器解析獲取抽象語法樹;
最后就是請求解釋器解釋并執行這個抽象語法樹,就得到最后的結果了;
客戶端測試的示例代碼如下:
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 解釋器模式的優缺點##
易于實現語法 在解釋器模式中,一條語法規則用一個解釋器對象來解釋執行,對于解釋器的實現來講,功能就變得比較簡單,只需要考慮這一條語法規則的實現就好了,其它的都不用管。
易于擴展新的語法 正是由于采用一個解釋器對象負責一條語法規則的方式,使得擴展新的語法非常容易,擴展了新的語法,只需要創建相應的解釋器對象,在創建抽象語法樹的時候使用這個新的解釋器對象就可以了。
不適合復雜的語法 如果語法特別復雜,構建解釋器模式需要的抽象語法樹的工作是非常艱巨的,再加上有可能會需要構建多個抽象語法樹。所以解釋器模式不太適合于復雜的語法,對于復雜的語法,使用語法分析程序或編譯器生成器可能會更好。
##3.5 思考解釋器模式##
解釋器模式的本質 解釋器模式的本質:分離實現,解釋執行。
解釋器模式通過一個解釋器對象處理一個語法規則的方式,把復雜的功能分離開;然后選擇需要被執行的功能,并把這些功能組合成為需要被解釋執行的抽象語法樹;然后再按照抽象語法樹來解釋執行,實現相應的功能。
認識這個本質對于識別和變形使用解釋器模式是很有作用的。從表面上看,解釋器模式是關注的我們平時不太用到的自定義語法的處理,但是從實質上看,解釋器模式的思路仍然是分離、封裝、簡化,跟很多模式是一樣的。
比如可以使用解釋器模式模擬狀態模式的功能。如果把解釋器模式要處理的語法簡化到只有一個狀態標記,把解釋器看成是對狀態的處理對象,對同一個表示狀態的語法,可以有很多不同的解釋器,也就是有很多不同的處理狀態的對象,然后在創建抽象語法樹的時候,簡化成根據狀態的標記來創建相應的解釋器,不用再構建樹了。你看看這么簡化下來,是不是可以用解釋器模擬出狀態模式的功能呢?
同理,解釋器模式可以模擬實現策略模式的功能,裝飾器模式的功能等等,尤其是模擬裝飾器模式的功能,構建抽象語法樹的過程,自然就對應成為組合裝飾器的過程。
何時選用解釋器模式 建議在如下情況中,選用解釋器模式:
當有一個語言需要解釋執行,并且可以將該語言中的句子表示為一個抽象語法樹的時候,可以考慮使用解釋器模式。
在使用解釋器模式的時候,還有兩個特點需要考慮,一個是語法相對應該比較簡單,太復雜的語法不合適使用解釋器模式;另一個是效率要求不是很高,對效率要求很高的情況下,不適合使用解釋器模式。
##3.6 相關模式##
解釋器模式和組合模式 這兩個模式可以組合使用。
通常解釋器模式都會使用組合模式來實現,這樣能夠方便的構建抽象語法樹。一般非終結符解釋器就相當于組合模式中的組合對象,終結符解釋器就相當于葉子對象。
解釋器模式和迭代器模式 這兩個模式可以組合使用。
由于解釋器模式通常使用組合模式來實現,因此在遍歷整個對象結構的時候,自然可以使用迭代器模式。
解釋器模式和享元模式 這兩個模式可以組合使用。
在使用解釋器模式的時候,可能會造成多個細粒度對象,比如會有各種各樣的終結符解釋器,而這些終結符解釋器對不同的表達式來說是一樣的,是可以共用的,因此可以引入享元模式來共享這些對象。
解釋器模式和訪問者模式 這兩個模式可以組合使用。
在解釋器模式中,語法規則和解釋器對象是有對應關系的。語法規則的變動意味著功能的變化,自然會導致使用不同的解釋器對象;而且一個語法規則可以被不同的解釋器解釋執行。
因此在構建抽象語法樹的時候,如果每個節點所對應的解釋器對象是固定的,這就意味著這個節點對應的功能是固定的,那么就不得不根據需要來構建不同的抽象語法樹。
為了讓構建的抽象語法樹較為通用,那就要求解釋器的功能不要那么固定,要能很方便的改變解釋器的功能,這個時候問題就變成了,如何能夠很方便的更改樹形結構中節點對象的功能了,訪問者模式可以很好的實現這個功能。
note:成年人的世界沒有容易
轉載于:https://juejin.im/post/5ce56e826fb9a07eee5ea5cd
總結
以上是生活随笔為你收集整理的设计模式(二十四)解释器模式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微信网页授权登录(公众号)
- 下一篇: flutter进行自动编译操作步骤