解释器模式

解释器模式

一、什么是解释器模式?

解释器模式(Interpreter Design Pattern):对具有某种语法规则的语言表示,定义一个解释器来处理这个语法

Interpreter pattern is used to defines a grammatical representation for a language and provides an interpreter to deal with this grammar.

解释器模式的一些特点:

  • 存在某种语法规则,比如算术运算表达式,中英文翻译等
  • 语法规则可能会出现循环、递归
  • 解释器模式就是根据语法规则去解读“句子”

二、为什么要用解释器模式?

场景:

  • 一些重复出现的问题可以用一种简单的语言来表达
  • 当有一个语言中的句子可以表示为一个抽象语法树时

原理:

  • 简单的语法规则,可以不用解释器模式,防止过度设计
  • 复杂的语法规则解析,逻辑复杂,代码量多
  • 复杂语法解析代码都写在一个类中,代码的耦合度会很高,并且不易维护和扩展
  • 将语法规则拆分成多条细则,拆分到各个小类中,避免解析类过于膨胀
  • 每条细则,都是单独的单元,只负责对这部分语法规则进行解析

三、解释器的结构是怎么样的?

解释器中包含的角色有:

  • 抽象表达式(AbstrctExpression):定义了语法树节点的共同接口
  • 终结符表达式(TerminalExpression):负责语法树中的叶子节点
  • 非终结符表达式(NoterminalExpression):负责语法树中的非叶子节点
  • 上下文环境(Context):提供必要信息或者公共的方法

解释器结构

示例程序结构:

示例程序结构

抽象表达式(AbstrctExpression):

1
2
3
public abstract class Node {
public abstract void parse(Context context) throws ParseException;
}

终结符表达式(TerminalExpression):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class PrimitiveCommandNode extends Node {

private String name;

@Override
public void parse(Context context) throws ParseException {
// <primitive command> ::= go | right | left
name = context.currentToken();
context.skipToken(name);
if (!"go".equals(name) && !"right".equals(name) && !"left".equals(name)) {
throw new ParseException(name + " is undefined");
}
}

@Override
public String toString() {
return name;
}
}

非终结符表达式(NoterminalExpression):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ProgramNode extends Node {
private Node commandListNode;

@Override
public void parse(Context context) throws ParseException {
// <program> ::= program <command list>
context.skipToken("program");
commandListNode = new CommandListNode();
commandListNode.parse(context);
}

@Override
public String toString() {
return "[" + "program=" + commandListNode + "]";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class CommandListNode extends Node {

private List<Node> commandNodes = new ArrayList<>();

@Override
public void parse(Context context) throws ParseException {
// <command list> ::= <command>* end
while (true) {
if (context.currentToken() == null) {
throw new ParseException("Missing end");
}
if ("end".equals(context.currentToken())) {
context.skipToken("end");
break;
}
Node commandNode = new CommandNode();
commandNode.parse(context);
commandNodes.add(commandNode);
}
}

@Override
public String toString() {
return commandNodes.toString();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class CommandNode extends Node {

private Node node;

@Override
public void parse(Context context) throws ParseException {
// <command> ::= <repeat command> | <primitive command>
if ("repeat".equals(context.currentToken())) {
node = new RepeatCommandNode();
} else {
node = new PrimitiveCommandNode();
}
node.parse(context);
}

@Override
public String toString() {
return node.toString();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class RepeatCommandNode extends Node {

private int number;
private Node commandListNode;

@Override
public void parse(Context context) throws ParseException {
// <repeat command> ::= repeat <number> <command list>
context.skipToken("repeat");
number = context.currentNumber();
context.nextToken();
commandListNode = new CommandListNode();
commandListNode.parse(context);
}

@Override
public String toString() {
return "[repeat " + number + " " + commandListNode + "]";
}
}

上下文环境(Context)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class Context {

private StringTokenizer tokenizer;
private String currentToken;

public Context(String text) {
tokenizer = new StringTokenizer(text);
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) throws ParseException {
if (!token.equals(currentToken)) {
throw new ParseException("Warning: " + token + " is expected, but " + currentToken + " is found");
}
nextToken();
}

public int currentNumber() throws ParseException {
int number = 0;
try {
number = Integer.parseInt(currentToken);
} catch (Exception e) {
throw new ParseException("Warning: " + e);
}
return number;
}

}

四、解释器模式有什么优缺点?

优点:

  • 符合“单一职责”原则,每个语法细则都是一个类,只负责自己那部分的解析
  • 可扩展性好,扩展新的语法规则简单,只需要新增类或者继承已有类即可

缺点:

  • 引起类膨胀,每条规则都定义一个类,如果规则很多,就会出现大量的类
  • 执行效率低,使用了大量的循环和递归,同时调试也很麻烦
  • 应用场景少,简单的规则一般用不上解释器模式,复杂规则的场景比较少,一般用在语言解析比较多

五、应用场景

  • 正则表达式
  • 算术运算表达式
作者

jiaduo

发布于

2022-01-15

更新于

2023-04-03

许可协议