解析器模式

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老师的设计模式是我目前看过最详细最有实践的教程。

相关文章
相关标签/搜索