Java 设计模式心法之第26篇 - 解释器 (Interpreter) - 构建领域特定语言的解析引擎
在某些特定的应用领域,我们可能需要处理一种简单的、自定义的“语言”——例如,数学表达式(如 a + b - c
)、搜索查询语句(如 author=Martin AND title=Refactoring
)、或者一套自定义的业务规则。直接用硬编码的 if-else
或复杂的逻辑来解析和执行这些“语言”的语句,往往会导致代码难以理解、扩展和维护。有没有一种方法,能够像构建语言编译器或解释器那样,为这种“语言”定义一套清晰的文法(语法规则),并提供一个解释器来根据这套文法解析并执行语句呢?本文将带你深入理解行为型模式中的“语言翻译官”——解释器模式。我们将揭示它如何通过为语言的每条文法规则创建一个对应的类(表达式类),并将语句表示为一个由这些类的实例构成的抽象语法树 (AST),然后通过递归地解释这棵树来完成语句的执行,从而实现一个简单、灵活且易于扩展的“迷你语言”解析引擎。正则表达式 (java.util.regex.Pattern
) 的内部机制就运用了解释器模式的思想。
一、问题的提出:当“简单规则”遭遇“复杂解析”
想象一下你需要开发一个功能,允许用户输入简单的数学算术表达式(只包含加减法和数字),并计算出结果。例如,用户输入 "10 + 5 - 3"
,程序需要输出 12
。
一种直接的做法可能是:
- 解析字符串,按照空格分割成操作数和操作符。
- 使用一个循环或复杂的
if-else
逻辑,根据操作符的优先级(虽然这里只有加减,优先级相同)和顺序来逐步计算结果。
如果需求稍微复杂一点,比如要支持括号 ()
来改变运算优先级,或者要支持变量(如 a + b
,其中 a
和 b
的值需要从别处获取),那么直接用过程化的方法来解析和计算就会变得极其复杂和脆弱:
- 解析逻辑难以管理: 处理括号、优先级、变量查找等会让解析代码变得非常混乱。
- 扩展困难: 如果想增加新的操作符(如乘除、取模)或者新的语法元素(如函数调用),修改现有的解析逻辑将是一场噩梦,极易引入错误。
- 代码可读性差: 复杂的解析和计算逻辑混杂在一起,难以理解和维护。
我们需要一种更结构化、更面向对象的方式来处理这种“领域特定语言 (Domain-Specific Language, DSL)”的解析和执行问题。我们希望能够:
- 清晰地定义语言的文法(语法规则)。
- 将复杂的语句表示成一种易于操作的数据结构(如树形结构)。
- 提供一种可扩展的方式来解释(执行)这个结构。
二、文法即类,树即语句:解释器模式的核心定义与意图
解释器模式 (Interpreter Pattern) 提供了一种解决方案。它为一种语言定义其文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。
GoF 的经典意图描述是:“给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。”
其核心思想在于将语言的文法规则映射到类的层次结构:
- 定义抽象表达式接口/类 (AbstractExpression): 定义一个接口或抽象类,声明一个
interpret()
(或evaluate
,execute
等)方法。所有代表语言文法元素的类都需要实现这个接口。这个方法通常需要一个上下文 (Context) 对象作为参数,用于存储和传递解释过程中可能需要的全局信息(如变量的值)。 - 创建终结符表达式类 (TerminalExpression): 为语言文法中的**终结符(基本元素,不能再分解的符号,如数字、变量名)**创建一个具体类。它实现了
AbstractExpression
接口。它的interpret()
方法通常直接返回自身代表的值,或者从上下文中查找变量的值。 - 创建非终结符表达式类 (NonterminalExpression): 为语言文法中的**非终结符(由其他表达式组合构成的规则,如加法、减法)**创建一个或多个具体类。它也实现了
AbstractExpression
接口。- 持有子表达式引用: 非终结符表达式内部通常会持有一个或多个指向其他
AbstractExpression
对象(它的子表达式)的引用。例如,一个加法表达式会持有左右两个操作数表达式的引用。 - 递归解释: 它的
interpret()
方法会递归地调用其子表达式的interpret()
方法,然后根据自身的文法规则(如加法规则)将子表达式的结果组合起来,得到最终结果。
- 持有子表达式引用: 非终结符表达式内部通常会持有一个或多个指向其他
- 构建抽象语法树 (Abstract Syntax Tree - AST): 对于一个给定的语句(输入字符串),需要一个解析器 (Parser)(解释器模式本身不包含解析器的构建,通常需要结合其他技术如递归下降、或使用解析器生成工具如 ANTLR)来根据语言文法将该语句解析成一个由各种表达式对象(终结符和非终结符)构成的树形结构——即抽象语法树 (AST)。树的根节点代表整个语句。
- 解释执行 (Interpret Execution): 客户端只需要调用 AST 根节点的
interpret()
方法,并传入必要的上下文。由于interpret()
方法的递归调用机制,整个 AST 会被遍历并解释执行,最终返回语句的执行结果。 - (可选)上下文 (Context): 一个用于存储和传递解释器在解释过程中可能需要的全局信息的对象,例如变量赋值、函数定义等。
核心角色:
- AbstractExpression (抽象表达式): 声明一个抽象的解释操作,这个接口为抽象语法树中所有的节点所共享。
- TerminalExpression (终结符表达式): 实现与文法中的终结符相关联的解释操作。一个句子中的每个终结符都需要一个实例。
- NonterminalExpression (非终结符表达式): 对文法中的每一条规则 R ::= R1 R2…Rn 都需要一个 NonterminalExpression 类。为从 R1 到 Rn 的每个符号都维护一个实例变量。实现解释操作时,会递归调用这些变量(子表达式)的解释操作。
- Context (上下文): 包含解释器之外的一些全局信息。
- Client (客户端): 构建(或由解析器生成)代表一个特定句子的抽象语法树。调用解释操作。
关键:用类层次结构表示语言文法,用递归的 interpret
方法解释由这些类的实例构成的 AST。
三、迷你语言引擎:解释器模式的适用场景
解释器模式通常用于以下场景:
- 当有一个简单的语言需要解释执行,并且你希望将语言的文法表示为可执行的、易于扩展的对象模型时。
- 语言的文法相对简单且稳定: 对于过于复杂的文法,解释器模式可能会导致类的数量急剧增加,维护成本过高。这时更适合使用专门的编译器或解析器生成工具。
- 效率不是关键问题: 解释器模式通常涉及大量的递归调用和对象创建,性能可能不如直接编译执行或优化的解析器。它更侧重于设计的灵活性和文法表示的清晰性。
应用实例:
java.util.regex.Pattern
: Java 的正则表达式引擎内部在编译正则表达式时,会将其转换成一个类似于抽象语法树的内部表示(由各种匹配节点构成),然后通过解释执行这个结构来进行模式匹配。- SQL 解析器: 许多数据库系统或 ORM 框架内部都有 SQL 解析器,将 SQL 语句解析成 AST,然后解释执行或进一步优化、编译。
- 各种规则引擎或脚本语言解释器(简单场景)。
- 编译器中的中间表示 (Intermediate Representation) 解释: 在某些编译阶段,可能会对 AST 或其他中间表示进行解释执行以进行分析或优化。
需要注意的是,解释器模式在通用业务开发中并不常用,它更多地出现在构建编译器、解释器、规则引擎等特定领域的基础设施中。
四、文法化类,树化语句:解释器模式的 Java 实践
我们以计算简单的加减法表达式为例(如 “10 + 5 - 3”),演示解释器模式的实现。
1. 定义抽象表达式接口 (AbstractExpression):
import java.util.Map; // 用于上下文存储变量值/*** 抽象表达式接口*/
interface Expression {// 解释方法,接收上下文 (这里用 Map 存变量值)int interpret(Map<String, Integer> context);
}
2. 创建终结符表达式类 (TerminalExpression):
(这里我们有两种终结符:数字和变量)
/*** 终结符表达式:数字*/
class NumberExpression implements Expression {private int number;public NumberExpression(int number) { this.number = number; }@Overridepublic int interpret(Map<String, Integer> context) {// 数字直接返回值return number;}
}/*** 终结符表达式:变量*/
class VariableExpression implements Expression {private String name;public VariableExpression(String name) { this.name = name; }@Overridepublic int interpret(Map<String, Integer> context) {// 从上下文中查找变量的值if (context.containsKey(name)) {return context.get(name);}// 如果变量未定义,可以抛异常或返回默认值System.err.println("警告:变量 '" + name + "' 未在上下文中定义,默认为 0");return 0;}
}
3. 创建非终结符表达式类 (NonterminalExpression):
(这里我们有加法和减法两种非终结符)
/*** 非终结符表达式:加法*/
class AddExpression implements Expression {private Expression leftOperand; // 左操作数表达式private Expression rightOperand; // 右操作数表达式public AddExpression(Expression left, Expression right) {this.leftOperand = left;this.rightOperand = right;}@Overridepublic int interpret(Map<String, Integer> context) {// 递归解释左右子表达式,然后执行加法return leftOperand.interpret(context) + rightOperand.interpret(context);}
}/*** 非终结符表达式:减法*/
class SubtractExpression implements Expression {private Expression leftOperand;private Expression rightOperand;public SubtractExpression(Expression left, Expression right) {this.leftOperand = left;this.rightOperand = right;}@Overridepublic int interpret(Map<String, Integer> context) {// 递归解释左右子表达式,然后执行减法return leftOperand.interpret(context) - rightOperand.interpret(context);}
}
4. 构建抽象语法树 (AST) 与客户端使用:
(简单起见,我们手动构建 AST,实际应用中需要解析器)
import java.util.HashMap;
import java.util.Stack; // 可以用来辅助解析public class InterpreterClient {public static void main(String[] args) {// === 示例 1: 解释表达式 "10 + 5 - 3" ===System.out.println("=== 解释 '10 + 5 - 3' ===");// 手动构建 AST: (10 + 5) - 3// 叶子节点Expression num10 = new NumberExpression(10);Expression num5 = new NumberExpression(5);Expression num3 = new NumberExpression(3);// 中间节点Expression sum = new AddExpression(num10, num5); // 10 + 5// 根节点Expression finalExpr1 = new SubtractExpression(sum, num3); // (10 + 5) - 3// 创建上下文 (这里不需要变量,为空 map)Map<String, Integer> context1 = new HashMap<>();// 解释执行 ASTint result1 = finalExpr1.interpret(context1);System.out.println("表达式 '10 + 5 - 3' 的计算结果: " + result1); // 输出 12// === 示例 2: 解释带变量的表达式 "a + b - c" ===System.out.println("\n=== 解释 'a + b - c' ===");// 手动构建 AST: (a + b) - cExpression varA = new VariableExpression("a");Expression varB = new VariableExpression("b");Expression varC = new VariableExpression("c");Expression sumAB = new AddExpression(varA, varB);Expression finalExpr2 = new SubtractExpression(sumAB, varC);// 创建上下文,并为变量赋值Map<String, Integer> context2 = new HashMap<>();context2.put("a", 20);context2.put("b", 8);context2.put("c", 5);System.out.println("上下文变量: a=" + context2.get("a") + ", b=" + context2.get("b") + ", c=" + context2.get("c"));// 解释执行 ASTint result2 = finalExpr2.interpret(context2);System.out.println("表达式 'a + b - c' 的计算结果: " + result2); // 输出 23// === 实际应用中 ===// String expressionString = "x - 10 + y";// Expression astRoot = buildAST(expressionString); // 需要一个解析器来构建 AST// Map<String, Integer> runtimeContext = ... ; // 运行时获取变量值// int finalResult = astRoot.interpret(runtimeContext);}// 注意:构建 AST 的解析器 (如 buildAST 方法) 通常比解释器本身更复杂,// 解释器模式主要关注如何表示文法和解释 AST,而非如何构建 AST。// 可以使用递归下降、栈或其他解析技术,或 ANTLR 等工具来生成解析器。
}
代码解读:
Expression
是抽象表达式接口,定义了interpret
方法。NumberExpression
和VariableExpression
是终结符,它们的interpret
方法直接返回值或从上下文中取值。AddExpression
和SubtractExpression
是非终结符,它们持有左右子表达式的引用,并在interpret
方法中递归调用子表达式的interpret
方法,然后执行自身的运算(加或减)。- 客户端代码负责手动构建了表示表达式
"10 + 5 - 3"
和"a + b - c"
的抽象语法树 (AST)。 - 客户端创建了上下文
Map
来存储变量的值(如果需要)。 - 客户端调用 AST 根节点的
interpret
方法,触发整个树的递归解释,得到最终结果。
五、模式的价值:解释器带来的文法清晰与扩展性
解释器模式的主要价值在于:
- 文法表示清晰 (Clear Grammar Representation): 语言的每条文法规则都直接映射到一个类,使得文法结构清晰,易于理解。
- 易于扩展文法 (Easy to Extend Grammar): 如果需要给语言增加新的规则(如新的操作符或语法结构),通常只需要增加新的
AbstractExpression
子类(终结符或非终结符)即可。符合开闭原则 (OCP)(对于扩展操作而言)。 - 易于实现语言解释器 (Easy to Implement Interpreter): 一旦文法被表示为类层次结构,实现解释器(即
interpret
方法)就相对直接,通常是递归调用。 - 行为与表示分离 (Separation of Behavior and Representation): 解释逻辑(
interpret
方法)封装在表达式类中,与如何构建 AST(解析过程)分离。
六、权衡与考量:解释器模式的适用性限制
使用解释器模式需要特别注意其局限性:
- 仅适用于简单文法 (Suitable Only for Simple Grammars): 对于复杂的文法,解释器模式会导致类层次结构非常庞大且难以维护。每条规则一个类,很快就会导致类的数量爆炸。对于复杂语言,通常需要使用更专业的编译器技术(如解析器生成器)。
- 性能问题 (Performance Issues): 解释器模式通常涉及大量的类创建(构建 AST)和递归调用(解释 AST),其执行效率往往不如直接编译的代码或优化的解析器。对于性能要求高的场景不适用。
- 需要解析器 (Requires a Parser): 模式本身不负责将输入语句转换成 AST,这需要一个额外的解析器来完成,而解析器的构建本身可能就很复杂。
因此,解释器模式是一个相对“小众”的模式,只在处理特定领域内的简单语言或规则解释时才考虑使用。
七、心法归纳:文法化类,递归解释
解释器模式的核心“心法”在于**“文法化类”与“递归解释”**:
- 文法化类 (Grammar as Classes): 将目标语言的语法规则(文法)直接映射到一组类(
AbstractExpression
的子类)上。终结符对应叶子节点类,非终结符(产生式规则)对应容器节点类。 - 递归解释 (Recursive Interpretation): 将输入的语句解析成由这些类的实例构成的抽象语法树 (AST)。通过在每个表达式类中实现一个递归的
interpret
方法,从根节点开始调用,即可完成对整个 AST 的遍历和解释执行。
掌握解释器模式,意味着你理解了:
- 一种将语言文法与面向对象设计相结合的有趣方式。
- 如何通过类层次结构来表示和解释简单的领域特定语言。
- 抽象语法树 (AST) 和递归解释的基本概念。
- 该模式的适用场景(简单文法)和局限性(复杂性、性能)。
当你需要为某个特定领域设计一套简单的规则或表达式语言,并需要一个灵活、易于扩展的解释引擎时,解释器模式提供了一种结构化的思考框架和实现途径。但务必审慎评估语言的复杂度和性能要求,避免将其误用于过于复杂的场景。
GoF 23 种经典设计模式已全部探讨完毕!