解释器模式

设计模式笔记:解释器模式 (Interpreter Pattern)

一、一句话概括

给定一种语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。

二、为什么需要它 (Why - The Pain Point)

在某些特定领域,你可能需要处理一些简单、重复的指令或表达式。例如:

  • 搜索引擎:用户输入 “cat AND (dog OR bird)”。
  • 数学计算器:处理 “3 + 4 * 2”。
  • 机器人控制:解析 “MOVE FORWARD 10; TURN RIGHT;”。

痛点:
如果为每一种可能的表达式都写一堆 if-elseswitch 逻辑来处理,代码会变得极其复杂、难以维护和扩展。你需要一种结构化的方式来表示和解析这些“迷你语言”。

核心问题:如何为一个简单的语言构建一个解析引擎,使其能够轻松地解释和执行该语言的指令?

三、它是什么 (What - The Solution)

解释器模式建议为语言中的每条语法规则创建一个类。然后,将一个句子表示成一个由这些类的实例构成的抽象语法树(AST, Abstract Syntax Tree)。最后,通过调用树根节点的 interpret() 方法来解释整个句子。

核心思想和角色:

  1. **抽象表达式 (AbstractExpression)**:
    • 声明一个 interpret() 方法,所有语法树中的节点(终结符和非终-结符)都实现这个接口。
  2. **终结符表达式 (TerminalExpression)**:
    • 实现了 AbstractExpression 接口,代表了语言中的“终结符”(即最基本的元素,不能再被分解)。例如,数字、变量名。
    • 它的 interpret() 方法通常是返回自身的值。
  3. **非终结符表达式 (NonterminalExpression)**:
    • 也实现了 AbstractExpression 接口,代表了语言中的“非终结符”(即语法规则,如加法、减法)。
    • 它通常包含对其他 AbstractExpression(它的子节点)的引用。
    • 它的 interpret() 方法会递归地调用其子节点的 interpret() 方法,并组合它们的结果。
  4. **上下文 (Context)**:
    • 包含一些解释器需要用到的全局信息,比如变量的值映射表。
  5. **客户端 (Client)**:
    • 负责构建代表特定句子的抽象语法树,并调用其 interpret() 方法。

四、怎么做 (How - The Blueprint)

基本步骤(以计算 “a + b - c” 为例):

  1. 创建 Expression 接口,包含 interpret(Context context) 方法。
  2. 创建终结符表达式 NumberExpression,代表一个数字或变量。
  3. 创建非终结符表达式 AddExpressionSubtractExpression。每个都持有两个 Expression 的引用(左表达式和右表达式)。
  4. 客户端根据输入的字符串,手动或通过一个解析器来构建语法树:new SubtractExpression(new AddExpression(new NumberExpression("a"), new NumberExpression("b")), new NumberExpression("c"))
  5. 创建 Context,放入 a, b, c 的值。
  6. 调用根节点的 interpret() 方法。

Java 示例代码

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import java.util.Map;
import java.util.HashMap;

// 1. 抽象表达式
interface Expression {
int interpret(Map<String, Integer> context);
}

// 2. 终结符表达式
class NumberExpression implements Expression {
private String variable;
public NumberExpression(String variable) { this.variable = variable; }
@Override
public int interpret(Map<String, Integer> context) {
return context.get(variable);
}
}

// 3. 非终结符表达式
class AddExpression implements Expression {
private Expression left, right;
public AddExpression(Expression left, Expression right) { this.left = left; this.right = right; }
@Override
public int interpret(Map<String, Integer> context) {
return left.interpret(context) + right.interpret(context);
}
}

class SubtractExpression implements Expression {
private Expression left, right;
public SubtractExpression(Expression left, Expression right) { this.left = left; this.right = right; }
@Override
public int interpret(Map<String, Integer> context) {
return left.interpret(context) - right.interpret(context);
}
}

// 客户端
public class InterpreterPatternDemo {
public static void main(String[] args) {
// 4. 上下文
Map<String, Integer> context = new HashMap<>();
context.put("a", 10);
context.put("b", 5);
context.put("c", 2);

// 5. 构建语法树: a + b - c
Expression expression = new SubtractExpression(
new AddExpression(
new NumberExpression("a"),
new NumberExpression("b")
),
new NumberExpression("c")
);

// 6. 解释
int result = expression.interpret(context);
System.out.println("Result of 'a + b - c' is: " + result); // 10 + 5 - 2 = 13
}
}

五、UML 类图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@startuml
skinparam classAttributeIconSize 0
hide empty members

interface Expression {
+ int interpret(Context context)
}
class NumberExpression implements Expression
class AddExpression implements Expression
class SubtractExpression implements Expression

AddExpression o-- "2" Expression
SubtractExpression o-- "2" Expression

class InterpreterPatternDemo {
}
InterpreterPatternDemo ..> Expression : builds & uses

@enduml

六、优缺点分析

优点 (Pros):

  1. 易于扩展语法:增加新的语法规则只需要增加新的表达式类,符合开闭原则。
  2. 结构清晰:用类来表示语法规则,使得文法结构非常清晰,易于实现。

缺点 (Cons):

  1. 复杂性高:对于每条语法规则都需要创建一个类,当文法非常复杂时,会导致类的数量急剧增加。
  2. 性能问题:解释过程通常涉及到大量的递归调用,效率可能不高。
  3. 适用场景有限:只适用于定义和解释一些简单、重复的“迷你语言”。对于复杂的文法(如编程语言),通常会使用更专业的工具(如 ANTLR, JavaCC)。