1 定义:
解释器模式(Interpreter)
Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language.(给定一门语言,定义它的方法的一种表示,并定义一个解释器,该解释器使用表示来解释语言中的句子。)
解释器模式为自定义语言的设计和实现提供了一种解决方案,它用于定义一组文法规则并通过这组文法规则来解释语言中的句子。虽然解释器模式的使用频率不是特别高,但是它在正则表达式、XML文档解释等领域还是得到了广泛使用。与解释器模式类似,目前还诞生了很多基于抽象语法树的源代码处理工具,例如Eclipse中的Eclipse AST,它可以用于表示Java语言的语法结构,用户可以通过扩展其功能,创建自己的文法规则。
1.1 通用类图:
在解释器模式结构图中包含如下几个角色:
● 抽象表达式(AbstractExpression):在抽象表达式中声明了抽象的解释操作,它是所有终结符表达式和非终结符表达式的公共父类。
● 终结符表达式(TerminalExpression):终结符表达式是抽象表达式的子类,它实现了与文法中的终结符相关联的解释操作,在句子中的每一个终结符都是该类的一个实例。通常在一个解释器模式中只有少数几个终结符表达式类,它们的实例可以通过非终结符表达式组成较为复杂的句子。
● 非终结符表达式(NonterminalExpression):非终结符表达式也是抽象表达式的子类,它实现了文法中非终结符的解释操作,由于在非终结符表达式中可以包含终结符表达式,也可以继续包含非终结符表达式,因此其解释操作一般通过递归的方式来完成。
● 环境类(Context):环境类又称为上下文类,它用于存储解释器之外的一些全局信息,通常它临时存储了需要解释的语句。
在解释器模式中,每一种终结符和非终结符都有一个具体类与之对应,正因为使用类来表示每一条文法规则,所以系统将具有较好的灵活性和可扩展性。对于所有的终结符和非终结符,我们首先需要抽象出一个公共父类,即抽象表达式类,其典型代码如下所示:
abstract class AbstractExpression { public abstract void interpret(Context ctx); } |
终结符表达式和非终结符表达式类都是抽象表达式类的子类,对于终结符表达式,其代码很简单,主要是对终结符元素的处理,其典型代码如下所示:
class TerminalExpression extends AbstractExpression { public void interpret(Context ctx) { //终结符表达式的解释操作 } } |
对于非终结符表达式,其代码相对比较复杂,因为可以通过非终结符将表达式组合成更加复杂的结构,对于包含两个操作元素的非终结符表达式类,其典型代码如下:
class NonterminalExpression extends AbstractExpression { private AbstractExpression left; private AbstractExpression right;
public NonterminalExpression(AbstractExpression left,AbstractExpression right) { this.left=left; this.right=right; }
public void interpret(Context ctx) { //递归调用每一个组成部分的interpret()方法 //在递归调用时指定组成部分的连接方式,即非终结符的功能 } } |
除了上述用于表示表达式的类以外,通常在解释器模式中还提供了一个环境类Context,用于存储一些全局信息,通常在Context中包含了一个HashMap或ArrayList等类型的集合对象(也可以直接由HashMap等集合类充当环境类),存储一系列公共信息,如变量名与值的映射关系(key/value)等,用于在进行具体的解释操作时从中获取相关信息。其典型代码片段如下:
class Context { private HashMap map = new HashMap();
public void assign(String key, String value) { //往环境类中设值 }
public String lookup(String key) { //获取存储在环境类中的值 } } |
当系统无须提供全局公共信息时可以省略环境类,可根据实际情况决定是否需要环境类。
1.2 通用代码:
- public abstract class Expression {
- // 每个表达式必须有一个解析任务
- public abstract Object interpreter(Context ctx);
- }
- public class TerminalExpression extends Expression {
- // 通常终结符表达式只有一个,但是有多个对象
- public Object interpreter(Context ctx) {
- return null;
- }
- }
- public class NonterminalExpression extends Expression {
- // 每个非终结符表达式都会对其他表达式产生依赖
- public NonterminalExpression(Expression... expression) {
- }
- public Object interpreter(Context ctx) {
- // 进行文法处理
- return null;
- }
- }
- public class Context {
- }
- public class Client {
- public static void main(String[] args) {
- Context ctx = new Context();
- // 通常定一个语法容器,容纳一个具体的表达式,通常为ListArray,LinkedList,Stack等类型
- Stack<Expression> stack = null;
- /*
- * for(;;){ //进行语法判断,并产生递归调用 }
- */
- // 产生一个完整的语法树,由各各个具体的语法分析进行解析
- Expression exp = stack.pop();
- // 具体元素进入场景
- exp.interpreter(ctx);
- }
- }
2 优点
解释器是一个简单语法分析工具,它最显著的特点就是扩展性,修改语法规则只要修改相应的非终结符表达式就可以了,若扩展语法,则只要增加非终结符类就可以了。
3 缺点
3.1 类膨胀:每个语法都要产生一个非终结符表达式,语法规则比较复杂时,就可能产生大量的类文件,为维护带来了非常多的麻烦;
3.2 不便于调试:因为采用递归调用,若要排查错误,则非常复杂;
3.3 效率问题:由于使用了大量的循环和递归,效率是一个不容忽视的问题。
4 应用场景
4.1 重复发生的问题可以使用解释器模式:如服务器日志文件的分析处理;
4.2 简单语法需要解释:不过已逐步被被专用工具所取代;
在以下情况下可以考虑使用解释器模式:
(1) 可以将一个需要解释执行的语言中的句子表示为一个抽象语法树。
(2) 一些重复出现的问题可以用一种简单的语言来进行表达。
(3) 一个语言的文法较为简单。
(4) 执行效率不是关键问题。【注:高效的解释器通常不是通过直接解释抽象语法树来实现的,而是需要将它们转换成其他形式,使用解释器模式的执行效率并不高。】
5 注意事项
尽量不要在重要的模块中使用解释器模式,否则维护会是一个很大的问题。在项目中可以使用shell、JRuby、Groovy等脚本语言来代替解释器模式,弥补Java编译型语言的不足。我们在一个银行的分析型项目中就采用JRuby进行运算处理,避免使用解释器模式的四则运算,效率和性能各方面表现良好。(而且已有众多开源解释器,没必要自己写。)
6 扩展
6.1文法规则和抽象语法树
解释器模式描述了如何为简单的语言定义一个文法,如何在该语言中表示一个句子,以及如何解释这些句子。在正式分析解释器模式结构之前,我们先来学习如何表示一个语言的文法规则以及如何构造一棵抽象语法树。
在前面所提到的加法/减法解释器中,每一个输入表达式,例如“1 + 2 + 3 – 4 + 1”,都包含了三个语言单位,可以使用如下文法规则来定义:
expression ::= value | operation operation ::= expression '+' expression | expression '-' expression value ::= an integer //一个整数值 |
该文法规则包含三条语句,第一条表示表达式的组成方式,其中value和operation是后面两个语言单位的定义,每一条语句所定义的字符串如operation和value称为语言构造成分或语言单位,符号“::=”表示“定义为”的意思,其左边的语言单位通过右边来进行说明和定义,语言单位对应终结符表达式和非终结符表达式。如本规则中的operation是非终结符表达式,它的组成元素仍然可以是表达式,可以进一步分解,而value是终结符表达式,它的组成元素是最基本的语言单位,不能再进行分解。
在文法规则定义中可以使用一些符号来表示不同的含义,如使用“|”表示或,使用“{”和“}”表示组合,使用“*”表示出现0次或多次等,其中使用频率最高的符号是表示“或”关系的“|”,如文法规则“boolValue ::= 0 | 1”表示终结符表达式boolValue的取值可以为0或者1。
除了使用文法规则来定义一个语言,在解释器模式中还可以通过一种称之为抽象语法树(Abstract Syntax Tree, AST)的图形方式来直观地表示语言的构成,每一棵抽象语法树对应一个语言实例,如加法/减法表达式语言中的语句“1+ 2 + 3 – 4 + 1”,可以通过如图18-2所示抽象语法树来表示:
图18-2 抽象语法树示意图
在该抽象语法树中,可以通过终结符表达式value和非终结符表达式operation组成复杂的语句,每个文法规则的语言实例都可以表示为一个抽象语法树,即每一条具体的语句都可以用类似图18-2所示的抽象语法树来表示,在图中终结符表达式类的实例作为树的叶子节点,而非终结符表达式类的实例作为非叶子节点,它们可以将终结符表达式类的实例以及包含终结符和非终结符实例的子表达式作为其子节点。抽象语法树描述了如何构成一个复杂的句子,通过对抽象语法树的分析,可以识别出语言中的终结符类和非终结符类。
7 范例
7.1 银行系统中模型运算
在银行、证券类项目中,经常会有一些模型运算,通过对现有数据的统计、分析而预测不可知或未来可能发生的商业行为。模型运算大部分是针对海量数据的,例如建立一个模型公式,分析一个城市的消费倾向,进而影响银行的营销和业务扩张方向,一般的模型运算都有一个或多个运算公式,通常是加减乘除四则运算,偶尔也有指数、开方等复杂运算。具体到一个金融业务中,模型公式是非常复杂的,虽然只有加减乘除四则运算,但是公式有可能有十多个参数,而且上百个业务品各有不同的取参路径,同时相关表的数据量都在百万级,呵呵,复杂了吧,不复杂那就不叫金融业务,我们就来讲讲运算的核心——模型公式,如何实现。
业务需求:输入一个模型公式(加减四则运算),然后输入模型中的参数,运算出结果。
设计要求:
- 公式可以运行期编辑,并且符合正常算术书写方式,例如a+b-c;
- 高扩展性,未来增加指数、开方、极限、求导等运算符号时,较少改动量;
- 效率可以不用考虑,晚间批量运算。
需求不复杂,若仅仅对数字采用四则运算,每个程序员都可以写出来。但是增加了增加模型公式就复杂了。先解释一下为什么需要公式, 而不采用直接计算的方法,例如有如下3个公式:
- 业务种类1的公式:a+b+c-d;
- 业务种类2的公式:a+b+e-d;
- 业务种类3的公式:a-f。
其中,a、b、c、d、e、f参数的值都可以取得,如果使用直接计算数值的方法需要为每个品种写一个算法,目前仅仅是3个业务种类,那上百个品种呢?歇菜了吧!建立公式,然后通过公式运算才是王道。
我们以实现加减算法(由于篇幅所限,乘除法的运算读者可以自行扩展)的公式为例,讲解如何解析一个固定语法逻辑。由于使用语法解析的场景比较少,而且一些商业公司(比如SAS、SPSS等统计分析软件)都支持类似的规则运算,亲自编写语法解析的工作已经非常少,以下例程采用逐步分析方法,带领大家了解这一实现过程。
我们来想,公式中有什么?仅有两类元素:运算元素和运算符号,运算元素就是指a、b、c等符号,需要具体赋值的对象,也叫做终结符号,为什么叫终结符号呢?因为这些元素除了需要赋值外,不需要做任何处理,所有运算元素都对应一个具体的业务参数,这是语法中最小的单元逻辑,不可再拆分;运算符号就是加减符号,需要我们编写算法进行处理,每个运算符号都要对应处理单元,否则公式无法运行,运算符号也叫做非终结符号。两类元素的共同点是都要被解析,不同点是所有的运算元素具有相同的功能,可以用一个类表示,而运算符号则是需要分别进行解释,加法需要加法解析器,减法也需要减法解析器。分析到这里,我们就可以先画一个简单的类图,如图27-1所示。
图27-1 初步分析加减法类图
很简单的一个类图,VarExpression用来解析运算元素,各个公式能运算元素的数量是不同的,但每个运算元素都对应一个VarExpression对象。SybmolExpression负责运算符号解析,由两个子类AddExpression(负责加法运算)和SubExpression(负责减法运算)来实现。解析的工作完成了,我们还需要把安排运行的先后顺序(加减法是不用考虑,但是乘除法呢?注意扩展性),并且还要返回结果,因此我们需要增加一个封装类来进行封装处理,由于我们只做运算,暂时还不与业务有关联,定义为Calculator类,分析到这里,思路就比较清晰了,优化后加减法类图如图27-2所示。
图27-2 优化后加减法类图
Calcuator的作用是封装,根据迪米特原则,Client只与直接的朋友Calcuator交流,与其他类没关系。整个类图的结构也比较清晰,于是我们开始填充类图中的方法,完整类图如图27-3所示。
图27-3 完整加减法类图
类图已经完成,我们来看代码实现。Expression抽象类如代码清单27-1所示。
代码清单27-1 抽象表达式类
1234567 | public abstract class Expression { //解析公式和数值,其中var中的key值是是公式中的参数,value值是具体的数字 public abstract int interpreter(HashMap<String,Integer> var); } |
抽象类非常简单,仅一个方法interpreter负责对对传递进来的参数和值进行解析和匹配,其中输入参数为HashMap类型,key值为模型中的参数,如a、b、c等,value为运算时取得的具体数字。
变量的解析器如代码清单27-2所示。
代码清单27-2 变量解析器
12345678910111213141516171819 | public class VarExpression extends Expression { private String key; public VarExpression(String _key){ this .key = _key; } //从map中取之 public int interpreter(HashMap<String, Integer> var) { return var.get( this .key); } } |
抽象运算符号解析器如代码清单27-3所示。
代码清单27-3 抽象运算符号解析器
public abstract class SymbolExpression extends Expression {
protected Expression left;
protected Expression right;
//所有的解析公式都应只关心自己左右两个表达式的结果
public SymbolExpression(Expression _left,Expression _right){
this.left = _left;
this.right = _right;
}
}
这个解析过程还是比较有意思的,每个运算符号都只和自己左右两个数字有关系,但左右两个数字有可能也是一个解析的结果,无论何种类型,都是Expression的实现类,于是在对运算符解析的子类中增加了一个构造函数,传递左右两个表达式。具体的加、减法解析器如代码清单27-4、27-5所示。
代码清单27-4 加法解析器
12345678910111213141516171819202122232425262728293031323334353637 | public class AddExpression extends SymbolExpression { public AddExpression(Expression _left,Expression _right){ super (_left,_right); } //把左右两个表达式运算的结果加起来 public int interpreter(HashMap<String, Integer> var) { return super .left.interpreter(var) + super .right.interpreter(var); } } 代码清单 27 - 5 减法解析器 public class SubExpression extends SymbolExpression { public SubExpression(Expression _left,Expression _right){ super (_left,_right); } //左右两个表达式相减 public int interpreter(HashMap<String, Integer> var) { return super .left.interpreter(var) - super .right.interpreter(var); } } |
解析器的开发工作已经完成了,但是需求还没有完全实现,我们还需要对解析器进行封装,封装类Calculator如代码清单27-6所示。
代码清单27-6 解析器封装类
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273 | public class Calculator { //定义的表达式 private Expression expression; //构造函数传参,并解析 public Calculator(String expStr){ //定义一个堆栈,安排运算的先后顺序 Stack<Expression> new Stack<Expression>(); //表达式拆分为字符数组 char [] charArray = expStr.toCharArray(); //运算 Expression null ; Expression null ; for ( int i= 0 ;i<charArray.length;i++){ switch (charArray[i]) { case '+' : //加法 //加法结果放到堆栈中 left right new VarExpression(String.valueOf(charArray[++i])); stack.push( new AddExpression(left,right)); break ; case '-' : left right new VarExpression(String.valueOf(charArray[++i])); stack.push( new SubExpression(left,right)); break ; default : //公式中的变量 stack.push( new VarExpression(String.valueOf(charArray[i]))); } } //把运算结果抛出来 this .expression = stack.pop(); } //开始运算 public int run(HashMap<String,Integer> var){ return this .expression.interpreter(var); } } |
方法比较长,我们来分析一下,Calculator构造函数接收一个表达式,然后把表达式转化为char数组,并判断运算符号,如果是“+”则进行加法运算,把左边的数(left变量)和右边的数(right变量)加起来就可以了,那左边的数为什么是在堆栈中呢?例如这个公式:a+b-c,根据for循环,首先被压入堆栈中的应该是有a元素生成的VarExpression对象,然后判断到加号时,把a元素的对象VarExpression从堆栈中弹出,与右边的数组b进行相加,b又是怎么得来的呢?当前的数组游标下移一个单元格即可,同时为了防止该元素再次被遍历,则通过++i的方式跳过下一个遍历——于是一个加法的运行结束。减法也是相同的运行原理。
为了满足业务要求,我们设置了一个Client类来模拟用户情况,用户要求可以扩展,可以修改公式,那就通过接收键盘事件来处理,Client类如代码清单27-7所示。
代码清单27-7 客户模拟类
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859 | public class Client { //运行四则运算 public static void main(String[] args) throws IOException{ String //赋值 HashMap<String,Integer> Calculator new Calculator(expStr); System.out.println( "运算结果为:" +expStr + "=" +cal.run(var)); } //获得表达式 public static String getExpStr() throws IOException{ System.out.print( "请输入表达式:" ); return ( new BufferedReader( new InputStreamReader(System.in))).readLine(); } //获得值映射 public static HashMap<String,Integer> getValue(String exprStr) throws IOException{ HashMap<String,Integer> new HashMap<String,Integer>(); //解析有几个参数要传递 for ( char ch:exprStr.toCharArray()){ if (ch != '+' && ch != '-' ){ //解决重复参数的问题 if (!map.containsKey(String.valueOf(ch))){ System.out.print( "请输入" +ch+ "的值:" ); String new BufferedReader( new InputStreamReader(System.in))).readLine(); map.put(String.valueOf(ch),Integer.valueOf(in)); } } } return map; } } |
其中,getExpStr是从键盘事件中获得的表达式,getValue方法是从键盘事件中获得表达式中的元素映射值,运行过程如下。
首先,要求输入公式。
请输入表达式:a+b-c
其次,要求输入公式中的参数。
请输入a的值:100
请输入b的值:20
请输入c的值:40
最后,运行出结果。
运算结果为:a+b-c=80
看,要求输入一个公式,然后输入参数,运行结果出来了!那我们是不是可以修改公式?当然可以了,我们只要输入公式,然后输入相应的值就可以了,公式是在运行期定义的,而不是在运行前就制定好的,是不是类似于初中学过的“代数”这门课?先公式,然后赋值,运算出结果。
需求已经开发完毕,公式可以*定义,只要符合规则(有变量有运算符合)就可以运算出结果;若需要扩展也非常容易,只要增加SymbolExpression的子类就可以了 ,这就是解释器模式。
7.2机器人控制程序
Sunny软件公司欲为某玩具公司开发一套机器人控制程序,在该机器人控制程序中包含一些简单的英文控制指令,每一个指令对应一个表达式(expression),该表达式可以是简单表达式也可以是复合表达式,每一个简单表达式由移动方向(direction),移动方式(action)和移动距离(distance)三部分组成,其中移动方向包括上(up)、下(down)、左(left)、右(right);移动方式包括移动(move)和快速移动(run);移动距离为一个正整数。两个表达式之间可以通过与(and)连接,形成复合(composite)表达式。 用户通过对图形化的设置界面进行操作可以创建一个机器人控制指令,机器人在收到指令后将按照指令的设置进行移动,例如输入控制指令:up move 5,则“向上移动5个单位”;输入控制指令:down run 10 and left move 20,则“向下快速移动10个单位再向左移动20个单位”。 |
Sunny软件公司开发人员决定自定义一个简单的语言来解释机器人控制指令,根据上述需求描述,用形式化语言来表示该简单语言的文法规则如下:
expression ::= direction action distance | composite //表达式 composite ::= expression 'and' expression //复合表达式 direction ::= 'up' | 'down' | 'left' | 'right' //移动方向 action ::= 'move' | 'run' //移动方式 distance ::= an integer //移动距离 |
上述语言一共定义了五条文法规则,对应五个语言单位,这些语言单位可以分为两类,一类为终结符(也称为终结符表达式),例如direction、action和distance,它们是语言的最小组成单位,不能再进行拆分;另一类为非终结符(也称为非终结符表达式),例如expression和composite,它们都是一个完整的句子,包含一系列终结符或非终结符。
我们根据上述规则定义出的语言可以构成很多语句,计算机程序将根据这些语句进行某种操作。为了实现对语句的解释,可以使用解释器模式,在解释器模式中每一个文法规则都将对应一个类,扩展、改变文法以及增加新的文法规则都很方便,下面就让我们正式进入解释器模式的学习,看看使用解释器模式如何来实现对机器人控制指令的处理。
完整解决方案为了能够解释机器人控制指令,Sunny软件公司开发人员使用解释器模式来设计和实现机器人控制程序。针对五条文法规则,分别提供五个类来实现,其中终结符表达式direction、action和distance对应DirectionNode类、ActionNode类和DistanceNode类,非终结符表达式expression和composite对应SentenceNode类和AndNode类。
我们可以通过抽象语法树来表示具体解释过程,例如机器人控制指令“down run 10 and left move 20”对应的抽象语法树如图18-4所示:
图18-4 机器人控制程序抽象语法树实例
机器人控制程序实例基本结构如图18-5所示:
图18-5 机器人控制程序结构图
在图18-5中,AbstractNode充当抽象表达式角色,DirectionNode、ActionNode和DistanceNode充当终结符表达式角色,AndNode和SentenceNode充当非终结符表达式角色。完整代码如下所示:
- //注:本实例对机器人控制指令的输出结果进行模拟,将英文指令翻译为中文指令,实际情况是调用不同的控制程序进行机器人的控制,包括对移动方向、方式和距离的控制等
- import java.util.*;
- //抽象表达式
- abstract class AbstractNode {
- public abstract String interpret();
- }
- //And解释:非终结符表达式
- class AndNode extends AbstractNode {
- private AbstractNode left; //And的左表达式
- private AbstractNode right; //And的右表达式
- public AndNode(AbstractNode left, AbstractNode right) {
- this.left = left;
- this.right = right;
- }
- //And表达式解释操作
- public String interpret() {
- return left.interpret() + "再" + right.interpret();
- }
- }
- //简单句子解释:非终结符表达式
- class SentenceNode extends AbstractNode {
- private AbstractNode direction;
- private AbstractNode action;
- private AbstractNode distance;
- public SentenceNode(AbstractNode direction,AbstractNode action,AbstractNode distance) {
- this.direction = direction;
- this.action = action;
- this.distance = distance;
- }
- //简单句子的解释操作
- public String interpret() {
- return direction.interpret() + action.interpret() + distance.interpret();
- }
- }
- //方向解释:终结符表达式
- class DirectionNode extends AbstractNode {
- private String direction;
- public DirectionNode(String direction) {
- this.direction = direction;
- }
- //方向表达式的解释操作
- public String interpret() {
- if (direction.equalsIgnoreCase("up")) {
- return "向上";
- }
- else if (direction.equalsIgnoreCase("down")) {
- return "向下";
- }
- else if (direction.equalsIgnoreCase("left")) {
- return "向左";
- }
- else if (direction.equalsIgnoreCase("right")) {
- return "向右";
- }
- else {
- return "无效指令";
- }
- }
- }
- //动作解释:终结符表达式
- class ActionNode extends AbstractNode {
- private String action;
- public ActionNode(String action) {
- this.action = action;
- }
- //动作(移动方式)表达式的解释操作
- public String interpret() {
- if (action.equalsIgnoreCase("move")) {
- return "移动";
- }
- else if (action.equalsIgnoreCase("run")) {
- return "快速移动";
- }
- else {
- return "无效指令";
- }
- }
- }
- //距离解释:终结符表达式
- class DistanceNode extends AbstractNode {
- private String distance;
- public DistanceNode(String distance) {
- this.distance = distance;
- }
- //距离表达式的解释操作
- public String interpret() {
- return this.distance;
- }
- }
- //指令处理类:工具类
- class InstructionHandler {
- private String instruction;
- private AbstractNode node;
- public void handle(String instruction) {
- AbstractNode left = null, right = null;
- AbstractNode direction = null, action = null, distance = null;
- Stack stack = new Stack(); //声明一个栈对象用于存储抽象语法树
- String[] words = instruction.split(" "); //以空格分隔指令字符串
- for (int i = 0; i < words.length; i++) {
- //本实例采用栈的方式来处理指令,如果遇到“and”,则将其后的三个单词作为三个终结符表达式连成一个简单句子SentenceNode作为“and”的右表达式,而将从栈顶弹出的表达式作为“and”的左表达式,最后将新的“and”表达式压入栈中。 if (words[i].equalsIgnoreCase("and")) {
- left = (AbstractNode)stack.pop(); //弹出栈顶表达式作为左表达式
- String word1= words[++i];
- direction = new DirectionNode(word1);
- String word2 = words[++i];
- action = new ActionNode(word2);
- String word3 = words[++i];
- distance = new DistanceNode(word3);
- right = new SentenceNode(direction,action,distance); //右表达式
- stack.push(new AndNode(left,right)); //将新表达式压入栈中
- }
- //如果是从头开始进行解释,则将前三个单词组成一个简单句子SentenceNode并将该句子压入栈中
- else {
- String word1 = words[i];
- direction = new DirectionNode(word1);
- String word2 = words[++i];
- action = new ActionNode(word2);
- String word3 = words[++i];
- distance = new DistanceNode(word3);
- left = new SentenceNode(direction,action,distance);
- stack.push(left); //将新表达式压入栈中
- }
- }
- this.node = (AbstractNode)stack.pop(); //将全部表达式从栈中弹出
- }
- public String output() {
- String result = node.interpret(); //解释表达式
- return result;
- }
- }
工具类InstructionHandler用于对输入指令进行处理,将输入指令分割为字符串数组,将第1个、第2个和第3个单词组合成一个句子,并存入栈中;如果发现有单词“and”,则将“and”后的第1个、第2个和第3个单词组合成一个新的句子作为“and”的右表达式,并从栈中取出原先所存句子作为左表达式,然后组合成一个And节点存入栈中。依此类推,直到整个指令解析结束。
编写如下客户端测试代码:
- class Client {
- public static void main(String args[]) {
- String instruction = "up move 5 and down run 10 and left move 5";
- InstructionHandler handler = new InstructionHandler();
- handler.handle(instruction);
- String outString;
- outString = handler.output();
- System.out.println(outString);
- }
- }
编译并运行程序,输出结果如下:
向上移动5再向下快速移动10再向左移动5 |
在解释器模式中,环境类Context用于存储解释器之外的一些全局信息,它通常作为参数被传递到所有表达式的解释方法interpret()中,可以在Context对象中存储和访问表达式解释器的状态,向表达式解释器提供一些全局的、公共的数据,此外还可以在Context中增加一些所有表达式解释器都共有的功能,减轻解释器的职责。
在上面的机器人控制程序实例中,我们省略了环境类角色,下面再通过一个简单实例来说明环境类的用途:
Sunny软件公司开发了一套简单的基于字符界面的格式化指令,可以根据输入的指令在字符界面中输出一些格式化内容,例如输入“LOOP 2 PRINT杨过 SPACE SPACE PRINT 小龙女 BREAK END PRINT郭靖 SPACE SPACE PRINT 黄蓉”,将输出如下结果:
其中关键词LOOP表示“循环”,后面的数字表示循环次数;PRINT表示“打印”,后面的字符串表示打印的内容;SPACE表示“空格”;BREAK表示“换行”;END表示“循环结束”。每一个关键词对应一条命令,计算机程序将根据关键词执行相应的处理操作。 现使用解释器模式设计并实现该格式化指令的解释,对指令进行分析并调用相应的操作执行指令中每一条命令。 |
Sunny软件公司开发人员通过分析,根据该格式化指令中句子的组成,定义了如下文法规则:
expression ::= command* //表达式,一个表达式包含多条命令 command ::= loop | primitive //语句命令 loop ::= 'loopnumber' expression 'end' //循环命令,其中number为自然数 primitive ::= 'printstring' | 'space' | 'break' //基本命令,其中string为字符串 |
根据以上文法规则,通过进一步分析,绘制如图18-6所示结构图:
图18-6 格式化指令结构图
在图18-6中,Context充当环境角色,Node充当抽象表达式角色,ExpressionNode、CommandNode和LoopCommandNode充当非终结符表达式角色,PrimitiveCommandNode充当终结符表达式角色。完整代码如下所示:
- import java.util.*;
- //环境类:用于存储和操作需要解释的语句,在本实例中每一个需要解释的单词可以称为一个动作标记(Action Token)或命令
- class Context {
- private StringTokenizer tokenizer; //StringTokenizer类,用于将字符串分解为更小的字符串标记(Token),默认情况下以空格作为分隔符
- private String currentToken; //当前字符串标记
- public Context(String text) {
- tokenizer = new StringTokenizer(text); //通过传入的指令字符串创建StringTokenizer对象
- nextToken();
- }
- //返回下一个标记
- public String nextToken() {
- if (tokenizer.hasMoreTokens()) {
- currentToken = tokenizer.nextToken();
- }
- else {
- currentToken = null;
- }
- return currentToken;
- }
- //返回当前的标记
- public String currentToken() {
- return currentToken;
- }
- //跳过一个标记
- public void skipToken(String token) {
- if (!token.equals(currentToken)) {
- System.err.println("错误提示:" + currentToken + "解释错误!");
- }
- nextToken();
- }
- //如果当前的标记是一个数字,则返回对应的数值
- public int currentNumber() {
- int number = 0;
- try{
- number = Integer.parseInt(currentToken); //将字符串转换为整数
- }
- catch(NumberFormatException e) {
- System.err.println("错误提示:" + e);
- }
- return number;
- }
- }
- //抽象节点类:抽象表达式
- abstract class Node {
- public abstract void interpret(Context text); //声明一个方法用于解释语句
- public abstract void execute(); //声明一个方法用于执行标记对应的命令
- }
- //表达式节点类:非终结符表达式
- class ExpressionNode extends Node {
- private ArrayList<Node> list = new ArrayList<Node>(); //定义一个集合用于存储多条命令
- public void interpret(Context context) {
- //循环处理Context中的标记
- while (true){
- //如果已经没有任何标记,则退出解释
- if (context.currentToken() == null) {
- break;
- }
- //如果标记为END,则不解释END并结束本次解释过程,可以继续之后的解释
- else if (context.currentToken().equals("END")) {
- context.skipToken("END");
- break;
- }
- //如果为其他标记,则解释标记并将其加入命令集合
- else {
- Node commandNode = new CommandNode();
- commandNode.interpret(context);
- list.add(commandNode);
- }
- }
- }
- //循环执行命令集合中的每一条命令
- public void execute() {
- Iterator iterator = list.iterator();
- while (iterator.hasNext()){
- ((Node)iterator.next()).execute();
- }
- }
- }
- //语句命令节点类:非终结符表达式
- class CommandNode extends Node {
- private Node node;
- public void interpret(Context context) {
- //处理LOOP循环命令
- if (context.currentToken().equals("LOOP")) {
- node = new LoopCommandNode();
- node.interpret(context);
- }
- //处理其他基本命令
- else {
- node = new PrimitiveCommandNode();
- node.interpret(context);
- }
- }
- public void execute() {
- node.execute();
- }
- }
- //循环命令节点类:非终结符表达式
- class LoopCommandNode extends Node {
- private int number; //循环次数
- private Node commandNode; //循环语句中的表达式
- //解释循环命令
- public void interpret(Context context) {
- context.skipToken("LOOP");
- number = context.currentNumber();
- context.nextToken();
- commandNode = new ExpressionNode(); //循环语句中的表达式
- commandNode.interpret(context);
- }
- public void execute() {
- for (int i=0;i<number;i++)
- commandNode.execute();
- }
- }
- //基本命令节点类:终结符表达式
- class PrimitiveCommandNode extends Node {
- private String name;
- private String text;
- //解释基本命令
- public void interpret(Context context) {
- name = context.currentToken();
- context.skipToken(name);
- if (!name.equals("PRINT") && !name.equals("BREAK") && !name.equals ("SPACE")){
- System.err.println("非法命令!");
- }
- if (name.equals("PRINT")){
- text = context.currentToken();
- context.nextToken();
- }
- }
- public void execute(){
- if (name.equals("PRINT"))
- System.out.print(text);
- else if (name.equals("SPACE"))
- System.out.print(" ");
- else if (name.equals("BREAK"))
- System.out.println();
- }
- }
在本实例代码中,环境类Context类似一个工具类,它提供了用于处理指令的方法,如nextToken()、currentToken()、skipToken()等,同时它存储了需要解释的指令并记录了每一次解释的当前标记(Token),而具体的解释过程交给表达式解释器类来处理。我们还可以将各种解释器类包含的公共方法移至环境类中,更好地实现这些方法的重用和扩展。
针对本实例代码,我们编写如下客户端测试代码:
- class Client{
- public static void main(String[] args){
- String text = "LOOP 2 PRINT 杨过 SPACE SPACE PRINT 小龙女 BREAK END PRINT 郭靖 SPACE SPACE PRINT 黄蓉";
- Context context = new Context(text);
- Node node = new ExpressionNode();
- node.interpret(context);
- node.execute();
- }
- }
编译并运行程序,输出结果如下:
杨过 小龙女 杨过 小龙女 郭靖 黄蓉 |