解释器模式是平时工作当中相对冷门的一个设计模式,也非常的难于理解,百度百科上的解释也非常之少,只是简单的介绍了一下,并且说了一句,可以参考正则表达式为一个实际的应用例子。
不过资料的匮乏并不能阻止我们对真理的探索,下面LZ先将百度百科上的定义以及解决的问题拔到这里,方便各位观看。
定义:给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。 使用场景:解释器模式需要解决的是,如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题。
LZ先给各位解释一下定义当中所提到的文法。文法也称为语法,指的是语言的结构方式。包括词的构成和变化,词组和句子的组织。对于文法来说,我们可以简单的理解为一种语言的规则,那么从解释器模式的定义可以看出,首先我们要先设计一种语言,然后给出语言的文法的表示,而在此基础上,我们采用解释器模式去解释语言中的句子。
要想彻底的理解解释器模式,LZ必须要先普及一下文法的定义,请各位暂且忍受住枯燥的理论知识,后面LZ会将这些理论用各位熟悉的代码诠释一遍。
首先我们来讨论一下上下文无关文法的组成,有四种组成部分。
1,非终结符号集(LZ标注:像JAVA语言中的表达式,程序语句,标识符等)
2,终结符号集(LZ标注:类似JAVA语言中的+,-,*,\,=等)
3,产生式集合,也可以称为规则集合(LZ标注:假设我们记JAVA中的标识符为id,那么下面这句话可以被成视为一条规则 id->a|b...|z|0..|9|_,其中|是或者的意思)
4,一个起始符号,这个符号是非终结符号集的一个元素(LZ标注:JAVA语言使用CompilationUnit(编译单元)作为起始符号。)
上面所说的定义有些抽象,所以LZ在后面加了一些标注,那么上下文无关文法的作用是什么呢?
它可以生成一组由文法导出的语句,这些语句可以根据文法的产生式进行分析,下面LZ给一个《编译原理》一书中的简单例子,为了方便理解,LZ将符号稍微更改了一下。
假设有一上下文无关文法如下:
arithmetic -> arithmetic + number | arithmetic - number | number
number -> 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
我们根据这个文法可以得到所有个位数的加减表达式,比如对于 9 + 2 - 1 ,我们可以通过以下步骤推导出来。
arithmetic - >arithmetic - number -> arithmetic + number - number -> number + number - number -> 9 + number -number -> 9 + 2 - number -> 9 + 2 - 1
对于文法来说,一个语句如果能够按照产生式推导出该语句,就称该语句是符合文法的,所以9 + 2 - 1是符合上述文法的一个语句。
在这个文法当中,其中非终结者符号是 arithmetic 和 number, 而终结者符号是 0 - 9 、-、+ 。
我们从文法中可以得知由该文法组成的语句有以下规则。
1、operator的右边必须是一个number。
2、operator的左边必须是一个arithmetic。
3、arithmetic的最右边一定是一个number。
4、由2和3,operator的左边必须是number。
5、由4,number的右边必须是空或者operator。
6、number只能是 0 和 1 - 9 的正整数。
7、operator只能是 - 和 + 。
针对这个文法,我们可以写一个解释器,去计算表达式的结果,而这个解释器就可以使用解释器模式编写。而在编写的过程中,我们需要验证以上的规则,如果违反了规则,则表达式是非法的。为了便于使用程序语言表示,我们只验证以上的后四条规则,这也是由原本的产生式推算出来的规则。
我们先来看下解释器模式的类图,引自《大话设计模式》。
可以看到类图中有四个角色,抽象表达式(AbstractExpression)、终结符表达式(TerminalExpression)、非终结符表达式(NonterminalExpression)以及上下文(Context)。
四个角色所负责的任务在类图中已有解释,LZ这里不再重复,这里要说的是,这里具体的表达式类个数是不定的。
换句话说,终结符表达式(TerminalExpression)和非终结符表达式(NonterminalExpression)的个数都是根据文法需要而定的,并非是一成不变。
下面我们就使用上述的解释器模式的结构去写一个解释器,用于解释上面的加减表达式,首先我们先写一个上下文,它记录了一些全局信息,提供给表达式类使用,如下。
package com.interpreter;
import java.util.ArrayList;
import java.util.List;
//上下文
public class Context {
private int result;//结果
private int index;//当前位置
private int mark;//标志位
private char[] inputChars;//输入的字符数组
private List operateNumbers = new ArrayList(2);//操作数
private char operator;//运算符
public Context(char[] inputChars) {
super();
this.inputChars = inputChars;
}
public int getResult() {
return result;
}
public void setResult(int result) {
this.result = result;
}
public boolean hasNext(){
return index != inputChars.length;
}
public char next() {
return inputChars[index++];
}
public char current(){
return inputChars[index];
}
public List getOperateNumbers() {
return operateNumbers;
}
public void setLeftOperateNumber(int operateNumber) {
this.operateNumbers.add(0, operateNumber);
}
public void setRightOperateNumber(int operateNumber) {
this.operateNumbers.add(1, operateNumber);
}
public char getOperator() {
return operator;
}
public void setOperator(char operator) {
this.operator = operator;
}
public void mark(){
mark = index;
}
public void reset(){
index = mark;
}
}
|
上下文的各个属性,都是表达式在计算过程中需要使用的,也就是类图中所说的全局信息,其中的操作数和运算符是模拟的计算机中寄存器加减指令的执行方式。下面我们给出抽象的表达式,它只是定义一个解释操作。
package com.interpreter;
//抽象表达式,定义一个解释操作
public interface Expression {
void interpreter(Context context);
}
|
下面便是最重要的四个具体表达式了,这其中对应于上面文法提到的终结符和非终结符,如下。
package com.interpreter;
//算数表达式(非终结符表达式,对应arithmetic)
public class ArithmeticExpression implements Expression {
public void interpreter(Context context) {
context.setResult(getResult(context));//计算结果
context.getOperateNumbers().clear();//清空操作数
context.setLeftOperateNumber(context.getResult());//将结果压入左操作数
}
private int getResult(Context context){
int result = 0;
switch (context.getOperator()) {
case '+':
result = context.getOperateNumbers().get(0) + context.getOperateNumbers().get(1);
break;
case '-':
result = context.getOperateNumbers().get(0) - context.getOperateNumbers().get(1);
break;
default:
break;
}
return result;
}
}
|
package com.interpreter;
//非终结符表达式,对应number
public class NumberExpression implements Expression{
public void interpreter(Context context) {
//设置操作数
Integer operateNumber = Integer.valueOf(String.valueOf(context.current()));
if (context.getOperateNumbers().size() == 0) {
context.setLeftOperateNumber(operateNumber);
context.setResult(operateNumber);
}else {
context.setRightOperateNumber(operateNumber);
Expression expression = new ArithmeticExpression();//转换成算数表达式
expression.interpreter(context);
}
}
}
|
package com.interpreter;
//终结符表达式,对应-、+
public class OperatorExpression implements Expression{
public void interpreter(Context context) {
context.setOperator(context.current());//设置运算符
}
}
|
package com.interpreter;
//终结符表达式,对应0、1、2、3、4、5、6、7、8、9
public class DigitExpression implements Expression{
public void interpreter(Context context) {
Expression expression = new NumberExpression();//如果是数字,则直接转为number表达式
expression.interpreter(context);
}
}
|
这四个类就是简单的解释操作,值得一提的就是其中的两次转换,这个在稍后LZ会解释一下。
下面本来该是客户端程序了,不过由于我们的例子较为复杂,客户端的代码会比较臃肿,所以LZ抽出了一个语法分析类,分担了一些客户端的任务,在标准解释器模式的类图中是没有这个类的。
各位可以把它的代码想象成在客户端里面就好,这并不影响各位理解解释器模式本身,语法分析器的代码如下。
package com.interpreter;
//语法解析器(如果按照解释器模式的设计,这些代码应该是在客户端,为了更加清晰,我们添加一个语法解析器)
public class GrammarParser {
//语法解析
public void parse(Context context) throws Exception{
while (context.hasNext()) {
Expression expression = null;
switch (context.current()) {
case '+':
case '-':
checkGrammar(context);
expression = new OperatorExpression();
break;
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
context.mark();
checkGrammar(context, context.current());
context.reset();
expression = new DigitExpression();
break;
default:
throw new RuntimeException("语法错误!");//无效符号
}
expression.interpreter(context);
context.next();
}
}
//检查语法
private void checkGrammar(Context context,char current){
context.next();
if (context.hasNext() && context.current() != '+' && context.current() != '-') {
throw new RuntimeException("语法错误!");//第5条
}
try {
Integer.valueOf(String.valueOf(current));
} catch (Exception e) {
throw new RuntimeException("语法错误!");//第6条
}
}
//检查语法
private void checkGrammar(Context context){
if (context.getOperateNumbers().size() == 0) {//第4条
throw new RuntimeException("语法错误!");
}
if (context.current() != '+' && context.current() != '-') {//第7条
throw new RuntimeException("语法错误!");
}
}
}
|
可以看到,我们的语法分析器不仅做了简单的分析语句,从而得出相应表达式的工作,还做了一个工作,就是语法的正确性检查。
下面我们写个客户端去计算几个表达式试一下。
package com.interpreter;
import java.util.ArrayList;
import java.util.List;
public class Client {
public static void main(String[] args) {
List inputList = new ArrayList();
//三个正确的,三个错误的
inputList.add("1+2+3+4+5+6+7+8+9");
inputList.add("1-2+3-4+5-6+7-8+9");
inputList.add("9");
inputList.add("-1+2+3+5");
inputList.add("1*2");
inputList.add("11+2+3+9");
GrammarParser grammarParser = new GrammarParser();//语法分析器
for (String input : inputList) {
Context context = new Context(input.toCharArray());
try {
grammarParser.parse(context);//语法分析器会调用解释器解释表达式
System.out.println(input + "=" + context.getResult());
} catch (Exception e) {
System.out.println("语法错误,请输入正确的表达式!");
}
}
}
}
|
输出结果:
1+2+3+4+5+6+7+8+9=45
1-2+3-4+5-6+7-8+9=5
9=9
语法错误,请输入正确的表达式!
语法错误,请输入正确的表达式!
语法错误,请输入正确的表达式!
可以看到,前三个表达式是符合我们的文法规则的,而后三个都不符合规则,所以提示了错误,这样的结果,与我们文法所表述的规则是相符的。
LZ需要提示的是,这里面本来是客户端使用解释器来解释语句的,不过由于我们抽离出了语法分析器,所以由语法分析器调用解释器来解释语句,这消除了客户端对解释器的关联,与标准类图不符,不过这其实只是我们所做的简单的改善而已,并不影响解释器模式的结构。
另外,上面的例子当中,还有两点是LZ要提一下的。LZ为了方便理解,已经尽量的将例子简化,不过其中有两个地方的转换是值得注意的。
1、一个是操作数满足条件时,会产生一个ArithmeticExpression表达式。
2、另外一个是从DigitExpression直接转换成NumberExpression的地方,这其实和第1点一样,都是对文法规则的使用,不过这个更加清晰。我们可以清楚的看到,0-9的数字或者说DigitExpression只对应唯一一种方式的非终结者符号,就是number,所以我们直接转换成NumberExpression。
不过我们的转换是由终结者符号反向转换成非终结者符号的顺序,也就是相当于从抽象语法树的低端向上转换的顺序。其实相当于LZ省去了抽象语法树的潜在构建过程,直接开始解释表达式。
我们看上面的类图中,非终结者表达式有一条到抽象表达式的聚合线,那其实是将非终结者表达式按照产生式分解的过程,这会是一个递归的过程,而我们省去了这一步,直接采用反向计算的方式。
然后再说说我们的语法分析器,它的工作就是将终结者符号对应上对应的表达式,可以看到它里面的swich结构就是用来选取表达式的。实际当中,我们当然不会写这么糟糕的swich结构,我们可以使用很多方式优化它。当然,语法分析器的另外一个工作就是检查语法的正确性,这点可以从两个check方法明显的看到。
不过很遗憾,在日常工作当中,我们使用到解释器模式的概率几乎为0,因为写一个解释器就基本相当于创造了一种语言,这对于大多数人来说,是几乎不可能接到的工作。不过我们了解一下解释器模式,还是对我们有好处的。
前面已经提到过解释器模式适用的场景,我们这里结合上面的例子总结一下解释器模式的优点:
1、由于我们使用具体的终止符和非终止符去解释文法,所以会比较易于编写。
2、可以比较方便的修改和扩展文法规则。
相对于优点来说,它的缺点也非常明显,那就是由于我们几乎针对每一个规则都定义了一个类,所以如果一个文法的规则比较多,那对于文法的维护工作也会变得非常困难。
下面LZ将我们例子的类图贴上来,各位参考一下。
针对这个类图,LZ简单的说两点。
1、Client本来是关联的Expression接口,不过由于中间加了个语法分析器(GrammerParser),所以变成了Client关联语法分析器,语法分析器再关联Expression接口。
2、由于我们采用反向计算的方式,所以非终结者表达式没有到Expression接口的聚合线,而是由两条关联线代替了两条聚合线。
解释器模式的分享就到此结束了,各位只要大致了解一下即可,如果实在理解不了,也不必死抠这个模式。
学习设计模式,有时候就像追女生一样,如果你怎么追都追不上,那说明缘分未到,该放弃的时候就得放弃,说不定哪天缘分到了,不需要你追,你心中的她自然就上钩了。所以如果有哪一个设计模式一时半会理解不了,请不要着急去理解它,不理解的原因是因为你的积累还不够,如果霸王硬上弓的话,往往会被设计模式暴揍一顿,最后还得自己付医药费。
LZ只想说,何必呢?
到这篇文章为止,LZ已经将所有24种设计模式全部讲解了一遍,其中有好有坏,有对有错。不过不管怎样,LZ本人的收获还是很大的,也很感谢这当中支持LZ的猿友。设计模式系列或许还会有最后一篇,内容自然是对24种设计模式的总结,LZ最近也在为此而准备着,敬请各位猿友擦亮双眼期待吧。
一个系列结束了,不代表LZ的学习之路结束了,在上一章已经提到过,LZ最近在研究虚拟机源码,在接下来的时间里,LZ或许会写一些与虚拟机相关的内容,如果有哪位猿友对虚拟机有兴趣的话,可以继续关注下LZ。
转自:http://www.uml.org.cn/j2ee/201311044.asp
|
相关推荐
Java 中 23 种设计模式详解 在软件设计中,设计模式是解决特定问题的通用解决方案。 Java 中有 23 种常见的设计模式,下面将对每种设计模式进行详细的解释: 1. 抽象工厂模式(Abstract Factory) 抽象工厂模式...
设计模式 的分类 总体来说设计模式分为三大类: 创建型模式(5): ...策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
- 解释器模式(Interpreter):给定一种语言,定义其语法的表示,并提供一个解释器来处理这种语言中的句子。 - 迭代器模式(Iterator):提供一种方法顺序访问聚合对象的元素,而又不暴露其底层表示。 - 中介者...
15. **解释器模式**:定义一个表示语言的文法,并给出一个解释器,用于解释语言中的句子。 16. **迭代器模式**:提供一种方法顺序访问聚合对象的元素,而又不暴露其底层表示。 17. **中介者模式**:用一个中介对象...
15. **解释器模式(Interpreter)**:给定一种语言,定义它的文法表示,并且提供一个解释器,该解释器用于根据这种文法处理输入。 16. **迭代器模式(Iterator)**:提供一种方法顺序访问聚合对象中的元素,而又不...
本主题将深入探讨Java开发中的23种设计模式,这些模式被广泛应用于创建、结构和行为三个主要类别,旨在提高代码的可读性、可维护性和复用性。 一、创建型设计模式: 1. 单例模式:确保一个类只有一个实例,并提供一...
Java中的23种设计模式被广泛应用于提高代码的可读性、可维护性和灵活性。以下是对这些模式的详细解释: 1. **工厂模式**(Factory Method):这是一种创建型设计模式,它提供了一个接口来创建对象,但允许子类决定...
【Android编程设计模式之解释器模式详解】 解释器模式(Interpreter Pattern)是设计模式中的一种行为模式,主要用于解析特定语言或表达式。在Android开发中,尽管不常见,但当需要自定义简单的语言或者表达式解释...
15. **解释器模式(Interpreter)**:给定一个语言,定义它的文法表示,并提供一个解释器来处理该语言中的句子。 16. **迭代器模式(Iterator)**:提供一种方法顺序访问聚合对象的元素,而又不暴露其底层表示。 ...
- **解释器模式(Interpreter)**:给定一个语言,定义它的文法表示,并提供一个解释器来处理这个语言中的句子。 - **访问者模式(Visitor)**:表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变...
"23种设计模式详解及案例"涵盖了这些模式的理论基础和实际应用,旨在帮助开发者更好地理解和掌握它们。 1. **单例模式**:保证一个类只有一个实例,常用于管理共享资源,如数据库连接。在Android中,单例模式常用于...
行为模式是指在对象的行为上使用的模式,包括 Iterator(迭代模式)、Template(模板模式)、Chain of Responsibility(责任链模式)、Memento(纪念品模式)、Mediator(中介模式)、Interpreter(解释器模式)、...
本文档详细介绍了23种C#设计模式,包括创建型、结构型和行为型三个大类。这些设计模式是.NET进阶必备的知识,通过学习和掌握这些设计模式,可以提高程序员的设计和编码能力。 创建型设计模式 1. 单件模式...
以上23种设计模式都是软件设计中的宝贵经验,理解并熟练应用这些模式,能帮助开发者写出更高效、可维护的代码,提升系统的可扩展性和灵活性。通过阅读提供的"DesignPattern"压缩包文件,可以深入学习每种模式的实现...
解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器。 策略模式:定义一系列算法,把他们封装起来,并且使它们可以相互替换。 状态模式:允许一个对象在其对象内部状态改变时改变它的行为。 ...
3. 解释器模式(Interpreter):给定一种语言,定义它的文法表示,并提供一个解释器来处理该语言中的句子。 4. 迭代器模式(Iterator):提供一种方法顺序访问聚合对象的元素,而又不暴露其底层表示。 5. 中介者模式...
Java设计模式详解 Java设计模式是前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。... * 解释器模式
这23种设计模式包括但不限于单例模式、工厂模式、建造者模式、原型模式、适配器模式、桥接模式、组合模式、装饰模式、外观模式、享元模式、代理模式、命令模式、责任链模式、解释器模式、迭代器模式、中介者模式、...
解释器模式为一个语言定义它的文法的一种表示,并同时提供一个解释器。 设计模式的六大原则包括:开闭原则、里氏代换原则、依赖倒转原则、接口隔离原则、迪米特法则(最少知道原则)、组合/聚合复用原则。开闭原则...