文章中如果有图看不到,可以点这里去 csdn 看看。从那边导过来的,文章太多,没法一篇篇修改好。

深入浅出设计模式【十五、解释器模式】

一、解释器模式介绍

解释器模式的核心思想是为一种特定的、通常是小型的“语言”或表达式定义一套文法规则,并提供一个解释器来解释执行这些语言中的句子

这种模式主要用来处理那些有稳定文法规则的、且需要频繁解释执行的场景。它将一个文法表示为一个类层次结构(抽象语法树,AST),并将每一个文法规则映射到一个类。通过组合这些类的实例,可以构建出能解释执行复杂句子的解释器。

重要提示: 解释器模式在实际企业级后端开发中应用场景非常有限。它适用于小而简单的语言,对于复杂的、工业级的语言或协议(如SQL、HTTP),通常会使用更强大的工具(如解析器生成器ANTLR)。理解此模式的价值更多在于理解其思想,而非直接应用。

二、核心概念与意图

  1. 核心概念

    • 抽象表达式 (Abstract Expression): 声明一个抽象的 interpret() 操作,它是所有终结符和非终结符表达式所共有的接口。
    • 终结符表达式 (Terminal Expression): 实现与文法中的终结符相关的 interpret() 操作。文法中的每一个终结符都需要一个具体的终结符表达式类。它不再包含其他表达式,是递归的终点。
    • 非终结符表达式 (Nonterminal Expression): 文法中的每一条规则都需要一个具体的非终结符表达式类。它通常包含其他抽象表达式的引用(可以是终结符,也可以是非终结符),并通过递归调用其子表达式的 interpret() 操作来实现自身的 interpret() 操作。
    • 上下文 (Context): 包含解释器之外的一些全局信息。它通常存储并维护解释过程中需要访问的公共数据或状态。
    • 客户端 (Client): 构建(或被给定)一个由 TerminalExpressionNonterminalExpression 实例组成的抽象语法树 (AST)。该抽象语法树代表一个特定的句子。客户端通常会调用顶层表达式的 interpret() 操作来启动解释过程。
  2. 意图

    • 为某个语言定义它的文法表示
    • 定义一个解释器,使用该文法来解释语言中的句子
    • 将一个特定类型的问题(需要解释执行的语句)表达为一个简单的语言

三、适用场景剖析

解释器模式在以下非常特定的场景中可能有效:

  1. 简单的、需要解释执行的语言: 当有一个简单的语言需要解释执行,并且可以将语言中的句子表示为一棵抽象语法树时。例如,简单的布尔表达式、正则表达式、数学公式、业务规则等。
  2. 文法简单且稳定: 该语言的文法相对简单,不会频繁变化。因为每增加一条新的规则,都需要修改和扩展抽象语法树的类结构,违反开闭原则。
  3. 效率不是关键问题: 解释器模式通常效率不高。在性能要求很高的场景下,可能需要将解释器模式转换为其他形式(如编译成字节码)。

不适用场景

  • 复杂的、工业级的语言: 如SQL、Java、XML等。为这些语言实现解释器会导致类结构极其庞大、复杂且难以维护。
  • 文法频繁变化: 如果需要频繁地修改文法并添加新规则,使用解释器模式会非常繁琐。

四、UML 类图解析(Mermaid)

以下UML类图清晰地展示了解释器模式的结构和角色间的关系,以及如何构建抽象语法树(AST):

builds AST
contains
Client
Context
-lookup(String key) : Object
-assign(String key, Object value)
«interface»
AbstractExpression
+interpret(Context context)
TerminalExpression
+interpret(Context context)
NonterminalExpression
-expression1: AbstractExpression
-expression2: AbstractExpression
+interpret(Context context)
  • AbstractExpression (抽象表达式): 声明抽象的 interpret(Context context) 接口,为所有具体表达式(终结符和非终结符)所共有。
  • TerminalExpression (终结符表达式): 代表文法中的基本元素(终结符),不能再被分解。其 interpret(Context context) 方法直接实现与自身相关的操作,通常需要从上下文中获取或存储数据。
  • NonterminalExpression (非终结符表达式): 代表文法中的复合元素(规则),它由一个或多个其他表达式(通过引用)组成。其 interpret(Context context) 方法通常是一个递归过程:它会调用其包含的所有子表达式的 interpret 方法,并根据规则组合它们的结果。
  • Context (上下文): 充当解释器的“环境”,存储和提供解释过程中需要的全局信息。例如,变量名与值的映射关系。
  • Client (客户端)
    • 负责构建(或接收)一个抽象语法树 (AST)。这棵树由 TerminalExpressionNonterminalExpression 的实例递归组合而成,代表一个需要被解释的句子。
    • 创建并初始化一个 Context 对象,该对象可能包含一些初始状态。
    • 调用抽象语法树根节点的 interpret(Context context) 方法来启动解释过程。

五、各种实现方式及其优缺点

解释器模式的实现相对固定,核心在于构建AST和递归解释。

1. 标准实现(类层次结构)

即上述UML所描述的方式,为文法中的每个符号创建一个类。

import java.util.Map;
import java.util.Stack;

// 1. Context: 存储变量值
class Context {
    private Map<String, Integer> variables = new HashMap<>();

    public void setVariable(String name, int value) {
        variables.put(name, value);
    }

    public int getVariable(String name) {
        return variables.getOrDefault(name, 0);
    }
}

// 2. Abstract Expression
interface Expression {
    int interpret(Context context);
}

// 3. Terminal Expression: Variable
class VariableExpression implements Expression {
    private String name;

    public VariableExpression(String name) {
        this.name = name;
    }

    @Override
    public int interpret(Context context) {
        return context.getVariable(name); // 从上下文中获取变量的值
    }
}

// 3. Terminal Expression: Number (Constant)
class NumberExpression implements Expression {
    private int number;

    public NumberExpression(int number) {
        this.number = number;
    }

    @Override
    public int interpret(Context context) {
        return number; // 数字直接返回值本身,与上下文无关
    }
}

// 4. Nonterminal Expression: Addition
class AddExpression implements Expression {
    private Expression left;
    private Expression right;

    public AddExpression(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }

    @Override
    public int interpret(Context context) {
        // 递归解释左表达式和右表达式,然后相加
        return left.interpret(context) + right.interpret(context);
    }
}

// 4. Nonterminal Expression: Subtraction
class SubtractExpression implements Expression {
    private Expression left;
    private Expression right;

    public SubtractExpression(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }

    @Override
    public int interpret(Context context) {
        return left.interpret(context) - right.interpret(context);
    }
}

// 5. Client: Builds AST and interprets
public class Client {
    public static void main(String[] args) {
        // Build AST for expression: a + b - 2
        // Equivalent to: new SubtractExpression(new AddExpression(new Variable("a"), new Variable("b")), new Number(2))
        Context context = new Context();
        context.setVariable("a", 5);
        context.setVariable("b", 3);

        Expression a = new VariableExpression("a");
        Expression b = new VariableExpression("b");
        Expression two = new NumberExpression(2);

        Expression add = new AddExpression(a, b);
        Expression expression = new SubtractExpression(add, two); // (a + b) - 2

        int result = expression.interpret(context);
        System.out.println("Result: " + result); // Output: Result: (5 + 3) - 2 = 6
    }
}
  • 优点
    • 易于改变和扩展文法: 要扩展语言,只需增加新的表达式子类即可。符合开闭原则(对扩展开放)。
    • 实现文法简单: 将文法中的每条规则直接映射到一个类,实现起来直观。
    • 易于实现简单语言: 对于小型语言,可以快速实现一个解释器。
  • 缺点
    • 类膨胀 (Class Explosion): 对于复杂的文法,会产生大量的类,使得系统变得难以管理和维护。
    • 效率较低: 递归调用和大量的对象创建可能导致性能不佳,尤其对于复杂的句子。
    • 难以维护和扩展复杂文法: 每增加一条新的规则,实际上都需要修改和扩展抽象语法树的类结构,对于复杂文法,这实际上违反了开闭原则(对修改关闭)。

六、最佳实践

  1. 使用解析器生成器对于任何有实际用途的语言,强烈不建议手动实现解释器模式。 应该使用解析器生成器 (Parser Generator) 工具,如 ANTLR (Another Tool for Language Recognition)。ANTLR可以根据你定义的文法文件(.g4),自动生成词法分析器 (Lexer) 和语法分析器 (Parser),并帮你构建好AST。你可以然后使用访问者模式 (Visitor Pattern) 来遍历和解释/转换AST,这是一种更强大、更专业的方法。
  2. 与访问者模式结合: 即使在手动实现的简单解释器中,也可以使用访问者模式来将操作(如interpret)与表达式类的结构分离。这允许你在不修改表达式类的情况下添加新的操作(如prettyPrint, typeCheck)。
  3. 考虑使用组合模式: AST本身就是组合模式的一个典型应用。NonterminalExpression是容器,TerminalExpression是叶子。
  4. 仅适用于最简单的情况: 只在语言非常简单、稳定,且确定不会扩展的情况下,才考虑使用纯解释器模式。例如,解析一个非常特定的配置文件格式。
  5. 使用Map作为上下文Context 类通常使用一个 Map 来存储变量和值的映射关系,这简单且有效。

七、在开发中的演变和应用

解释器模式的思想是现代语言处理和DSL(领域特定语言)的基础,但其实现方式已演进为更专业的工具和模式:

  1. 解析器生成器 (ANTLR, Yacc, Bison): 这些工具接管了最繁重的工作:词法分析、语法分析、AST构建。开发者只需定义文法,然后使用生成的解析器,并编写访问AST的代码(通常通过访问者模式)。这是解释器模式在工业界的真正形态。
  2. 表达式求值引擎: 许多规则引擎或流程引擎内置了表达式求值功能。例如,Spring Framework的Spring Expression Language (SpEL) 就是一个强大的表达式语言,它可以在运行时解析和求值表达式,其内部实现远比手动实现的解释器模式复杂和高效。
  3. 数据库SQL解析: 所有数据库引擎(如MySQL, PostgreSQL)都包含一个复杂的SQL解析器和查询优化器。它们绝不会使用手写的解释器模式,而是使用高度优化的自定义解析器或解析器生成器。
  4. 模板引擎: 模板引擎(如Thymeleaf, FreeMarker)会将模板文件解析成AST,然后通过解释或编译的方式来生成最终的输出内容。

八、真实开发案例(Java语言内部、知名开源框架、工具)

  1. Java 正则表达式 (java.util.regex.Pattern)

    • 从概念上讲,正则表达式的处理体现了解释器模式的思想。你提供一个正则表达式字符串(一种小型语言),Java会将其编译成一个内部表示(类似于AST),然后使用这个内部表示来匹配输入的字符串。
    • 但是,其内部实现是高度优化的,通常会将正则表达式编译成一种状态机(如NFA/DFA)而不是简单的AST递归解释,以获得极高的性能。这可以看作是解释器模式的一种“编译”演进。
  2. Spring Expression Language (SpEL)

    • Spring SpEL 是一个强大的表达式语言,用于在运行时查询和操作对象图。它的核心是一个解析器和求值器。
    • 虽然其内部实现非常复杂,但基本流程符合解释器模式的思想:将表达式字符串解析成AST,然后在指定的上下文中解释(求值)这棵AST。
    • ExpressionParser parser = new SpelExpressionParser();
    • Expression exp = parser.parseExpression('name.toUpperCase()');
    • String value = exp.getValue(context, String.class);
  3. Java 注解处理器 (Annotation Processing)

    • 在编译时,注解处理器会扫描代码中的注解(一种标记语言),并根据注解信息生成新的代码。处理注解的过程可以看作是在解释一种用于代码生成的简单语言。
  4. 简单的业务规则引擎

    • 在一些特定应用中,可能会手动实现一个简单的规则引擎来计算布尔表达式(如 condition1 AND (condition2 OR condition3))。这可能是企业应用中少数几个可能会考虑手动使用解释器模式的地方,但通常也会使用现成的库(如MVEL, JEXL)或SpEL。

九、总结

方面总结
模式类型行为型设计模式
核心意图给定一个语言,定义其文法的一种表示,并定义一个解释器来解释语言中的句子。
关键角色抽象表达式(AbstractExpression), 终结符表达式(TerminalExpression), 非终结符表达式(NonterminalExpression), 上下文(Context), 客户端(Client)
核心机制1. 构建AST: 客户端用表达式对象构建抽象语法树。
2. 递归解释: 从根节点开始,递归调用子节点的 interpret(),直到终结符。
主要优点1. 易于实现简单文法
2. 易于扩展语言(增加新类)。
3. 将文法表示与解释过程分离
主要缺点1. 类膨胀: 复杂文法导致类数量剧增,难以维护。
2. 效率低下: 递归解释效率通常不高。
3. 不适用于复杂语言
适用场景非常有限。仅适用于简单的、稳定的、需要解释执行的语言(如简单表达式、业务规则)。
最佳实践1. 使用ANTLR等专业工具,不要手动实现复杂解释器。
2. 与访问者模式结合,分离AST结构与操作。
3. 仅用于最简单场景
现代应用解析器生成器 (ANTLR) 是其在工业界的标准形态,用于生成复杂语言的解析器。表达式引擎 (SpEL) 是其思想的应用。
真实案例Java正则表达式 (概念相近),Spring SpEL (工业级表达式解释),ANTLR生成的解析器 (真正实践)。

解释器模式是一个“学院派”模式,其核心思想(定义文法、构建AST、递归解释)是编译原理和语言处理的基础,极具教育价值。然而,在实际的企业级后端开发中,你几乎永远不会需要手动实现它。它的价值在于帮助我们理解更强大的工具(如ANTLR)和语言(如SpEL)背后的原理。当面临需要解析自定义语言或表达式的问题时,正确的做法是优先评估现有解决方案(如SpEL),如果必须自定义,则毫不犹豫地选择使用ANTLR等专业工具,而不是手动构建解释器。

posted @ 2025-08-29 13:25  NeoLshu  阅读(4)  评论(0)    收藏  举报  来源