23、Interpreter 解释器模式
1、Interpreter 解释器模式
解释器模式是一种使用频率相对较低但学习难度较大的设计模式,它用于描述如何使用面向对象语言构成一个简单的语言解释器。在某些情况下,为了更好地描述某一些特定类型的问题,我们可以创建一种新的语言,这种语言拥有自己的表达式和结构,即文法规则,这些问题的实例将对应为该语言中的句子。此时,可以使用解释器模式来设计这种新的语言。对解释器模式的学习能够加深我们对面向对象思想的理解,并且掌握编程语言中文法规则的解释过程。
解释器模式定义如下:解释器模式(Interpreter Pattern):定义一个语言的文法,并且建立一个解释器来解释该语言中的句子,这里的“语言”是指使用规定格式和语法的代码。解释器模式是一种类行为型模式。
虽然使用解释器模式的实例不是很多,但对于满足以上特点,且对运行效率要求不是很高的应用实例,如果用解释器模式来实现,其效果是非常好的。
比如:一个公交系统,当输入北京的老人、北京的儿童、北京的白领等时,会根据不同的输入来判断是否需要收费以及怎么收费。本文就以公交系统来演示这个模式。
2、示例代码
1、使用解释器模式来设计和实现机器人控制程序
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
2、增加环境类实例
可以根据输入的指令在字符界面中输出一些格式化内容,例如输入“LOOP 2 PRINT杨过 SPACE SPACE PRINT 小龙女 BREAK END PRINT郭靖 SPACE SPACE PRINT 黄蓉”,将输出如下结果:
杨过 小龙女 杨过 小龙女 郭靖 黄蓉
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(); } }
编译并运行程序,输出结果如下:
杨过 小龙女 杨过 小龙女 郭靖 黄蓉
思考
预测指令“LOOP 2 LOOP 2 PRINT杨过 SPACE SPACE PRINT 小龙女 BREAK END PRINT 郭靖 SPACE SPACE PRINT 黄蓉 BREAK END”的输出结果。
3、Interpreter 类图
在解释器模式结构图中包含如下几个角色:
● AbstractExpression(抽象表达式):在抽象表达式中声明了抽象的解释操作,它是所有终结符表达式和非终结符表达式的公共父类。
● TerminalExpression(终结符表达式):终结符表达式是抽象表达式的子类,它实现了与文法中的终结符相关联的解释操作,在句子中的每一个终结符都是该类的一个实例。通常在一个解释器模式中只有少数几个终结符表达式类,它们的实例可以通过非终结符表达式组成较为复杂的句子。
● NonterminalExpression(非终结符表达式):非终结符表达式也是抽象表达式的子类,它实现了文法中非终结符的解释操作,由于在非终结符表达式中可以包含终结符表达式,也可以继续包含非终结符表达式,因此其解释操作一般通过递归的方式来完成。
● Context(环境类):环境类又称为上下文类,它用于存储解释器之外的一些全局信息,通常它临时存储了需要解释的语句。
4、小结
解释器模式为自定义语言的设计和实现提供了一种解决方案,它用于定义一组文法规则并通过这组文法规则来解释语言中的句子。虽然解释器模式的使用频率不是特别高,但是它在正则表达式、XML文档解释等领域还是得到了广泛使用。与解释器模式类似,目前还诞生了很多基于抽象语法树的源代码处理工具,例如Eclipse中的Eclipse AST,它可以用于表示Java语言的语法结构,用户可以通过扩展其功能,创建自己的文法规则。
优点: 1、可扩展性比较好,灵活。2、增加了新的解释表达式的方式。3、易于实现简单文法。
缺点: 1、可利用场景比较少。2、对于复杂的文法比较难维护。3、解释器模式会引起类膨胀。4、解释器模式采用递归调用方法。
使用场景: 1、可以将一个需要解释执行的语言中的句子表示为一个抽象语法树。2、一些重复出现的问题可以用一种简单的语言来进行表达。3、一个简单语法需要解释的场景。
公众号:发哥讲
这是一个稍偏基础和偏技术的公众号,甚至其中包括一些可能阅读量很低的包含代码的技术文,不知道你是不是喜欢,期待你的关注。
如果你觉得文章还不错,就请点击右上角选择发送给朋友或者转发到朋友圈~
● 扫码关注我们
据说看到好文章不推荐的人,服务器容易宕机!