一天一模式之20直譯器模式
本節課程概覽
學習直譯器模式
一:初識直譯器模式
包括:定義、結構、參考實現
二:體會直譯器模式
包括:場景問題、不用模式的解決方案、使用模式的解決方案
三:理解直譯器模式
包括:認識直譯器模式、讀取多個元素或屬性的值、解析器、
直譯器模式的優缺點
四:思考直譯器模式
包括:直譯器模式的本質、何時選用
初識直譯器模式
定義
給定一個語言,定義它的文法的一種表示,並定義一個直譯器,這個解釋
器使用該表示來解釋語言中的句子。
結構和說明
體會直譯器模式
AbstractExpression:
定義直譯器的介面,約定直譯器的解釋操作。
TerminalExpression:
終結符直譯器,用來實現語法規則中和終結符相關的操作,不再包含其它
的直譯器,如果用組合模式來構建抽象語法樹的話,就相當於組合模式中的葉子
物件,可以有多種終結符直譯器。
NonterminalExpression:
非終結符直譯器,用來實現語法規則中非終結符相關的操作,通常一個解
釋器對應一個語法規則,可以包含其它的直譯器,如果用組合模式來構建抽象語
法樹的話,就相當於組合模式中的組合物件,可以有多種非終結符直譯器。
Context:
上下文,通常包含各個直譯器需要的資料,或是公共的功能。
Client:
客戶端,指的是使用直譯器的客戶端,通常在這裡去把按照語言的語法做
的表示式,轉換成為使用直譯器物件描述的抽象語法樹,然後呼叫解釋操作。
體會直譯器模式
讀取配置檔案
考慮這樣一個實際的應用,維護系統自定義的配置檔案。幾乎每個實際的
應用系統都有與應用自身相關的配置檔案,這個配置檔案是由開發人員根據需要
自定義的,系統執行時會根據配置的資料進行相應的功能處理。
系統現有的配置資料很簡單,主要是JDBC所需要的資料,還有預設讀取
Spring的配置檔案,目前系統只需要一個Spring的配置檔案。示例如下:
示例
抽象表示式
package cn.javass.dp.interpreter.example2;
/**
* 抽象表示式
*/
public abstract class AbstractExpression {
/**
* 解釋的操作
* @param ctx 上下文物件
*/
public abstract void interpret(Context ctx);
}
終結符表示式
package cn.javass.dp.interpreter.example2;
/**
* 終結符表示式
*/
public class TerminalExpression extends AbstractExpression{
public void interpret(Context ctx) {
//實現與語法規則中的終結符相關聯的解釋操作
}
}
非終結符表示式===相當於組合物件
package cn.javass.dp.interpreter.example2;
import java.util.ArrayList;
import java.util.List;
/**
* 非終結符表示式===相當於組合物件
*/
public class NonterminalExpression extends AbstractExpression{
private List<AbstractExpression> list = new ArrayList<AbstractExpression>();
public void addAbstractExpression(AbstractExpression ae){
list.add(ae);
}
public void interpret(Context ctx) {
//實現與語法規則中的非終結符相關聯的解釋操作
}
}
上下文,包含直譯器之外的一些全域性資訊
package cn.javass.dp.interpreter.example2;
/**
* 上下文,包含直譯器之外的一些全域性資訊
*/
public class Context {
//多個直譯器 公共的屬性
//多個直譯器 公共的方法
}
使用直譯器的客戶
package cn.javass.dp.interpreter.example2;
/**
* 使用直譯器的客戶
*/
public class Client {
//第一大步:主要按照語法規則對 特定的句子 構建抽象語法樹
//注意,這個步驟不在直譯器模式裡面
//第二大步:然後呼叫解釋操作
//這個步驟才是直譯器模式要完成的工作
}
現在的功能需求是:如何能夠靈活的讀取配置檔案的內容?
不用模式的解決方案
不就是讀取配置檔案嗎?實現很簡單,直接讀取並解析xml就可以了。讀取
xml的應用包很多,這裡都不用,直接採用最基礎的Dom解析就可以了。另外,讀
取到xml中的值過後,後續如何處理,這裡也不去管,這裡只是實現把配置檔案
讀取並解析出來。
示例
app.xml
<?xml version="1.0" encoding="UTF-8"?>
<root>
<jdbc>
<driver-class>驅動類名</driver-class>
<url>連線資料庫的URL</url>
<user>連線資料庫的使用者名稱</user>
<password>連線資料庫的密碼</password>
</jdbc>
<application-xml>預設讀取的Spring配置的檔名稱</application-xml>
</root>
app2.xml
<?xml version="1.0" encoding="UTF-8"?>
<root>
<database-connection>
<connection-type>連線資料庫的型別,1-用Spring整合的方式(也就是不用下面兩種方式了),2-DataSource(就是使用JNDI),3-使用JDBC自己來連線資料庫</connection-type>
<jndi>DataSource的方式用,伺服器資料來源的JNDI名稱</jndi>
<jdbc>跟上面一樣,省略了</jdbc>
</database-connection>
<system-operator>系統管理員ID</system-operator>
<log>
<operate-type>記錄日誌的方式,1-資料庫,2-檔案</operate-type>
<file-name>記錄日誌的檔名稱</file-name>
</log>
<thread-interval>快取執行緒的間隔時長</thread-interval>
<spring-default>
<application-xmls>
<application-xml>預設讀取的Spring配置的檔名稱</application-xml>
<application-xml>其它需要讀取的Spring配置的檔名稱</application-xml>
</application-xmls>
</spring-default>
</root>
讀取配置檔案
package cn.javass.dp.interpreter.example1;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.*;
/**
* 讀取配置檔案
*/
public class ReadAppXml {
/**
* 讀取配置檔案內容
* @param filePathName 配置檔案的路徑和檔名
* @throws Exception
*/
public void read(String filePathName)throws Exception{
Document doc = null;
//建立一個解析器工廠
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
//獲得一個DocumentBuilder物件,這個物件代表了具體的DOM解析器
DocumentBuilder builder=factory.newDocumentBuilder();
//得到一個表示XML文件的Document物件
doc=builder.parse(filePathName);
//去掉XML文件中作為格式化內容的空白而對映在DOM樹中的不必要的Text Node物件
doc.normalize();
//獲取jdbc
// NodeList jdbc = doc.getElementsByTagName("jdbc");
// //只有一個jdbc,獲取jdbc中的驅動類的名稱
// NodeList driverClassNode = ((Element)jdbc.item(0)).getElementsByTagName("driver-class");
// String driverClass = driverClassNode.item(0).getFirstChild().getNodeValue();
// System.out.println("driverClass=="+driverClass);
// //同理獲取url、user、password等值
// NodeList urlNode = ((Element)jdbc.item(0)).getElementsByTagName("url");
// String url = urlNode.item(0).getFirstChild().getNodeValue();
// System.out.println("url=="+url);
//
// NodeList userNode = ((Element)jdbc.item(0)).getElementsByTagName("user");
// String user = userNode.item(0).getFirstChild().getNodeValue();
// System.out.println("user=="+user);
//
// NodeList passwordNode = ((Element)jdbc.item(0)).getElementsByTagName("password");
// String password = passwordNode.item(0).getFirstChild().getNodeValue();
// System.out.println("password=="+password);
//
//
// //獲取application-xml
// NodeList applicationXmlNode = doc.getElementsByTagName("application-xml");
// String applicationXml = applicationXmlNode.item(0).getFirstChild().getNodeValue();
// System.out.println("applicationXml=="+applicationXml);
//先要獲取spring-default,然後獲取application-xmls
//然後才能獲取application-xml
NodeList springDefaultNode = doc.getElementsByTagName("spring-default");
NodeList appXmlsNode = ((Element)springDefaultNode.item(0)).getElementsByTagName("application-xmls");
NodeList appXmlNode = ((Element)appXmlsNode.item(0)).getElementsByTagName("application-xml");
//迴圈獲取每個application-xml元素的值
for(int i=0;i<appXmlNode.getLength();i++){
String applicationXml = appXmlNode.item(i).getFirstChild().getNodeValue();
System.out.println("applicationXml=="+applicationXml);
}
}
public static void main(String[] args) throws Exception {
ReadAppXml t = new ReadAppXml();
t.read("App2.xml");
}
}
有何問題
看了上面的實現,多簡單啊,就是最基本的Dom解析嘛,要是採用其它的開
源工具包,比如dom4j、jDom之類的來處理,會更簡單,這好像不值得一提呀,
真的是這樣嗎?
請思考一個問題:如果配置檔案的結構需要變動呢?仔細想想,就會感覺
出問題來了。還是先看例子,然後再來總結這個問題。
隨著開發的深入進行,越來越多可配置的資料被抽取出來,需要新增到配
置檔案中,比如與資料庫的連線配置:就加入了是否需要、是否使用DataSource
等配置。除了這些還加入了一些其它需要配置的資料,例如:系統管理員、日誌
記錄方式、快取執行緒的間隔時長、預設讀取哪些Spring配置檔案等等,示例如
下:
改變一下配置檔案不是件大事情,但是帶來的一系列麻煩也不容忽視,比
如:修改了配置檔案的結構,那麼讀取配置檔案的程式就需要做出相應的變更;
用來封裝配置檔案資料的資料物件也需要相應的修改;外部使用配置檔案的地
方,獲取資料的地方也會相應變動。
當然在這一系列麻煩中,最讓人痛苦的莫過於修改讀取配置檔案的程式
了,有時候幾乎是重寫。比如在使用Dom讀取第一個配置檔案,讀取預設的
Spring配置檔案的值的時候,可能的片斷程式碼示例如下:
但是如果配置檔案改成第二個,檔案的結構發生了改變,需要讀取的配置
檔案變成了多個了,讀取的程式也發生了改變,而且application-xml節點也不是
直接從doc下獲取了。幾乎是完全重寫了,此時可能的片斷程式碼示例如下:
仔細對比上面在xml變化前後讀取值的程式碼,你會發現,由於xml結構的變
化,導致讀取xml檔案內容的程式碼,基本上完全重寫了。
問題還不僅僅限於讀取元素的值,同樣體現在讀取屬性上。可能有些朋友
說可以換不同的xml解析方式來簡化,不是還有Sax解析,實在不行換用其它開
源的解決方案。
確實通過使用不同的解析xml的方式是會讓程式變得簡單點,但是每次xml
的結構發生變化過後,或多或少都是需要修改程式中解析xml部分的。
有沒有辦法解決這個問題呢?也就是當xml的結構發生改變過後,能夠很方
便的獲取相應元素、或者是屬性的值,而不用再去修改解析xml的程式。
使用模式來解決的思路
要想解決當xml的結構發生改變後,不用修改解析部分的程式碼,一個自然的
思路就是要把解析部分的程式碼寫成公共的,而且還要是通用的,能夠滿足各種
xml取值的需要,比如:獲取單個元素的值,獲取多個相同名稱的元素的值,獲
取單個元素的屬性的值,獲取多個相同名稱的元素的屬性的值,等等。
要寫成通用的程式碼,又有幾個問題要解決,如何組織這些通用的程式碼?如
何呼叫這些通用的程式碼?以何種方式來告訴這些通用程式碼,客戶端的需要?
要解決這些問題,其中的一個解決方案就是直譯器模式。在描述這個模式
的解決思路之前,先解釋兩個概念,一個是解析器(不是指xml的解析器),一
個是直譯器。
1:這裡的解析器,指的是把描述客戶端呼叫要求的表示式,經過解析,形成一個抽
象語法樹的程式,不是指xml的解析器。2:這裡的直譯器,指的是解釋抽象語法樹,並執行每個節點對應的功能的程式。
要解決通用解析xml的問題,第一步:需要先設計一個簡單的表示式語言,
在客戶端呼叫解析程式的時候,傳入用這個表示式語言描述的一個表示式,然後
把這個表示式通過解析器的解析,形成一個抽象的語法樹。
第二步:解析完成後,自動呼叫直譯器來解釋抽象語法樹,並執行每個節
點所對應的功能,從而完成通用的xml解析。
這樣一來,每次當xml結構發生了更改,也就是在客戶端呼叫的時候,傳入
不同的表示式即可,整個解析xml過程的程式碼都不需要再修改了。
為表示式設計簡單的文法
為了通用,用root表示根元素,a、b、c、d等來代表元素,一個簡單的xml
如下:
約定表示式的文法如下:
- 1:獲取單個元素的值:從根元素開始,一直到想要獲取值的元素,元素中間用“/”分
隔,根元素前不加“/”。比如表示式“root/a/b/c”就表示獲取根元素下、a元素
下、b元素下的c元素的值 - 2:獲取單個元素的屬性的值:要獲取值的屬性一定是表示式的最後一個元素的屬性,在
最後一個元素後面新增“.”然後再加上屬性的名稱。如“root/a/b/c.name”就表示
獲取根元素下、a元素下、b元素下、c元素的name屬性的值 - 3:獲取相同元素名稱的值,當然是多個:要獲取值的元素一定是表示式的最後一個元
素,在最後一個元素後面新增“”就表示獲取根元素
下、a元素下、b元素下的多個d元素的值的集合 - 4:獲取相同元素名稱的屬性的值,當然也是多個:要獲取屬性值的元素一定是表示式的
最後一個元素,在最後一個元素後面新增“”。比如表示式“root/a/b/d”就表示獲
取根元素下、a元素下、b元素下的多個d元素的id屬性的值的集合
示例說明
為了示例的通用性,就使用上面這個xml來實現功能,不去使用前面定義的
具體的xml了,解決的方法是一樣的。
另外一個問題,直譯器模式主要解決的是“解釋抽象語法樹,並執行每個
節點所對應的功能”,並不包含如何從一個表示式轉換成為抽象的語法樹。因此
下面的範例就先來實現直譯器模式所要求的功能。至於如何從一個表示式轉換成
為相應的抽象語法樹,後面會給出一個示例。
對於抽象的語法樹這個樹狀結構,很明顯可以使用組合模式來構建。解釋
器模式把需要解釋的物件分成了兩大類,一類是節點元素,就是可以包含其它元
素的組合元素,比如非終結符元素,對應成為組合模式的Composite;另一類是
終結符元素,相當於組合模式的葉子物件。解釋整個抽象語法樹的過程,也就是
執行相應物件的功能的過程。
比如上面的xml,對應成為抽象語法樹,可能的結構如下圖
示例
用於處理自定義Xml取值表示式的介面
package cn.javass.dp.interpreter.example3;
/**
* 用於處理自定義Xml取值表示式的介面
*/
public abstract class ReadXmlExpression {
/**
* 解釋表示式
* @param c 上下文
* @return 解析過後的值,為了通用,可能是單個值,也可能是多個值,
* 因此就返回一個數組
*/
public abstract String[] interpret(Context c);
}
元素作為非終結符對應的直譯器,解釋並執行中間元素
package cn.javass.dp.interpreter.example3;
import java.util.*;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
/**
* 元素作為非終結符對應的直譯器,解釋並執行中間元素
*/
public class ElementExpression extends ReadXmlExpression{
/**
* 用來記錄組合的ReadXmlExpression元素
*/
private Collection<ReadXmlExpression> eles = new ArrayList<ReadXmlExpression>();
/**
* 元素的名稱
*/
private String eleName = "";
public ElementExpression(String eleName){
this.eleName = eleName;
}
public boolean addEle(ReadXmlExpression ele){
this.eles.add(ele);
return true;
}
public boolean removeEle(ReadXmlExpression ele){
this.eles.remove(ele);
return true;
}
public String[] interpret(Context c) {
//先取出上下文裡的當前元素作為父級元素
//查詢到當前元素名稱所對應的xml元素,並設定回到上下文中
Element pEle = c.getPreEle();
if(pEle==null){
//說明現在獲取的是根元素
c.setPreEle(c.getDocument().getDocumentElement());
}else{
//根據父級元素和要查詢的元素的名稱來獲取當前的元素
Element nowEle = c.getNowEle(pEle, eleName);
//把當前獲取的元素放到上下文裡面
c.setPreEle(nowEle);
}
//迴圈呼叫子元素的interpret方法
String [] ss = null;
for(ReadXmlExpression ele : eles){
ss = ele.interpret(c);
}
return ss;
}
}
元素作為終結符對應的直譯器
package cn.javass.dp.interpreter.example3;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
/**
* 元素作為終結符對應的直譯器
*/
public class ElementTerminalExpression extends ReadXmlExpression{
/**
* 元素的名字
*/
private String eleName = "";
public ElementTerminalExpression(String name){
this.eleName = name;
}
public String[] interpret(Context c) {
//先取出上下文裡的當前元素作為父級元素
Element pEle = c.getPreEle();
//查詢到當前元素名稱所對應的xml元素
Element ele = null;
if(pEle==null){
//說明現在獲取的是根元素
ele = c.getDocument().getDocumentElement();
c.setPreEle(ele);
}else{
//根據父級元素和要查詢的元素的名稱來獲取當前的元素
ele = c.getNowEle(pEle, eleName);
//把當前獲取的元素放到上下文裡面
c.setPreEle(ele);
}
//然後需要去獲取這個元素的值
String[] ss = new String[1];
ss[0] = ele.getFirstChild().getNodeValue();
return ss;
}
}
屬性作為終結符對應的直譯器
package cn.javass.dp.interpreter.example3;
/**
* 屬性作為終結符對應的直譯器
*/
public class PropertyTerminalExpression extends ReadXmlExpression{
/**
* 屬性的名字
*/
private String propName;
public PropertyTerminalExpression(String propName){
this.propName = propName;
}
public String[] interpret(Context c) {
//直接獲取最後的元素的屬性的值
String[] ss = new String[1];
ss[0] = c.getPreEle().getAttribute(this.propName);
return ss;
}
}
XmlUtil
package cn.javass.dp.interpreter.example3;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
public class XmlUtil {
public static Document getRoot(String filePathName) throws Exception{
Document doc = null;
//建立一個解析器工廠
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
//獲得一個DocumentBuilder物件,這個物件代表了具體的DOM解析器
DocumentBuilder builder=factory.newDocumentBuilder();
//得到一個表示XML文件的Document物件
doc=builder.parse(filePathName);
//去掉XML文件中作為格式化內容的空白而對映在DOM樹中的不必要的Text Node物件
doc.normalize();
return doc;
}
}
上下文,用來包含直譯器需要的一些全域性資訊
package cn.javass.dp.interpreter.example3;
import java.util.ArrayList;
import java.util.List;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
/**
* 上下文,用來包含直譯器需要的一些全域性資訊
*/
public class Context {
/**
* 上一個被處理的元素
*/
private Element preEle = null;
/**
* Dom解析Xml的Document物件
*/
private Document document = null;
/**
* 構造方法
* @param filePathName 需要讀取的xml的路徑和名字
* @throws Exception
*/
public Context(String filePathName) throws Exception{
//通過輔助的Xml工具類來獲取被解析的xml對應的Document物件
this.document = XmlUtil.getRoot(filePathName);
}
/**
* 重新初始化上下文
*/
public void reInit(){
preEle = null;
}
/**
* 各個Expression公共使用的方法,
* 根據父元素和當前元素的名稱來獲取當前的元素
* @param pEle 父元素
* @param eleName 當前元素的名稱
* @return 找到的當前元素
*/
public Element getNowEle(Element pEle,String eleName){
NodeList tempNodeList = pEle.getChildNodes();
for(int i=0;i<tempNodeList.getLength();i++){
if(tempNodeList.item(i) instanceof Element){
Element nowEle = (Element)tempNodeList.item(i);
if(nowEle.getTagName().equals(eleName)){
return nowEle;
}
}
}
return null;
}
public Element getPreEle() {
return preEle;
}
public void setPreEle(Element preEle) {
this.preEle = preEle;
}
public Document getDocument() {
return document;
}
}
客戶端
package cn.javass.dp.interpreter.example3;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
public class Client {
public static void main(String[] args) throws Exception {
//準備上下文
Context c = new Context("InterpreterTest.xml");
//想要獲取c元素的值,也就是如下表達式的值:"root/a/b/c"
//首先要構建直譯器的抽象語法樹
ElementExpression root = new ElementExpression("root");
// ElementExpression aEle = new ElementExpression("a");
// ElementExpression bEle = new ElementExpression("b");
ElementTerminalExpression cEle = new ElementTerminalExpression("c");
//組合起來
root.addEle(cEle);
// aEle.addEle(bEle);
// bEle.addEle(cEle);
//呼叫
String ss[] = root.interpret(c);
System.out.println("c的值是="+ss[0]);
// //想要獲取c元素的name屬性,也就是如下表達式的值:"root/a/b/c.name"
// //這個時候c不是終結了,需要把c修改成ElementExpressioin
// ElementExpression root = new ElementExpression("root");
// ElementExpression aEle = new ElementExpression("a");
// ElementExpression bEle = new ElementExpression("b");
// ElementExpression cEle = new ElementExpression("c");
// PropertyTerminalExpression prop = new PropertyTerminalExpression("name");
// //組合
// root.addEle(aEle);
// aEle.addEle(bEle);
// bEle.addEle(cEle);
// cEle.addEle(prop);
//
// //呼叫
// String ss[] = root.interpret(c);
// System.out.println("c的屬性name的值是="+ss[0]);
//
// //如果要使用同一個上下文,連續進行解析,需要重新初始化上下文物件
// //比如要連續的重新再獲取一次屬性name的值,當然你可以重新組合元素,
// //重新解析,只要是在使用同一個上下文,就需要重新初始化上下文物件
// c.reInit();
// String ss2[] = root.interpret(c);
// System.out.println("重新獲取c的屬性name的值是="+ss2[0]);
}
}
理解直譯器模式
認識直譯器模式
1:直譯器模式的功能
直譯器模式使用直譯器物件來表示和處理相應的語法規則,一般一個解釋
器處理一條語法規則。理論上來說,只要能用直譯器物件把符合語法的表示式表
示出來,而且能夠構成抽象的語法樹,那都可以使用直譯器模式來處理。
2:語法規則和直譯器
語法規則和直譯器之間是有對應關係的,一般一個直譯器處理一條語法規
則,但是反過來並不成立,一條語法規則是可以有多種解釋和處理的,也就是一
條語法規則可以對應多個直譯器物件。
3:上下文的公用性
上下文在直譯器模式中起到非常重要的作用,由於上下文會被傳遞到所有
的直譯器中,因此可以在上下文中儲存和訪問直譯器的狀態,比如前面的直譯器
可以儲存一些資料在上下文中,後面的直譯器就可以獲取這些值。
另外還可以通過上下文傳遞一些在直譯器外部,但是直譯器需要的資料,
也可以是一些全域性的,公共的資料。
上下文還有一個功能,可以提供所有直譯器物件的公共功能,類似於物件
組合,而不是使用繼承來獲取公共功能,在每個直譯器物件裡面都可以呼叫。
4:誰來構建抽象語法樹
在前面的示例中,大家已經發現,自己在客戶端手工來構建抽象語法樹,是很
麻煩的,但是在直譯器模式中,並沒有涉及這部分功能,只是負責對構建好的抽象語
法樹進行解釋處理。前面的測試簡單,所以手工構