有时,我们希望输入一串字符串,然后计算机能够按照预先定义的文法规则来对这个字符串进行解释,从而实现相应的功能。
例如,我们想实现简单的加减法接收器,只需输入一个表达式,它就能计算出表达式结果。比如输入字符串:“2+4-1+4-5”,计算机输出4。像这种用户自己定义一套文法规则来实现对这些语句的解释,即设计一个自定义语言。此时可以使用解释器模式来实现自定义语言。
1 解释器模式
1.1 文法规则
在前面所提到的加法/减法解释器中,emigrants表达式都包含了3个语言单位,可以使用如下语法规则来定义:
expression ::= value | operation
operation ::= expression ‘+’ expression | expression ‘-’ expression
value ::= an integer // 一个整数值
::= | 表示定义为 |
| | 表示或 |
“{” 和 “}” | 表示组合 |
* | 表示出现0次或多次 |
语言单位 | 又称为语言构造成分,每一条语句所定义的字符串,如上例中的value和operation。 |
终结表达式 | 组成元素是最基本的语言单位,不能再进行分解。 |
非终结表达式 | 组成元素仍然可以是表达式,可以进一步分解。如expression 和 operation。 |
图 文法规则说明
1.2 抽象语法树
除了使用文法规则来定义一个语言外,还可以通过一种被称为抽象语法树(Abstract Syntax Tree, AST)的图形方式来直观的表示语言的构成。每一棵抽象语法树对应一个语言实例。“2+4-1+4-5” 的抽象语法树表示为:
图 抽象语法树示意图
图中终结符表达式作为树的叶子节点,而非终结表达式作为非叶子节点,它们可将终结符表达式以及包含终结符和非终结符的子表达式作为其子节点。通过对抽象语法树的分析,可以识别出语言中的终结符类和非终结符类。
1.3 解释器模式概述
用于描述如何使用对象语言构成一个简单的语言解释器。定义一个语言的文法,并建立一个解释器来解释语言中的句子。这里的“语言”是指使用规定格式和语法的代码。
图 解释器模式结构图
- AbstractExpression,抽象表达式,声明了抽象的解释操作,是所有终结符表达式和非终结符表达式的父类。
- TerminalExpression,终结符表达式,实现了与文法中的终结符相关联的解释操作,在句子中的每个终结符都是该类的一个实例,它们的实例可以通过非终结符表达式组成较为复杂的句子。
- NonterminalExpression,非终结符表达式,实现了文法中非终结符的解释操作。在非终结符表达式中可以包含终结符表达式,也可以包含非终结符表达式,因此其解释操作一般通过递归的方式来实现。
- Context,环境类,又称为上下文,用于存储解释器之外的一些全局信息。通常存储了需要解释的语句。
1.3.1 解释器模式实现简单的英文控制指令
需求描述:每个指令对应一个表达式,该表达式可以是简单表达式,也可以是复合表达式。两个表达式之间可以通过and连接,形成复合表达式。简单表达式组成如下表:
移动方向(direction) | 上(up)、下(down)、左(left)、右(right) |
移动方式(action) | 移动(move)、快速移动(run) |
移动距离(distance) | 为一个正整数 |
表 简单表达式的组成
例如“up move 5”表示向上移动5个单位。“dow run 10 and left move 20”表示向下快速移动10个单位然后向左移动20个单位。
direction ::= ‘up’|’down’|’left’|’right’;
action :: = ‘move’|’run’;
distance ::= an integer; //一个正整数
expression ::= direction action distance | complexExpression
complexExpression ::= expression ‘and’ expression
// 简单的英语控制指令,例如 left move 10
public interface AbstractExpression {
void interpret();
}
public class ActionExpression implements AbstractExpression{
private final String action;
public ActionExpression(String action) {
this.action = action;
}
@Override
public void interpret() {
if (action == null) {
throw new RuntimeException("解析失败:行为值为空");
}
String tempStr = action.toLowerCase();
switch (tempStr) {
case "move":
System.out.print("移动");
break;
case "run":
System.out.print("快速移动");
break;
default:
throw new RuntimeException("解析失败:行为值不合法");
}
}
}
public class ComplexExpression implements AbstractExpression{
private AbstractExpression leftExpress;
private AbstractExpression rightExpress;
public ComplexExpression(AbstractExpression leftExpress, AbstractExpression rightExpress) {
this.leftExpress = leftExpress;
this.rightExpress = rightExpress;
}
@Override
public void interpret() {
leftExpress.interpret();
System.out.print(" 然后 ");
rightExpress.interpret();
}
}
public class DirectionExpression implements AbstractExpression{
private final String direction;
public DirectionExpression(String direction) {
this.direction = direction;
}
@Override
public void interpret() {
if (direction == null) {
throw new RuntimeException("解析失败:方向值为空");
}
String tempStr = direction.toLowerCase();
switch (tempStr) {
case "up":
System.out.print("向上");
break;
case "down":
System.out.print("向下");
break;
case "left":
System.out.print("向左");
break;
case "right":
System.out.print("向右");
break;
default: throw new RuntimeException("解析失败:方向值不合法");
}
}
}
public class DistanceExpression implements AbstractExpression{
private final String value;
public DistanceExpression(String value) {
this.value = value;
}
@Override
public void interpret() {
if (value == null) {
throw new RuntimeException("解析失败:距离为空");
}
System.out.print(Integer.valueOf(value) + "个单位");
}
}
public class SimpleExpression implements AbstractExpression{
private final AbstractExpression direction;
private final AbstractExpression action;
private final AbstractExpression distance;
public SimpleExpression(AbstractExpression direction, AbstractExpression action, AbstractExpression distance) {
this.direction = direction;
this.action = action;
this.distance = distance;
}
@Override
public void interpret() {
direction.interpret();
action.interpret();
distance.interpret();
}
}
public class Client {
private static final String[] expressArr = {"left move 12 and down run 5","down run 3","right run 1 and left move 12 and left run 4 and down move 12"};
public static void main(String[] args) {
for (String str : expressArr) {
String[] strArr = str.split(" ");
Stack<AbstractExpression> expressionStack = new Stack<>();
for (int i = 0; i < strArr.length; i++) {
if ("and".equals(strArr[i])) {
AbstractExpression leftExp = expressionStack.pop();
AbstractExpression rightExp = buildSimpleExp(strArr, i + 1);
expressionStack.push(new ComplexExpression(leftExp,rightExp));
i += 3;
} else {
expressionStack.push(buildSimpleExp(strArr,i));
i += 2;
}
}
expressionStack.pop().interpret();
System.out.println();
}
// 运行结果:
// 向左移动12个单位 然后 向下快速移动5个单位
// 向下快速移动3个单位
// 向右快速移动1个单位 然后 向左移动12个单位 然后 向左快速移动4个单位 然后 向下移动12个单位
}
private static AbstractExpression buildSimpleExp(String[] strArr, int start) {
AbstractExpression directionExp = new DirectionExpression(strArr[start]);
AbstractExpression actionExp = new ActionExpression(strArr[start + 1]);
AbstractExpression distance = new DistanceExpression(strArr[start + 2]);
return new SimpleExpression(directionExp,actionExp,distance);
}
}
1.3.2 “简单的英语控制指令” 为啥要用解释器模式?
其实,上面这个需求在不用解释器模式的情况下也可以实现,而且只需在一个类就能完成。
public class Client2 {
private static final String[] expressArr = {"left move 12 and down run 5","down run 3","right run 1 and left move 12 and left run 4 and down move 12"};
public static void main(String[] args) {
System.out.println("不用解释器模式实现需求:");
System.out.println("--------------");
for (String str : expressArr) {
StringBuilder sb = new StringBuilder();
String[] strArr = str.split(" ");
for (String s : strArr) {
switch (s) {
case "and":
sb.append(" 然后 ");
break;
case "up":
sb.append("向上");
break;
case "down":
sb.append("向下");
break;
case "left":
sb.append("向左");
break;
case "right":
sb.append("向右");
break;
case "move":
sb.append("移动");
break;
case "run":
sb.append("快速移动");
break;
default:
sb.append(s).append("个单位");
}
}
System.out.println(sb);
}
// 运行结果:
// 不用解释器模式实现需求:
// --------------
// 向左移动12个单位 然后 向下快速移动5个单位
// 向下快速移动3个单位
// 向右快速移动1个单位 然后 向左移动12个单位 然后 向左快速移动4个单位 然后 向下移动12个单位
}
}
不用解释器模式实现的代码量更少、类的数量也更少而且运行速度也更快。所以为什么要用解释器模式呢?
- 文法规则让需求更加清晰。上面分析的5个表达式完整的表示了这个需求,通过拆解表达式的形式,由复合语句到不可拆分表达式。
- 扩展及修改方便。不用解释器的情况下条件判断语句太多了,如果修改或者增加某个含义,则在修改代码时将会遇到很大的困难。而解释器模式把各种语句以类的形式来表示,只需要修改相应类即可。
1.3.3 Context的作用
上下文Context 类用于存储解释器之外的一些全局信息。通常作为参数被传递到所有表达式的解释方法interpret()中,可以在Context对象中存储和访问表达式解释器的状态,向表达式解释器提供一些全局的、公共的数据。此外,还可以在Context中增加一些所有表达式解释器都共有的功能,减轻解释器的职责。
需求描述:实现一套简单的基于字符串界面的格式化指令,可以根据输入的指令在字符串界面中输出一些格式化内容。
例如:”LOOP 2 PRINT 今天发工资啦 SPACE SPACE PRINT 亲爱的老婆 BREAK END PRINT 走 SPACE SPACE PRINT 下馆子去!” 将输出如下结果:
今天发工资啦 亲爱的老婆
今天发工资啦 亲爱的老婆
走 下馆子去!
BREAK | 换行 | SPACE | 空格 | END | 循环结束 |
| 打印,后面字符串表示打印的内容 | LOOP | 循环,后面的数字表示循环次数 |
表 关键字表示含义
每个关键字对应一条命令,程序根据关键字执行相应的处理操作。
primitive ::= ‘PRINT string’ | ‘BREAK’ | ‘SPACE’; // 基本命令,string 为字符串
command ::= loop | primitive; // 语句命令
expression ::= command *; // 表达式,一个表达式包含多条命令
loop ::= LOOP number expression ‘END’; // j循环命令,number 为自然数
public class Context {
private final String[] commandStrArr;
private int currentPos = 0;
public Context(String str) {
commandStrArr = str.split(" ");
}
public String getCurrentCommand() {
return commandStrArr[currentPos];
}
public void skip() { ++currentPos;}
public void interpreter(List<AbstractCommand> commandList) {
while (true) {
if (currentPos >= commandStrArr.length) {
break;
}
if ("END".equals(commandStrArr[currentPos])) {
skip();
break;
}
AbstractCommand command = new ConcreteCommand();
command.interpreter(this);
commandList.add(command);
}
}
}
public interface AbstractCommand {
void interpreter(Context context);
void execute();
}
public class ConcreteCommand implements AbstractCommand{
private AbstractCommand command;
@Override
public void interpreter(Context context) {
if ("LOOP".equals(context.getCurrentCommand())) {
command = new LoopCommand();
} else {
command = new PrimitiveCommand();
}
command.interpreter(context);
}
@Override
public void execute() {
command.execute();
}
}
public class ExpressionCommand implements AbstractCommand{
private final List<AbstractCommand> commandList = new ArrayList<>();
@Override
public void interpreter(Context context) {
context.interpreter(commandList);
}
@Override
public void execute() {
for (AbstractCommand command : commandList)
command.execute();
}
}
public class LoopCommand implements AbstractCommand{
private Integer number;
private AbstractCommand command;
@Override
public void interpreter(Context context) {
context.skip();
String value = context.getCurrentCommand();
context.skip();
number = Integer.valueOf(value);
command = new ExpressionCommand();
command.interpreter(context);
}
@Override
public void execute() {
for (int i = 0; i < number; i++) {
command.execute();
}
}
}
public class PrimitiveCommand implements AbstractCommand {
private String value;
@Override
public void interpreter(Context context) {
switch (context.getCurrentCommand()) {
case "PRINT":
context.skip();
value = context.getCurrentCommand();
break;
case "BREAK":
value = "\n";
break;
case "SPACE":
value = " ";
break;
}
context.skip();
}
@Override
public void execute() {
System.out.print(value);
}
}
public class Client {
private final static String commandStr = "LOOP 2 LOOP 2 PRINT 今天发工资啦 SPACE SPACE PRINT 亲爱的老婆 BREAK END PRINT 走 SPACE SPACE PRINT 下馆子去 BREAK END";
public static void main(String[] args) {
Context context = new Context(commandStr);
AbstractCommand command = new ExpressionCommand();
command.interpreter(context);
command.execute();
// 运行结果:
// 今天发工资啦 亲爱的老婆
// 今天发工资啦 亲爱的老婆
// 走 下馆子去
// 今天发工资啦 亲爱的老婆
// 今天发工资啦 亲爱的老婆
// 走 下馆子去
}
}
在这里,Context的作用是存储一些公共变量及实现公有方法。
1.3.4 解释器模式实现简单加减法
需求描述:实现简单的加减法接收器,只需输入一个表达式,它就能计算出表达式结果。比如输入字符串:“2+4-1+4-5”,计算机输出4。
number ::= a integer;// 一个整数
operation ::= expression ‘+|-’ expression
expression ::= number | operation
public class Context {
private final String[] expressionArr;
private Integer currentPos = 0;
public Context(String str) {
expressionArr = str.split(" ");
}
public String getCurrentExp() {
return getExp(0);
}
public String getNextExp() {
return getExp(1);
}
public String getPreExp() {
return getExp(-1);
}
private String getExp(int num) {
return currentPos + num >= expressionArr.length || currentPos + num < 0 ? null : expressionArr[currentPos + num];
}
public void skip(int num) {
currentPos += num;
}
public void interpreter(AbstractExpression preExpression) {
}
}
public interface AbstractExpression {
void interpreter(Context context);
Integer execute();
}
public class ConcreteExpression implements AbstractExpression{
private AbstractExpression expression;
@Override
public void interpreter(Context context) {
if (context.getNextExp() != null) {
expression = new OperationExpression();
} else {
expression = new NumberExpression();
}
expression.interpreter(context);
}
@Override
public Integer execute() {
return expression.execute();
}
}
public class NumberExpression implements AbstractExpression{
private Integer number;
@Override
public void interpreter(Context context) {
number = Integer.valueOf(context.getCurrentExp());
String type = context.getPreExp();
if ("-".equals(type)) {
number = -number;
}
}
@Override
public Integer execute() {
return number;
}
}
public class OperationExpression implements AbstractExpression{
private AbstractExpression leftExpression;
private AbstractExpression rightExpression;
@Override
public void interpreter(Context context) {
leftExpression = new NumberExpression();
leftExpression.interpreter(context);
context.skip(2);
rightExpression = new ConcreteExpression();
rightExpression.interpreter(context);
}
@Override
public Integer execute() {
return leftExpression.execute() + rightExpression.execute();
}
}
/* 实现简单的加减法接收器,只需输入一个表达式,
它就能计算出表达式结果。比如输入字符串:
“2 + 4 - 1 + 4 - 5”,计算机输出4。 */
public class Client {
private static final String STR = "23 - 5 + 22 - 3 - 33 - 22 + 1";
public static void main(String[] args) {
Context context = new Context(STR);
AbstractExpression expression = new ConcreteExpression();
expression.interpreter(context);
System.out.println(expression.execute());
// 运行结果:
// 17
}
}
2 优缺点
优点:
- 易于改变和扩展文法,使用类来表示文法规则,因此可以通过继承等机制来改变或扩展文法。
- 实现文法较为容易。增加新的解释表达式较为方便,只需增加相关表达式类即可,原有表达式类无须修改,符合开闭原则。
缺点:
- 对于复杂文法难以维护。如果一种语言包含太多文法规则,类的数量将会急剧增加,导致系统难以管理和维护,可以考虑使用语法分析程序等方式来取代解释器模式。
- 执行效率较低,使用了大量循环和递归调用,而且代码调试过程也比较麻烦。
3 适用场景
- 可以将一个需要解释执行的语言中的句子表示为一个抽象语法树。
- 执行效率不是关键,一个语言的文法较为简单。