概述
解释器模式是一种使用频率相对比较低但学习难度较大地设计模式,它用于描述如何使用面向对象语言构成一个简单的语言解释器。在某些情况下,为了更好的描述某些特定类型的问题,可以创建一种新的语言,这种语言拥有自己地表达式和结构,即文法规则,这些问题的实例将对应为该语言中的句子。此时,可以使用解释器模式来设计这种新的语言,对解释器模式的学习能够加深对面向对象思想的理解,并且掌握了编程语言中文法规则的解释过程
定义
解释器模式(Interpreter Pattern):定义一个语言的文法,并且建立一个解释器来解释该语言中的句子,这里的‘语言’是指使用规定格式和语语法的代码。解释器模式是一种类行为型模式。
结构图
由于表达式可分为终结符表达式和非终结符表达式,因此解释器模式的结构与组合模式的结构有些类似,但在解释器模式中包含更多的组成元素。
在解释器模式的结构图中包含以下4个角色
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){
//递归调用每一个组成部分的interpre()方法
//在递归调用时指定组成部分的连接方式,即非终结符的功能
}
}
除了上述用于表示表达式的类以外,通常在解释器模式中还提供了一个环境类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){
//获取储存在环境类中的值
}
}
当系统无须提供全局信息时可以省略环境类,也可以提供实际情况决定是否需要环境类。
例子:机器人控制程序
使用解释器模式来设计和实现一套机器人控制程序,在该机器人控制程序中包含一些简单的英文控制指令,每一个指令对应一个表达式(expression),该表达式可以是简单表达式,也可以是符合表达式,每一个简单表达式由移动方向(direction),移动方式(action)和移动距离(distance)三部分组成,其中移动方向包括上(up),下(down),左(left),右(right);移动方式包括移动(move)和快捷移动(run);移动距离为一个正整数。两个表达式之间可以通过与(and)连接,形成复合(composite)表达式。
根据上述需求,用形式化语言来表示该简单语言的文法规则如下:
expression ::= direction action distance | composite //表达式
composite ::= expression 'and' expression //复合表达式
direction ::= 'up' | 'down' | 'left' | 'right' //移动方向
action ::= 'move' | 'run' //移动方式
distance ::= an Integer //移动距离
上述语言一共定义了5条文法规则,对应5个语言单位,这些语言单位可以分成两类:一类为终结符(也称为终结符表达式),例如direction,action和distance,它们是语言最小组成单位,不能再进行拆分;另一类为非终结符(也称为非终结符表达式),例如expression和composite,它们都是一个完整的句子,包含一系列终结符或非终结符
针对5条文法规则,分别提供5个类来实现,其中终结符表达式direction,action和distance对应的是DirectionNode类,ActionNode类和DistanceNode类,非终结符表达式expression和composite对应SentenceNode类和AndNode类。
结构图
图中,AbstractNode充当抽象表达式角色,DirectionNode,ActionNode和DistanceNode充当终结符表达式角色,AndNode和SentenceNode充当非终结符表达式角色
代码实现
抽象表达式
public abstract class AbstractNode {
public abstract String interpret();
}
And解释:非终结符表达式
public 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();
}
}
简单句子解释:非终结符表达式
public 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();
}
}
方向解释:终结符表达式
public 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 "无效指令";
}
}
}
动作解释:终结符表达式
public 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 "无效指令";
}
}
}
距离解释:终结符表达式
public class DistanceNode extends AbstractNode {
private String distance;
public DistanceNode(String distance) {
this.distance = distance;
}
//距离表达式的解释操作
public String interpret() {
return this.distance;
}
}
指令处理类:工具类
public class InstructionHandler {
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’,则将其后的3个单词作为3个终结符表达式连成一个简单句子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)); //将新表达式压入栈中
}
//如果是从头开始进行解释,则将前3个单词组成一个简单句子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用于对输入指令进行处理,将输入指令分割为字符串数组,将第一个,第二个和第三个单词组合为一个句子,并存入栈中;如果发现有单词‘and’,则将‘and’后第一个,第二个和第三个单词组合为一个新的句子作‘and’的右表达式,并从栈中取出原先所存句子作为左表达式,然后组合成一个And节点存入栈中。以此类推,直到整个指令解析结束。
客户端
public class Client {
public static void main(String[] args) {
String instruction = "up move5 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作用
在解释器模式中,环境类Context用于存储解释器之外的一些全局信息,它通常作为参数被传递到所有的表达式的解释方法interpret()中,可以在Context对象中存储和访问表达式解释器的状态,向表达式解释器提供一些全局的,公共的数据。此外,还可以在Context中增加一些所有表达式解释器都共有的功能,减轻解释器的职责。
例子:格式化指令
开发一套简单的基于字符界面的格式化指令,可以根据输入的指令在字符界面输出一些格式化的内容,例如输入:‘LOOP 2 PRINY 杨过 SPACE SPACE PRINT 小龙女 BREAK END PRINT 郭靖 SPACE SPACE PRINT 黄蓉’,将输入如下结果:
杨过 小龙女
杨过 小龙女
郭靖 黄蓉
其中,关键字LOOP表示循环,后面的数字表示循环次数;PRINT表示打印,后面的字符串表示打印的内容;SPACE表示空格;BREAK表示换行;END表示循环结束
使用解释器模式来设计开发,根据上述格式化指令中句子的组成,可以定义如下文法规则:
expression ::= command* //表达式,一个表达式包含多个命令
command ::= loop | primitive //语句命令
loop ::= 'loop number' expression 'end' //循环命令,其中number为自然数
primitive ::= 'print string' | 'space' | 'break' //基本命令,其中string为字符串
结构图
在图中,Context充当环境角色,Node充当抽象表达式角色,ExpressionNode,CommandNode和LoopCommandNode充当非终结符表达式角色,PrimitiveCommandNode充当终结符表达式角色,PrimitiveCommandNode充当终结符表达式角色
代码实现
环境类:用于存储和操作需要解释的语句
public 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.out.println("错误提示:" + currentToken + "解释错误!");
}
nextToken();
}
//如果当前的标记是一个数字,则返回对应的数值
public int currentNumber() {
int number = 0;
try {
number = Integer.parseInt(currentToken);//将字符串转换为整数
}
catch (Exception e) {
System.out.println("错误提示:" + e);
}
return number;
}
}
抽象结点类:抽象表达式
public abstract class Node {
public abstract void interpret(Context text); //声明一个方法解释语句
public abstract void execute(); //声明一个方法用于执行标记对应的命令
}
抽象节点:抽象表达式
public class ExpressionNode extends Node {
private ArrayList<Node> list = new ArrayList<Node>();//定义一个集合用于存储多条命令
@Override
public void interpret(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);
}
}
}
//循环执行命令集合中的每一条命令
@Override
public void execute() {
Iterator iterator = list.iterator();
while(iterator.hasNext()) {
((Node)iterator.next()).execute();
}
}
}
语句命令节点类:非终结符表达式
public class CommandNode extends Node {
private Node node;
@Override
public void interpret(Context context) {
//处理LOOP循环命令
if(context.currentToken().equals("LOOP")) {
node = new LoopCommandNode();
node.interpret(context);
}
//处理其他基本命令
else {
node = new PrimitiveCommandNode();
node.interpret(context);
}
}
@Override
public void execute() {
node.execute();
}
}
循环命令节点类:非终结符表达式
public 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);
}
@Override
public void execute() {
for(int i = 0; i < number; i++) {
commandNode.execute();
}
}
}
基本命令节点类:终结符表达式
public 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.out.println("非法命令");
}
if(name.equals("PRINT")) {
text = context.currentToken();
context.nextToken();
}
}
@Override
public void execute() {
if(name.equals("PRINT")) {
System.out.println(text);
}else if(name.equals("SPACE")) {
System.out.println(" ");
}else if(name.equals("BREAK")) {
System.out.println();
}
}
}
在本实例代码中,环境类Context类似一个工具类,它提供了用于处理指令的方法,例如nextToken(),currentToken(),skipToken()等,同时它存储了需要解释的指令并记录了每一次解释的当前标记(Token),而具体的解释过程交给表达式解释器来处理,还可以将各种解释器类包含的公共方法移至环境类中,更好地实现这些方法的重用和扩展。
客户端
public 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();
}
}
总结
解释器模式为自定义语言的设计和实现提供了一种解决方案,它用于定义一组文法规则并通过这组文法规则来解释语言中的句子。虽然解释器模式的使用频率不是特别高,但它在正则表达式,XML文档解释等领域还是得到了广泛的使用。
优点
易于改变和扩展文法。由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法
每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言。
实现文法较为容易。在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂,还可以通过一些工具自动生成节点类代码
增加新的解释表达式较为方便。如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式类,原有表达式类代码无须修改,符合开闭原则
缺点
对于复杂文法难以维护。在解释器模式中,每一条规则至少需要定义一个类,因此如果一个语言包含太多文法规则,类的个数将会急剧增加,导致系统难以管理和维护,此时可以考虑使用语法分析程序等方式来取代解释器模式
执行效率低。由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时,其速度很慢,而且代码的调试过程也比较麻烦
适用场景
可以将一个需要解释执行的语言中的句子表示为一个抽象语法树
一些重复出现的问题可以用一种简单的语言来进行表达
一个语言的文法较为简单
执行效率不是关键问题(注:高效的解释器通常不是通过直接解释抽象语法树来实现的,而是需要将它们转换成其他形式,使用解释器模式的执行效率并不高)