1 基本介绍
解释器模式(Interpreter Pattern)是一种 行为型 设计模式,它通过 定义一个解释器 来解释语言中的表达式,从而实现对语言的解析和执行。
2 案例
本案例定义了一个针对机器小车的“小车语言”,使用 Java 语言编写 解释器,将 含有特殊指令的指令集 解释成 只含有基础指令的指令集。
先讲几个概念,有助于之后的理解:
- 记号:像
int i = 10;
中的int
、=
、;
这种指定的字符串就是记号,是一个语言规定的字符串。在小车语言中,记号有如下几种:advance
:前进指令。left
:左转指令。right
:右转指令。repeat
:重复指令,后面会有 重复的次数 和 重复的指令集,并且有与其配对的end
。数字
:在本案例中,数字只会出现在repeat
指令的后面,表示重复的次数。start
:表示指令集的开始。end
:表示 指令集 的结束。
- 基础指令:像
advance
、left
、right
这样的指令无法内含其他指令,所以称为 基础指令。 - 重复指令:像
repeat
这样的指令内部含有其他指令,是一种特殊指令。 - 起始指令:
start
表示指令集的开始,也是一种特殊指令。 - 指令集:可以将其理解成方法的 代码块,
start
或repeat
开始,以end
结束。
此外,本案例实现的 解释器 要求每条指令都与其他指令 至少间隔一个空格,这是为了简化 解释器 的编写。
例如 start repeat 4 advance right end end
就表示将 advance
和 right
这两条指令重复四次,形成的指令集为 advance right advance right advance right advance right
。
2.1 Instruction 接口
public interface Instruction { // 指令
void parse(Context context); // 解析具体的指令
}
2.2 StartInstruction 类
public class StartInstruction implements Instruction { // 起始指令
private Instruction instructionList; // 整个小车程序的指令集
@Override
public void parse(Context context) {
instructionList = new InstructionList();
instructionList.parse(context);
}
@Override
public String toString() {
return "指令集为:{ " + instructionList.toString() + " }";
}
}
2.3 PrimitiveInstruction 类
public class PrimitiveInstruction implements Instruction { // 基础指令
private String name; // 基础指令的名称
@Override
public void parse(Context context) {
name = context.currToken(); // 获取记号
context.skipToken(); // 跳过这个记号
if (!"advance".equals(name) && !"left".equals(name) && !"right".equals(name)) {
throw new IllegalArgumentException("未定义的记号「" + name + "」");
}
}
@Override
public String toString() {
return name;
}
}
2.4 RepeatInstruction 类
public class RepeatInstruction implements Instruction { // 重复指令
private int times; // 重复的次数
private Instruction instructionList; // 重复的指令集
@Override
public void parse(Context context) { // 执行集合中的所有指令
context.skipToken(); // 跳过这个记号
times = context.currNumber(); // 获取重复的次数
// 解析 重复的指令集
instructionList = new InstructionList();
instructionList.parse(context);
}
@Override
public String toString() {
// 将 重复的指令集的字符串 拼接 times 次
String listString = instructionList.toString();
StringBuilder builder = new StringBuilder(listString);
for (int i = 1; i < times; i++) {
builder.append(" ").append(listString);
}
return builder.toString();
}
}
2.5 InstructionList 类
import java.util.ArrayList;
import java.util.List;
public class InstructionList implements Instruction { // 指令集
private List<Instruction> instructions = new ArrayList<>(); // 存储指令的集合
@Override
public void parse(Context context) {
while (true) {
String currToken = context.currToken(); // 当前的记号
if (currToken == null) {
throw new IllegalArgumentException("缺少记号 'end'");
} else if ("start".equals(currToken)) {
context.skipToken(); // 跳过对 "start" 的检查
} else if ("end".equals(currToken)) {
context.skipToken(); // 跳过对 "end" 的检查
break; // 直接退出解析
} else if ("repeat".equals(currToken)) { // 如果记号是 "repeat"
// 则使用 RepeatInstruction 的实例进行解析
Instruction instruction = new RepeatInstruction();
instruction.parse(context);
instructions.add(instruction); // 将这条指令放到集合中
} else { // 否则记号就是 "advance", "left", "right"
// 则使用 PrimitiveInstruction 的实例进行解析
Instruction instruction = new PrimitiveInstruction();
instruction.parse(context); // 如果不是这三种记号,则会在这个方法中报错
instructions.add(instruction); // 将这条指令放到集合中
}
}
}
@Override
public String toString() {
if (instructions.isEmpty()) { // 如果指令集为空
return "empty"; // 则返回 "empty"
}
// 将指令集合中的指令拼接到一起
StringBuilder builder = new StringBuilder(instructions.get(0).toString());
for (int i = 1; i < instructions.size(); i++) {
builder.append(" ").append(instructions.get(i).toString());
}
return builder.toString();
}
}
2.6 Context 类
public class Context { // 上下文,存储指令中的所有记号
private String[] instructions; // 存储指令中记号的数组
private int currTokenIndex; // 当前记号的下标
public Context(String instructionString) {
this.instructions = instructionString.split(" "); // 用空格分隔记号
}
public void skipToken() { // 跳过当前记号
currTokenIndex++;
}
public String currToken() { // 返回当前记号
// 如果没有剩余记号,则返回 null;否则返回当前记号
return hasRestToken() ? instructions[currTokenIndex] : null;
}
public int currNumber() { // 获取当前记号表示的数字,并让下标指向下一个记号
return Integer.parseInt(instructions[currTokenIndex++]);
}
private boolean hasRestToken() { // 检查是否有剩余记号
return currTokenIndex < instructions.length;
}
}
2.7 Client 类
public class Client { // 客户端,测试了解释器解析指令
public static void main(String[] args) {
Instruction instruction = new StartInstruction();
instruction.parse(new Context("start end"));
System.out.println(instruction);
instruction.parse(new Context("start repeat 5 advance end end"));
System.out.println(instruction);
instruction.parse(
new Context("start repeat 3 repeat 2 advance end right end end"));
System.out.println(instruction);
}
}
2.8 Client 类的运行结果
指令集为:{ empty }
指令集为:{ advance advance advance advance advance }
指令集为:{ advance advance right advance advance right advance advance right }
2.9 总结
本案例有一个 bug,就是在写完合法的指令后再写任意个 end
都是合法的,例如 start left end end
,这是因为直接跳过了对 end
的判断,没有判断 end
是否有对应的 start
或 repeat
。但这点不会导致指令集的翻译出问题,所以就没有理会。
如果想要添加一种新的基础指令,例如 sound
发出响声,则只需要让它实现 Instruction
接口 和 实现 parse()
方法,并在 InstructionList
类的 parse()
方法中添加新的分支语句即可,然后小车语言就支持了一个新的指令。可以看出,这种模式增强了系统的扩展性。
3 各角色之间的关系
3.1 角色
3.1.1 AbstractExpression ( 抽象表达式 )
该角色负责 定义 用于解释语法的 接口。本案例中,Instruction
接口扮演了该角色。
3.1.2 TerminalExpression ( 终结符表达式 )
该角色对应 终结符表达式(类似二叉树的叶子节点),不需要被进一步展开,实现了 AbstractExpression 角色定义的 接口。本案例中,PrimitiveInstruction
类扮演了该角色。
3.1.3 NonTerminalExpression ( 非终结符表达式 )
该角色对应 非终结符表达式(类似二叉树的非叶子节点),需要被进一步展开,实现了 AbstractExpression 角色定义的 接口。本案例中,StartInstruction, RepeatInstruction, InstructionList
类都在扮演该角色。
3.1.4 Context ( 上下文 )
该角色负责 为解释器进行语法解析提供必要的信息,也就是为 TerminalExpression 角色和 NonTerminalExpression 角色服务。本案例中,Context
类扮演了该角色。
3.1.5 Client ( 客户端 )
该角色负责 生成 Context 角色的实例用以保存语句,调用 TerminalExpression 角色和 NonTerminalExpression 角色的解析方法进行解析。本案例中,Client
类扮演了该角色。
3.2 类图
说明:
- TerminalExpression 和 NonTerminalExpression 都使用了 Context,合起来就是 AbstractExpression 使用了 Context。
- Client 使用了 TerminalExpression 和 NonTerminalExpression,合起来就是 Client 使用了 AbstractExpression。
- NonTerminalExpression 聚合的 childExpressions 可以是 TerminalExpression,也可以是 NonTerminalExpression,具体可以是单个对象,或是链表、映射这种集合。
4 注意事项
- 语法规则的复杂度:解释器模式最适合用于 语法规则相对简单 且 易于用递归结构表示 的语言。如果语法非常复杂,包含大量的规则和例外情况,那么使用解释器模式可能会导致类数量激增,增加系统的复杂性和维护难度。
- 性能考虑:解释器模式通常通过解释的方式执行语言,其性能可能不如编译执行。在 性能要求较高的场景 下,需要仔细评估解释器模式的适用性。如果可能的话,可以考虑使用其他更高效的技术,如编译技术。
- 错误处理:在实现解释器时,需要充分考虑错误处理机制。由于 解释器需要处理各种可能的输入情况,因此必须能够识别并处理 语法错误、类型错误 等异常情况。这通常需要在解释器的实现中添加适当的错误检测和处理逻辑。
- 类设计:在设计解释器类时,需要保持类的简洁和专一。每个类应该只负责解释一种特定的文法符号,避免将多个符号的解释逻辑放在同一个类中。同时,应该仔细考虑类的 继承关系 和 组合关系,以确保系统的灵活性和可扩展性。
- 上下文的使用:上下文对象在解释器模式中扮演着重要的角色,它通常用于存储全局信息或状态,供各个解释器类共享。在使用上下文对象时,需要注意避免过度依赖它,以免导致类之间的耦合度增加。同时,还需要确保环境对象的线程安全性,以支持多线程环境下的解释执行。
- 递归调用的优化:解释器模式中的解释方法可能会涉及到 递归调用,特别是在 处理嵌套表达式 时。递归调用虽然可以简化代码结构,但在某些情况下可能会导致 栈溢出 等性能问题。因此,在使用递归调用时需要谨慎考虑其性能和安全性,并尝试通过 迭代 等方式进行优化。
- 测试和维护:解释器模式的实现通常比较复杂,因此 需要进行 充分的测试 以确保其正确性和稳定性。
5 优缺点
优点:
- 扩展性好:解释器模式为语法中的每一个符号(终结符 或 非终结符)定义了一个类,因此当需要增加新的语法规则时,只需添加新的类即可,而无需修改其他类的代码。这符合开闭原则(对扩展开放,对修改关闭)。在本案例中没有这样做,这是为了防止代码太多了。
- 灵活性高:由于语法规则是通过类来表示的,因此可以很容易地修改这些规则的实现,以适应不同的解释需求。
- 复用性强:解释器模式中的每个类 通常 只负责一个特定符号的解释,这使得 类之间的耦合度降低,提高了代码的 复用性。
- 易于实现:当语言的语法相对简单时,使用解释器模式可以很容易地实现一个解释器。通过定义一系列类来表示不同的语法规则,可以方便地解释和执行语言中的表达式。
缺点:
- 复杂度高:当语言文法变得复杂时,解释器模式中的类数量会急剧增加,导致系统变得庞大而难以维护。
- 性能问题:由于解释器模式是 通过解释的方式执行语言 的,其执行效率通常比编译执行要低。特别是在处理复杂的表达式时,可能需要大量的循环和递归调用,导致性能下降。
- 难以调试:由于解释器模式中的执行逻辑是通过多个类之间的交互来完成的,因此在调试时可能需要跟踪多个类的执行过程,增加了调试的难度。
- 不适合复杂语法:解释器模式通常适用于语法相对简单且易于用递归结构表示的语言。对于语法复杂、包含大量规则和例外的语言,解释器模式可能不是最佳选择。
6 适用场景
- 编程语言解释器:解释器模式最直接的应用就是 实现编程语言的解释器,编程语言通常包含一系列的语法规则和表达式,解释器模式可以很好地处理这些规则和表达式的解析和执行。
- 配置文件解析:许多应用程序使用配置文件来存储设置和参数,这些配置文件通常具有特定的语法结构,如 XML、JSON 等,使用解释器模式可以方便地解析这些配置文件,并提取出应用程序所需的信息。
- 正则表达式解析:虽然正则表达式本身不是一种编程语言,但它们具有复杂的语法规则,用于匹配字符串中的模式。在某些情况下,可以使用解释器模式来解析和执行正则表达式,尽管这通常不是最高效的方法,因为正则表达式引擎通常已经高度优化。
- 数学表达式求值:数学表达式(如 算术表达式、逻辑表达式 等)的求值 是一个典型的解释器模式应用场景。解释器模式可以解析表达式的语法结构,并按照运算优先级和规则计算表达式的值。
- 查询语言解析:一些应用程序支持 自定义查询语言,用于检索或处理数据。这些查询语言通常具有特定的语法规则,解释器模式可以解析这些规则,并根据查询语句执行相应的操作。
- 游戏规则解析:在游戏开发中,游戏规则可能包含复杂的逻辑和表达式。解释器模式可以用于解析这些规则,并根据游戏状态执行相应的操作。
7 总结
解释器模式 是一种 行为型 设计模式,它定义了一个语言的语法,并解析了语言中的表达式,提高了系统的扩展性和灵活性,但由于解释器本身存在 性能低 的缺点,所以多数情况下还是使用编译器进行优化。如果想要创造一个新的语言(可以是编程语言,也可以是对某种机器的语言),或者想要实现对配置文件的解析器,则本模式很重要。