摘要
JDK:1.8.0_202
# 一:场景问题
# 1.1 读取配置文件
考虑这样一个实际的应用,维护系统自定义的配置文件。
<?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>
2
3
4
5
6
7
8
9
10
11
12
现在的功能需求是:如何能够灵活的读取配置文件的内容?
# 1.2 不用模式的解决方案
解析XML:
import org.w3c.dom.*;
import javax.xml.parsers.*;
/**
* 读取配置文件
*/
public class ReadAppXml {
/**
* 读取配置文件内容
*
* @param filePathName 配置文件的路径和文件名
* @throws Exception 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();
// 获取a的配置值
NodeList a = doc.getElementsByTagName("a");
// 获得b的配置值
NodeList b = doc.getElementsByTagName("b");
// 只有一个c,获取c的名称
NodeList c = ((Element) b.item(0)).getElementsByTagName("c");
String cValue = c.item(0).getFirstChild().getNodeValue();
System.out.println("c:" + cValue);
// 获取d的值
NodeList d = ((Element) b.item(0)).getElementsByTagName("d");
for (int i = 0; i < d.getLength(); i++) {
System.out.println("d:" + d.item(i).getFirstChild().getNodeValue());
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
客户端:
public class Client {
public static void main(String[] args) throws Exception {
ReadAppXml readAppXml = new ReadAppXml();
String filePath = readAppXml.getClass().getClassLoader().getResource("interpreter.xml").getPath();
readAppXml.read(filePath);
}
}
2
3
4
5
6
7
8
9
运行结果:
# 1.3 有何问题
随着开发的深入进行,越来越多可配置的数据被抽取出来,需要添加到配置文件中,这时可能需要修改文件的结构、名称等等。这就导致了,如果xml结构的变化,导致读取xml文件内容的代码,基本上完全重写。有何解决这个问题?也就是当xml的结构发生改变过后,能够很方便的获取相应元素、或者是属性的值,而不用再去修改解析xml的程序
# 二:解决方案
用来解决上述问题的一个合理的解决方案,就是使用解释器模式。
解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
# 2.1 解决思路
要想解决当xml的结构发生改变后,不用修改解析部分的代码,一个自然的思路就是要把解析部分的代码写成公共的,而且还要是通用的,能够满足各种xml取值的需要,比如:获取单个元素的值,获取多个相同名称的元素的值,获取单个元素的属性的值,获取多个相同名称的元素的属性的值,等等。
要写成通用的代码,又有几个问题要解决,如何组织这些通用的代码?如何调用这些通用的代码?以何种方式来告诉这些通用代码,客户端的需要?
要解决这些问题,其中的一个解决方案就是解释器模式。在描述这个模式的解决思路之前,先解释两个概念,一个是解析器(不是指xml的解析器),一个是解释器。
这里的解析器,指的是把描述客户端调用要求的表达式,经过解析,形成一个抽象语法树的程序,不是指xml的解析器。
这里的解释器,指的是解释抽象语法树,并执行每个节点对应的功能的程序。
要解决通用解析xml的问题:
第一步:需要先设计一个简单的表达式语言,在客户端调用解析程序的时候,传入用这个表达式语言描述的一个表达式,然后把这个表达式通过解析器的解析,形成一个抽象的语法树。
第二步:解析完成后,自动调用解释器来解释抽象语法树,并执行每个节点所对应的功能,从而完成通用的xml解析。
这样一来,每次当xml结构发生了更改,也就是在客户端调用的时候,传入不同的表达式即可,整个解析xml过程的代码都不需要再修改了。
# 2.2 模式结构和说明
- AbstractExpression:定义解释器的接口,约定解释器的解释操作
- TerminalExpression:终结符解释器,用来实现语法规则中和终结符相关的操作,不再包含其它的解释器,如果用组合模式来构建抽象语法树的话,就相当于组合模式中的叶子对象,可以有多种终结符解释器
- NonterminalExpression:非终结符解释器,用来实现语法规则中非终结符相关的操作,通常一个解释器对应一个语法规则,可以包含其它的解释器,如果用组合模式来构建抽象语法树的话,就相当于组合模式中的组合对象,可以有多种非终结符解释器
- Context:上下文,通常包含各个解释器需要的数据,或是公共的功能
- Client:客户端,指的是使用解释器的客户端,通常在这里去把按照语言的语法做的表达式,转换成为使用解释器对象描述的抽象语法树,然后调用解释操作
# 2.3 示例代码
抽象表达式的定义,定义一个执行解释的方法:
/**
* 抽象表达式
*/
public abstract class AbstractExpression {
/**
* 解释的操作
*
* @param ctx 上下文对象
*/
public abstract void interpret(Context ctx);
}
2
3
4
5
6
7
8
9
10
11
终结符表达式的定义:
/**
* 终结符表达式
*/
public class TerminalExpression extends AbstractExpression {
public void interpret(Context ctx) {
// 实现与语法规则中的终结符相关联的解释操作
}
}
2
3
4
5
6
7
8
非终结符表达式的定义:
/**
* 非终结符表达式
*/
public class NonterminalExpression extends AbstractExpression {
public void interpret(Context ctx) {
// 实现与语法规则中的非终结符相关联的解释操作
}
}
2
3
4
5
6
7
8
上下文的定义:
/**
* 上下文,包含解释器之外的一些全局信息
*/
public class Context {
}
2
3
4
5
客户端的定义:
/**
* 使用解释器的客户
*/
public class Client {
// 主要按照语法规则对特定的句子构建抽象语法树
// 然后调用解释操作
}
2
3
4
5
6
7
# 2.4 重写案例
要使用解释器模式,一个重要的前提就是要定义一套语法规则,也称为文法。不管这套文法的规则是简单还是复杂,必须有这么个东西,因为解释器模式就是来按照这些规则进行解析并执行相应的功能的。
根据上面例子中的 XML 约定的表达式的文法如下:
获取单个元素的值:从根元素开始,一直到想要获取值的元素,元素中间用“/”分隔,根元素前不加“/”。比如表达式“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属性的值的集合;
说明:
对于抽象的语法树这个树状结构,很明显可以使用组合模式来构建。解释器模式把需要解释的对象分成了两大类,一类是节点元素,就是可以包含其它元素的组合元素,比如非终结符元素,对应成为组合模式的Composite;另一类是终结符元素,相当于组合模式的叶子对象。解释整个抽象语法树的过程,也就是执行相应对象的功能的过程。
比如上面的xml,对应成为抽象语法树,可能的结构如下图所示:
整体结构
定义抽象的解释器:
要实现解释器的功能,首先定义一个抽象的解释器,来约束所有被解释的语法对象,也就是节点元素和终结符元素都要实现的功能。
/**
* 用于处理自定义Xml取值表达式的接口
*/
public abstract class ReadXmlExpression {
/**
* 解释表达式
*
* @param c 上下文
* @return 解析过后的值,为了通用,可能是单个值,也可能是多个值,
* 因此就返回一个数组
*/
public abstract String[] interpret(Context c);
}
2
3
4
5
6
7
8
9
10
11
12
定义上下文:
上下文是用来封装解释器需要的一些全局数据,也可以在里面封装一些解释器的公共功能,可以相当于各个解释器的公共对象
import org.w3c.dom.*;
/**
* 上下文,用来包含解释器需要的一些全局信息
*/
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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
在上下文中使用了一个工具对象XmlUtil来获取Document对象,就是Dom解析xml,获取相应的Document对象
import org.w3c.dom.*;
import javax.xml.parsers.*;
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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
定义元素作为非终结符对应的解释器:
首先这个元素相当于组合模式的Composite对象,因此需要对子元素进行维护,另外这个元素的解释处理,只是需要把自己找到,作为下一个元素的父元素就好了。
import org.w3c.dom.*;
import java.util.ArrayList;
import java.util.Collection;
/**
* 元素作为非终结符对应的解释器,解释并执行中间元素
*/
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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
定义元素作为终结符对应的解释器:
对于单个元素的处理,终结符有两种,一个是元素终结,一个是属性终结。如果是元素终结,就是要获取元素的值;如果是属性终结,就是要获取属性的值。
import org.w3c.dom.*;
/**
* 元素作为终结符对应的解释器
*/
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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
定义属性作为终结符对应的解释器
/**
* 属性作为终结符对应的解释器
*/
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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
使用解释器
定义好了各个解释器的实现,可以写个客户端来测试一下这些解释器对象的功能了。使用解释器的客户端的工作会比较多,最主要的就是要组装抽象的语法树。
public class Client1 {
public static void main(String[] args) throws Exception {
//准备上下文
Context c = new Context("src/main/resources/interpreter.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]);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
运行结果:
再来测试一下获取单个元素的属性的值:
public class Client2 {
public static void main(String[] args) throws Exception {
//准备上下文
Context c = new Context("src/main/resources/interpreter.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]);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
运行结果:
就像前面讲述的那样,制定一种简单的语言,让客户端用来表达从xml中取值的表达式的语言,然后为它们定义一种文法的表示,也就是语法规则,然后用解释器对象来表示那些表达式,接下来通过运行解释器来解释并执行这些功能。
# 三:模式讲解
# 3.1 认识解释器模式
1. 解释器模式的功能
解释器模式使用解释器对象来表示和处理相应的语法规则,一般一个解释器处理一条语法规则。理论上来说,只要能用解释器对象把符合语法的表达式表示出来,而且能够构成抽象的语法树,那都可以使用解释器模式来处理。
2. 语法规则和解释器
语法规则和解释器之间是有对应关系的,一般一个解释器处理一条语法规则,但是反过来并不成立,一条语法规则是可以有多种解释和处理的,也就是一条语法规则可以对应多个解释器对象。
3. 上下文的公用性
上下文在解释器模式中起到非常重要的作用,由于上下文会被传递到所有的解释器中,因此可以在上下文中存储和访问解释器的状态,比如前面的解释器可以存储一些数据在上下文中,后面的解释器就可以获取这些值。
另外还可以通过上下文传递一些在解释器外部,但是解释器需要的数据,也可以是一些全局的,公共的数据。
上下文还有一个功能,就是可以提供所有解释器对象的公共功能,类似于对象组合,而不是使用继承来获取公共功能,在每个解释器对象里面都可以调用。
4. 谁来构建抽象语法树
在前面的示例中,大家已经发现,自己在客户端手工来构建抽象语法树,是很麻烦的,但是在解释器模式中,并没有涉及这部分功能,只是负责对构建好的抽象语法树进行解释处理。前面的测试简单,所以手工构建抽象语法树也不是特别困难的事,要是复杂了呢?如果还是手工创建,那跟修改解析xml的代码也差不了多少。后面会给大家讲到,可以提供解析器来实现把表达式转换成为抽象语法树。
还有一个问题,就是一条语法规则是可以对应多个解释器对象的,也就是说同一个元素,是可以转换成多个解释器对象的,这也就意味着同样一个表达式,是可以构成不同的抽象语法树的,这也造成构建抽象语法树变得很困难,而且工作量很大。
5. 谁负责解释操作
只要定义好了抽象语法树,肯定是解释器来负责解释执行。虽然有不同的语法规则,但是解释器不负责选择究竟用哪一个解释器对象来解释执行语法规则,选择解释器的功能在构建抽象语法树的时候就完成了。
所以解释器只要忠实的按照抽象语法树解释执行就好了。
# 3.2 优缺点
1. 易于实现语法
在解释器模式中,一条语法规则用一个解释器对象来解释执行,对于解释器的实现来讲,功能就变得比较简单,只需要考虑这一条语法规则的实现就好了,其它的都不用管。
2. 易于扩展新的语法
正是由于采用一个解释器对象负责一条语法规则的方式,使得扩展新的语法非常容易,扩展了新的语法,只需要创建相应的解释器对象,在创建抽象语法树的时候使用这个新的解释器对象就可以了。
3. 不适合复杂的语法
如果语法特别复杂,构建解释器模式需要的抽象语法树的工作是非常艰巨的,再加上有可能会需要构建多个抽象语法树。所以解释器模式不太适合于复杂的语法,对于复杂的语法,使用语法分析程序或编译器生成器可能会更好。
# 3.3 思考解释器模式
1. 解释器模式的本质
解释器模式的本质:分离实现,解释执行。
解释器模式通过一个解释器对象处理一个语法规则的方式,把复杂的功能分离开;然后选择需要被执行的功能,并把这些功能组合成为需要被解释执行的抽象语法树;然后再按照抽象语法树来解释执行,实现相应的功能。
认识这个本质对于识别和变形使用解释器模式是很有作用的。从表面上看,解释器模式是关注的我们平时不太用到的自定义语法的处理,但是从实质上看,解释器模式的思路仍然是分离、封装、简化,跟很多模式是一样的。
比如可以使用解释器模式模拟状态模式的功能。如果把解释器模式要处理的语法简化到只有一个状态标记,把解释器看成是对状态的处理对象,对同一个表示状态的语法,可以有很多不同的解释器,也就是有很多不同的处理状态的对象,然后在创建抽象语法树的时候,简化成根据状态的标记来创建相应的解释器,不用再构建树了。你看看这么简化下来,是不是可以用解释器模拟出状态模式的功能呢?
同理,解释器模式可以模拟实现策略模式的功能,装饰器模式的功能等等,尤其是模拟装饰器模式的功能,构建抽象语法树的过程,自然就对应成为组合装饰器的过程。
2. 何时选用解释器模式
建议在如下情况中,选用解释器模式:
当有一个语言需要解释执行,并且可以将该语言中的句子表示为一个抽象语法树的时候,可以考虑使用解释器模式。
在使用解释器模式的时候,还有两个特点需要考虑,一个是语法相对应该比较简单,太复杂的语法不合适使用解释器模式;另一个是效率要求不是很高,对效率要求很高的情况下,不适合使用解释器模式。
# 3.4 相关模式
1. 解释器模式和组合模式
这两个模式可以组合使用。
通常解释器模式都会使用组合模式来实现,这样能够方便的构建抽象语法树。一般非终结符解释器就相当于组合模式中的组合对象,终结符解释器就相当于叶子对象。
2. 解释器模式和迭代器模式
这两个模式可以组合使用。
由于解释器模式通常使用组合模式来实现,因此在遍历整个对象结构的时候,自然可以使用迭代器模式。
3. 解释器模式和享元模式
这两个模式可以组合使用。
在使用解释器模式的时候,可能会造成多个细粒度对象,比如会有各种各样的终结符解释器,而这些终结符解释器对不同的表达式来说是一样的,是可以共用的,因此可以引入享元模式来共享这些对象。
4. 解释器模式和访问者模式
这两个模式可以组合使用。
在解释器模式中,语法规则和解释器对象是有对应关系的。语法规则的变动意味着功能的变化,自然会导致使用不同的解释器对象;而且一个语法规则可以被不同的解释器解释执行。
因此在构建抽象语法树的时候,如果每个节点所对应的解释器对象是固定的,这就意味着这个节点对应的功能是固定的,那么就不得不根据需要来构建不同的抽象语法树。
为了让构建的抽象语法树较为通用,那就要求解释器的功能不要那么固定,要能很方便的改变解释器的功能,这个时候问题就变成了,如何能够很方便的更改树形结构中节点对象的功能了,访问者模式可以很好的实现这个功能。
# 四:SDK
- java.util.Pattern (opens new window)
- java.text.Normalizer (opens new window)
- java.text.Format (opens new window)
- javax.el.ELResolver (opens new window)