研磨设计模式 之 解释器模式(Interpreter)2——跟着cc学设计系列

21.2  解决方案

21.2.1  解释器模式来解决

用来解决上述问题的一个合理的解决方案,就是使用解释器模式。那么什么是解释器模式呢?

(1)解释器模式定义

 

       这里的文法,简单点说就是我们俗称的“语法规则”。

(2)应用解释器模式来解决的思路

       要想解决当xml的结构发生改变后,不用修改解析部分的代码,一个自然的思路就是要把解析部分的代码写成公共的,而且还要是通用的,能够满足各种xml取值的需要,比如:获取单个元素的值,获取多个相同名称的元素的值,获取单个元素的属性的值,获取多个相同名称的元素的属性的值,等等。

       要写成通用的代码,又有几个问题要解决,如何组织这些通用的代码?如何调用这些通用的代码?以何种方式来告诉这些通用代码,客户端的需要?

       要解决这些问题,其中的一个解决方案就是解释器模式。在描述这个模式的解决思路之前,先解释两个概念,一个是解析器(不是指xml的解析器),一个是解释器。

  • 这里的解析器,指的是把描述客户端调用要求的表达式,经过解析,形成一个抽象语法树的程序,不是指xml的解析器。
  • 这里的解释器,指的是解释抽象语法树,并执行每个节点对应的功能的程序。

       要解决通用解析xml的问题,第一步:需要先设计一个简单的表达式语言,在客户端调用解析程序的时候,传入用这个表达式语言描述的一个表达式,然后把这个表达式通过解析器的解析,形成一个抽象的语法树。

       第二步:解析完成后,自动调用解释器来解释抽象语法树,并执行每个节点所对应的功能,从而完成通用的xml解析。

       这样一来,每次当xml结构发生了更改,也就是在客户端调用的时候,传入不同的表达式即可,整个解析xml过程的代码都不需要再修改了。

21.2.2  模式结构和说明

解释器模式的结构如图21.1所示:

 

图21.1  解释器模式结构图

AbstractExpression

       定义解释器的接口,约定解释器的解释操作。

TerminalExpression

       终结符解释器,用来实现语法规则中和终结符相关的操作,不再包含其它的解释器,如果用组合模式来构建抽象语法树的话,就相当于组合模式中的叶子对象,可以有多种终结符解释器。

NonterminalExpression

       非终结符解释器,用来实现语法规则中非终结符相关的操作,通常一个解释器对应一个语法规则,可以包含其它的解释器,如果用组合模式来构建抽象语法树的话,就相当于组合模式中的组合对象,可以有多种非终结符解释器。

Context

       上下文,通常包含各个解释器需要的数据,或是公共的功能。

Client

       客户端,指的是使用解释器的客户端,通常在这里去把按照语言的语法做的表达式,转换成为使用解释器对象描述的抽象语法树,然后调用解释操作。

21.2.3  解释器模式示例代码

(1)先看看抽象表达式的定义,非常简单,定义一个执行解释的方法,示例代码如下:

/**

 * 抽象表达式

 */

public abstract class AbstractExpression {

    /**

     * 解释的操作

     * @param ctx 上下文对象

     */

    public abstract void interpret(Context ctx);

}

(2)再来看看终结符表达式的定义,示例代码如下:

/**

 * 终结符表达式

 */

public class TerminalExpression extends AbstractExpression{

    public void interpret(Context ctx) {

       //实现与语法规则中的终结符相关联的解释操作

    }

}

(3)接下来该看看非终结符表达式的定义了,示例代码如下:

/**

 * 非终结符表达式

 */

public class NonterminalExpression extends AbstractExpression{

    public void interpret(Context ctx) {

       //实现与语法规则中的非终结符相关联的解释操作

    }

}

(4)上下文的定义,示例代码如下:

/**

 * 上下文,包含解释器之外的一些全局信息

 */

public class Context {

}

(5)最后来看看客户端的定义,示例代码如下:

/**

 * 使用解释器的客户

 */

public class Client {

    //主要按照语法规则对特定的句子构建抽象语法树

    //然后调用解释操作

}

看到这里,可能有些朋友会觉得,上面的示例代码里面什么都没有啊。这主要是因为解释器模式是跟具体的语法规则联系在一起的,没有相应的语法规则,自然写不出对应的处理代码来。

但是这些示例还是有意义的,可以通过它们看出解释器模式实现的基本架子,只是没有内部具体的处理罢了。

21.2.4  使用解释器模式重写示例

       通过上面的讲述可以看出,要使用解释器模式,一个重要的前提就是要定义一套语法规则,也称为文法。不管这套文法的规则是简单还是复杂,必须有这么个东西,因为解释器模式就是来按照这些规则进行解析并执行相应的功能的。

1:为表达式设计简单的文法

为了通用,用root表示根元素,a、b、c、d等来代表元素,一个简单的xml如下:

<?xml version="1.0" encoding="UTF-8"?>

   

      

           12345

           d1

           d2

           d3

           d4

      

   

约定表达式的文法如下:

  • 获取单个元素的值:从根元素开始,一直到想要获取值的元素,元素中间用“/”分隔,根元素前不加“/”。比如表达式“root/a/b/c”就表示获取根元素下、a元素下、b元素下的c元素的值
  • 获取单个元素的属性的值:要获取值的属性一定是表达式的最后一个元素的属性,在最后一个元素后面添加“.”然后再加上属性的名称。比如表达式“root/a/b/c.name”就表示获取根元素下、a元素下、b元素下、c元素的name属性的值
  • 获取相同元素名称的值,当然是多个:要获取值的元素一定是表达式的最后一个元素,在最后一个元素后面添加“$”。比如表达式“root/a/b/d$”就表示获取根元素下、a元素下、b元素下的多个d元素的值的集合
  • 获取相同元素名称的属性的值,当然也是多个:要获取属性值的元素一定是表达式的最后一个元素,在最后一个元素后面添加“$”,然后在后面添加“.”然后再加上属性的名称,在属性名称后面也添加“$”。比如表达式“root/a/b/d$.id$”就表示获取根元素下、a元素下、b元素下的多个d元素的id属性的值的集合

2:示例说明

为了示例的通用性,就使用上面这个xml来实现功能,不去使用前面定义的具体的xml了,解决的方法是一样的。

另外一个问题,解释器模式主要解决的是“解释抽象语法树,并执行每个节点所对应的功能”,并不包含如何从一个表达式转换成为抽象的语法树。因此下面的范例就先来实现解释器模式所要求的功能。至于如何从一个表达式转换成为相应的抽象语法树,后面会给出一个示例。

对于抽象的语法树这个树状结构,很明显可以使用组合模式来构建。解释器模式把需要解释的对象分成了两大类,一类是节点元素,就是可以包含其它元素的组合元素,比如非终结符元素,对应成为组合模式的Composite;另一类是终结符元素,相当于组合模式的叶子对象。解释整个抽象语法树的过程,也就是执行相应对象的功能的过程。

比如上面的xml,对应成为抽象语法树,可能的结构如下图21.2所示:

 

图21.2  xml对应的抽象语法树示意图

3:具体示例

从简单的开始,先来演示获取单个元素的值和单个元素的属性的值。在看具体代码前,先来看看此时系统的整体结构,如图21.3所示:

 

图21.3  解释器模式示例的结构示意图

(1)定义抽象的解释器

要实现解释器的功能,首先定义一个抽象的解释器,来约束所有被解释的语法对象,也就是节点元素和终结符元素都要实现的功能。示例代码如下:

/**

 * 用于处理自定义Xml取值表达式的接口

 */

public abstract class ReadXmlExpression {

    /**

     * 解释表达式

     * @param c 上下文

     * @return 解析过后的值,为了通用,可能是单个值,也可能是多个值,

     *         因此就返回一个数组

     */

    public abstract String[] interpret(Context c);

}

(2)定义上下文

上下文是用来封装解释器需要的一些全局数据,也可以在里面封装一些解释器的公共功能,可以相当于各个解释器的公共对象,示例代码如下:

/**

 *  上下文,用来包含解释器需要的一些全局信息

 */

public class Context {

    /**

     * 上一个被处理的元素

     */

    private Element preEle = null;

    /**

     * Dom解析Xml的Document对象

     */

    private Document document = null;

    /**

     * 构造方法

     * @param filePathName 需要读取的xml的路径和名字

     * @throws Exception

     */

    public Context(String filePathName) throws Exception{

       //通过辅助的Xml工具类来获取被解析的xml对应的Document对象

       this.document = XmlUtil.getRoot(filePathName);

    }

    /**

     * 重新初始化上下文

     */

    public void reInit(){

       preEle = null;

    }

    /**

     * 各个Expression公共使用的方法,

     * 根据父元素和当前元素的名称来获取当前的元素

     * @param pEle 父元素

     * @param eleName 当前元素的名称

     * @return 找到的当前元素

     */

    public Element getNowEle(Element pEle,String eleName){

       NodeList tempNodeList = pEle.getChildNodes();

       for(int i=0;i

           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 eles =

new ArrayList();

    /**

     * 元素的名称

     */

    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中取什么值的语法树,而没有看到如何从语言的表达式转换成为这种解释器的表示,这个功能是属于解析器的功能,没有划分在标准的解释器模式中,所以这里就先不演示,在后面会有示例来讲解析器。

 


---------------------------------------------------------------------------

私塾在线学习网原创内容  跟着cc学设计系列 之 研磨设计模式

研磨设计讨论群【252780326】

原创内容,转载请注明出处【http://sishuok.com/forum/blogPost/list/0/5666.html

---------------------------------------------------------------------------

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/26715458/viewspace-1070988/,如需转载,请注明出处,否则将追究法律责任。

转载于:http://blog.itpub.net/26715458/viewspace-1070988/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值