走进 Gang of Four 设计模式:解释器模式

走进 Gang of Four 设计模式:解释器模式

说明:不仅告诉你"怎么用",更告诉你"为什么这样设计"

前置知识:Java 面向对象基础(接口、多态、递归思想)、组合模式基本概念


📊 设计模式分类总览

GoF 23 种设计模式可按两个维度交叉分类:类/对象 (处理方式) × 创建型/结构型/行为型(目的)。

维度 创建型(Creational) 结构型(Structural) 行为型(Behavioral)
类(Class) (通过继承复用) Factory Method 工厂方法 Adapter(类适配器) Interpreter(解释器) Template Method(模板方法)
对象(Object) (通过组合/聚合复用) Abstract Factory(抽象工厂) Builder(建造者) Prototype(原型) Singleton(单例) Adapter(对象适配器) Bridge(桥接) Composite(组合) Decorator(装饰) Facade(外观) Flyweight(享元) Proxy(代理) Chain of Resp.(责任链) Command(命令) Iterator(迭代器) Mediator(中介者) Memento(备忘录) Observer(观察者) State(状态) Strategy(策略) Visitor(访问者)

📑 目录

  1. [模式概述 · 15 问深度分析](#模式概述 · 15 问深度分析)
  2. 框架源码实战分析
    • [JDK 正则表达式 Pattern.Node](#JDK 正则表达式 Pattern.Node)
    • [Spring SpEL 表达式引擎](#Spring SpEL 表达式引擎)
    • [MyBatis 动态 SQL SqlNode](#MyBatis 动态 SQL SqlNode)
  3. [深度追问:Why / How / Trade-off / Evolution / Modern Practice](#深度追问:Why / How / Trade-off / Evolution / Modern Practice)
  4. 总结
  5. 参考文献

1. 模式概述 · 15 问深度分析

Q1: 为什么需要这个模式?它解决了什么问题?

解释器模式是行为型设计模式中相对冷门、但特定场景下威力巨大的一种模式。它本质上是为一种特定语言构建编译器/解释器的面向对象实现

在软件开发中,有时我们会遇到一类重复发生的问题,这些问题可以表达为一个特定的规则、表达式或命令序列。例如:自研的规则引擎、复杂的财务公式计算、自定义的 SQL 解析器。

它解决的核心问题 :提供了一种将领域内的特定语法规则映射为面向对象类结构的方法。通过这种方式,我们可以通过组合不同的类对象,来动态地表达和执行无穷无尽的语法逻辑,而无需修改底层解析代码。

Q2: 如果不用这个模式,会有什么缺陷?

如果不使用解释器模式,通常的做法是编写一个巨大的解析函数(往往充斥着 if-elseswitch-case 以及正则匹配):

java 复制代码
// ❌ 大量 if-else 硬编码,新增运算符需要改源码
public int eval(String expr, Map<String, Integer> ctx) {
    if (expr.contains("+")) {
        String[] parts = expr.split("\\+");
        return eval(parts[0], ctx) + eval(parts[1], ctx);
    } else if (expr.contains("-")) {
        // ...
    }
    // 新增乘法运算符需要在这里加 else-if
}
  • 违反开闭原则:一旦需要增加一种新的语法规则或运算符,必须深入到那个庞大的解析函数内部修改代码。
  • 难以扩展与维护:随着语法越来越复杂,条件分支会呈指数级嵌套。
  • 无法动态组合:硬编码的逻辑在编译期就确定了,无法在运行时由用户自由组合出新的复杂表达式。

Q3: 核心思想是什么?一句话如何概括?

  • 核心思想 :将语言的语法规则定义为类层次结构 ,通过将这些类的实例组合成一棵抽象语法树(AST),并让这棵树的每个节点自行负责其对应语法规则的评估与执行。
  • 一句话概括用"对象树"来表达语法,用"树遍历"来实现执行。

Q4: 包含哪些角色?每个角色的职责是什么?

角色 职责
AbstractExpression(抽象表达式) 定义一个抽象的解释操作接口(通常是 interpret(Context ctx))。
TerminalExpression(终结符表达式) 语法树的叶子节点,不再包含其他表达式(如变量、数字、常量)。
NonterminalExpression(非终结符表达式) 语法树的分支节点 ,包含一个或多个子表达式(如加、减法),通过递归调用子表达式的 interpret 组合结果。
Context(上下文) 包含解释器之外的全局信息,存储变量名与值的映射关系。

Q5: 它们之间如何协作?调用流程是怎样的?

构建期(编译期): 客户端或解析器接收字符串表达式(如 a + b - 3),根据语法规则将其解析并组装成一棵抽象语法树。根节点通常是非终结符,叶子节点是终结符。

执行期(解释期):

  1. 创建 Context(填充变量 a=10, b=5)。
  2. 调用 AST 根节点的 interpret(Context) 方法。
  3. 根节点(非终结符)递归调用子节点的 interpret 方法。
  4. 叶子节点(终结符)从 Context 中获取具体数值并返回。
  5. 结果一路向上回溯,根节点计算最终结果。

Q6: 为什么要这样设计?每个角色存在的意义是什么?

  • AbstractExpression:提供统一契约,使得解释器可以使用多态遍历整棵树。
  • TerminalExpression 和 NonterminalExpression 的分离:利用组合模式,终结符负责算值终点,非终结符负责流程控制与路由。
  • Context :保持表达式对象的无状态性,同一个语法树可以在不同上下文数据下重复使用。

Q7: 为什么使用接口、抽象类、组合,而不是其他方式?

  • 使用接口/抽象类 :为了利用多态 。语法树的节点类型千差万别,只有通过抽象出统一的 Expression 接口,非终结符节点才能以统一的方式去持有和调用子节点。
  • 使用组合(Composite) :因为语法天然具有嵌套和层次结构 。例如 (a + b) * c,"乘法"非终结符组合了"加法"和"变量"。组合模式是表达这种"部分-整体"层级结构最完美的手段。

Q8: 这种设计符合哪些面向对象原则?

  • 开闭原则(OCP) :增加新运算符只需新增一个类实现 Expression 接口,不需修改现有的类。
  • 单一职责原则(SRP):每个表达式类只负责自己那一部分语法的解释逻辑。
  • 依赖倒置原则(DIP) :非终结符节点持有的是抽象的 Expression 接口,而不是具体的子类。

Q9: 它有哪些优点和局限性?

优点:

  • 易于扩展文法:增加新规则只需加类。
  • 实现文法较容易:每个类小而直观,易于编写和单元测试。

局限性与额外成本:

  • 类爆炸:如果文法极其复杂,每一个小语法都需要一个类。
  • 执行效率低下:大量递归调用和对象组合,高频时性能差。
  • 未解决解析问题:模式本身不包含如何把字符串变成语法树的代码,仍需自己写 Lexer 和 Parser。

Q10: 最适合/不该用的场景

最适合:

  • 领域特定语言(DSL):如简单的自定义工作流规则引擎、财务公式配置。
  • 数据验证器 :需要动态组合各种条件(如 Age > 18 AND Region == 'CN')。
  • 简单的命令/机器人指令解析

不应该使用:

  • 复杂的、通用的高级语言:会导致类爆炸且效率低下,应使用 ANTLR、JavaCC 等专业工具链。
  • 对性能要求极高的场景

Q11: 与其他模式的区别

与组合模式(Composite)的联系:

  • 解释器模式的语法树结构本质上就是组合模式。区别在于意图:组合模式专注于数据结构管理,解释器模式专注于利用这棵树去解释执行。

与策略模式(Strategy)的区别:

  • 策略模式是互替的(选一个执行);解释器模式中的各类表达式是组合在一起共同工作的。

与访问者模式(Visitor)的配合:

  • 如果执行逻辑经常变动,通常引入访问者模式将具体的解释执行逻辑从 Expression 类中剥离到 Visitor 中,实现语法结构与求值逻辑的进一步解耦。

Q12: 框架中有哪些典型应用?

  • JDKjava.util.regex.Pattern,正则字符串被解析为内部 Node 节点链/树,通过匹配引擎解释执行。
  • Spring FrameworkSpEL(Spring Expression Language) ,形如 #{@bean.property ?: 'default'} 的表达式被解析为 SpelNode 树解释求值。
  • MyBatis动态 SQL 解析<if><where> 等标签被解析为 SqlNode 树,在运行时动态组装 SQL。

Q13: 最小可运行版本(数学加减法解释器)

java 复制代码
import java.util.HashMap;
import java.util.Map;

// 1. AbstractExpression
interface Expression {
    int interpret(Context context);
}

// Context
class Context {
    private final Map<String, Integer> valueMap = new HashMap<>();
    public void assign(String variable, int value) { valueMap.put(variable, value); }
    public int getValue(String variable) { return valueMap.getOrDefault(variable, 0); }
}

// 2. TerminalExpression:变量
class VariableExpression implements Expression {
    private final String name;
    public VariableExpression(String name) { this.name = name; }
    @Override
    public int interpret(Context context) { return context.getValue(name); }
}

// 2. TerminalExpression:常数
class NumberExpression implements Expression {
    private final int number;
    public NumberExpression(int number) { this.number = number; }
    @Override
    public int interpret(Context context) { return this.number; }
}

// 3. NonterminalExpression:加法
class AddExpression implements Expression {
    private final Expression left, 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);
    }
}

// 3. NonterminalExpression:减法
class SubExpression implements Expression {
    private final Expression left, right;
    public SubExpression(Expression left, Expression right) { this.left = left; this.right = right; }
    @Override
    public int interpret(Context context) {
        return left.interpret(context) - right.interpret(context);
    }
}

// 客户端
public class InterpreterDemo {
    public static void main(String[] args) {
        // AST: (a + b) - 3
        Expression ast = new SubExpression(
            new AddExpression(new VariableExpression("a"), new VariableExpression("b")),
            new NumberExpression(3)
        );
        Context context = new Context();
        context.assign("a", 10);
        context.assign("b", 5);
        System.out.println("Result: " + ast.interpret(context)); // 12
    }
}

Q14: 识别重构信号

  1. 配置驱动的复杂嵌套逻辑 :数据库里存大量规则字符串(如 if A and (B or C)),代码里有复杂的解析机制。
  2. 频繁变化的计算公式:业务人员经常要求修改或组合计算公式(如优惠券叠加规则)。
  3. 大面积的条件分支解析:入参是复杂的 JSON 或 String,方法内部通过几十层循环或 if 块逐句判断。

Q15: 现代改进(Java 16+ sealed + record + 模式匹配)

java 复制代码
import java.util.Map;

@FunctionalInterface
sealed interface ModernExpression permits Variable, Constant, Op {
    int interpret(Map<String, Integer> ctx);
}

record Variable(String name) implements ModernExpression {
    public int interpret(Map<String, Integer> ctx) { return ctx.getOrDefault(name, 0); }
}

record Constant(int value) implements ModernExpression {
    public int interpret(Map<String, Integer> ctx) { return value; }
}

record Op(ModernExpression left, ModernExpression right, char symbol) implements ModernExpression {
    public int interpret(Map<String, Integer> ctx) {
        return switch (symbol) {
            case '+' -> left.interpret(ctx) + right.interpret(ctx);
            case '-' -> left.interpret(ctx) - right.interpret(ctx);
            default -> throw new IllegalArgumentException("Unknown op: " + symbol);
        };
    }
}

2. 框架源码实战分析

2.1 JDK 正则表达式 java.util.regex.Pattern.Node

JDK 的正则引擎将正则字符串解析为由 Node 节点组成的链表/树状结构,运行时通过从头节点开始"解释执行"这些节点来完成匹配。

角色映射
角色 JDK 正则对应
AbstractExpression Pattern$Node(内部静态抽象类)
TerminalExpression Slice(匹配固定字符串)、Ctype(匹配预定义字符集 \d
NonterminalExpression Start(起始匹配)、Loop(处理 */+ 闭包)、Branch(处理 `
Context Matcher(保存当前匹配的输入字符串、指针位置、捕获组状态)
核心源码
java 复制代码
// Pattern 内部静态类,充当 AbstractExpression
static class Node extends Object {
    Node next; // 链表指针,用于组合下一个表达式节点

    Node() { next = Pattern.accept; }

    // 核心解释执行方法
    boolean match(Matcher matcher, int i, CharSequence seq) {
        return i == matcher.to && next == null;
    }
}

// 终结符节点:Ctype(字符类型匹配,如 \d)
static final class Ctype extends Node {
    final int ctype;
    Ctype(int type) { this.ctype = type; }

    @Override
    boolean match(Matcher matcher, int i, CharSequence seq) {
        if (i < matcher.to && isType(seq.codePointAt(i), ctype)) {
            return next.match(matcher, i + countChars(seq, i, 1), seq);
        }
        return false; // 不匹配,回溯
    }
}

// 非终结符节点:Curly(处理 {m,n} 重复量词)
static final class Curly extends Node {
    Node atom; // 组合模式:被量词修饰的子表达式
    int type, cmin, cmax;

    Curly(Node node, int min, int max, int type) {
        this.atom = node;
        this.cmin = min;
        this.cmax = max;
        this.type = type;
    }

    @Override
    boolean match(Matcher matcher, int i, CharSequence seq) {
        // 递归调用 atom.match(),控制指针回溯
        return java.util.regex.Pattern.Loop.match(matcher, i, seq, atom, cmin, cmax, next);
    }
}

深度分析 :JDK 正则没有采用标准的二叉树 AST,而是由于正则的线性特性将其拉平为链表+局部循环分支树 (通过 next 指针和 atom 组合)。通过 Node.match() 的递归调用与回溯,完美实现了正则语法的解释执行。

2.2 Spring SpEL (Spring Expression Language)

SpEL 允许在运行时查询和操作对象图,是解释器模式的高级落地。

角色映射
角色 Spring SpEL 对应
AbstractExpression SpelNode 接口 / SpelNodeImpl 抽象类
TerminalExpression Literal(字面量)、VariableReference(变量引用 #user
NonterminalExpression OpPlus(加法)、OpMatches(正则匹配)、Ternary(三元)
Context EvaluationContext(存储变量、函数、Root 对象)
核心源码
java 复制代码
// SpelNode 接口
public interface SpelNode {
    TypedValue getValue(ExpressionState state) throws EvaluationException;
    int getChildCount();
    SpelNode getChild(int index);
    boolean isCompilable();
    void generateCode(MethodVisitor mv, CodeFlow cf);
}

// 非终结符:OpPlus(加法)
public class OpPlus extends Operator {
    public OpPlus(int startPos, int endPos, SpelNodeImpl... operands) {
        super("+", startPos, endPos, operands);
    }

    @Override
    public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
        Object leftOperand = getLeftOperand().getValueInternal(state).getValue();
        Object rightOperand = getRightOperand().getValueInternal(state).getValue();

        if (leftOperand instanceof Number && rightOperand instanceof Number) {
            return new TypedValue(NumberUtils.add((Number) leftOperand, (Number) rightOperand));
        }
        if (leftOperand instanceof String || rightOperand instanceof String) {
            return new TypedValue(String.valueOf(leftOperand) + rightOperand);
        }
    }
}

// 终结符:VariableReference(变量引用)
public class VariableReference extends SpelNodeImpl {
    private final String name;

    @Override
    public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
        return state.lookupVariable(this.name); // 直接从 Context 中获取变量值
    }
}

深度分析 :SpEL 是解释器模式的教科书级实现。Spring 自 4.1 之后引入了 generateCode 机制,当某个表达式被高频执行时,会利用 ASM 框架直接将 SpelNode 树编译为原生 Java 字节码,从"解释器模式"动态优化为"直接指令执行"。

2.3 MyBatis 动态 SQL SqlNode

MyBatis 的强大之处在于通过 XML 标签动态拼接 SQL。在启动加载 XML 时,标签被解析为一棵 SqlNode 树,在运行时通过传入的参数去解释执行这棵树。

角色映射
角色 MyBatis 对应
AbstractExpression SqlNode 接口
TerminalExpression StaticTextSqlNode(纯 SQL 文本)
NonterminalExpression IfSqlNode<if>)、WhereSqlNode<where>)、MixedSqlNode(组合节点)
Context DynamicContext(持有参数 Map 和 StringBuilder 结果缓冲)
核心源码
java 复制代码
public interface SqlNode {
    boolean apply(DynamicContext context);
}

// 非终结符:MixedSqlNode(容器节点)
public class MixedSqlNode implements SqlNode {
    private final List<SqlNode> contents; // 组合多个子表达式节点

    @Override
    public boolean apply(DynamicContext context) {
        for (SqlNode sqlNode : contents) {
            sqlNode.apply(context); // 递归调用子节点
        }
        return true;
    }
}

// 非终结符:IfSqlNode(条件判断)
public class IfSqlNode implements SqlNode {
    private final ExpressionEvaluator evaluator;
    private final String test;              // "username != null"
    private final SqlNode contents;          // 标签内部的 SQL 片段

    @Override
    public boolean apply(DynamicContext context) {
        if (evaluator.evaluateBoolean(test, context.getBindings())) {
            contents.apply(context); // 条件满足,拼接 SQL
            return true;
        }
        return false; // 条件不满足,被过滤掉
    }
}

// 终结符:StaticTextSqlNode(纯文本)
public class StaticTextSqlNode implements SqlNode {
    private final String text;

    @Override
    public boolean apply(DynamicContext context) {
        context.appendSql(text); // 直接追加到 StringBuilder 中
        return true;
    }
}

深度分析 :MyBatis 将动态 SQL 拼接变成了语法树的深度优先遍历(DFS) 。调用 Mapper 时,找根 SqlNode → 创建 DynamicContext → 调用 rootSqlNode.apply(context) → 树遍历完成后从 context.getSql() 取出最终拼好的 SQL。

三者对比总结:

框架 AbstractExpression TerminalExpression NonterminalExpression Context 核心解释动作
JDK Pattern Pattern$Node Slice, Ctype Curly, Branch Matcher match(matcher, idx, seq)
Spring SpEL SpelNode Literal, VariableReference OpPlus, Ternary ExpressionState getValue(state)
MyBatis SqlNode StaticTextSqlNode IfSqlNode, MixedSqlNode DynamicContext apply(context)

3. 深度追问

3.1 Why(为什么):解决哪个根本矛盾?

解释器模式解决的根本矛盾是:"业务规则的无限多变性"与"硬编码系统编译期确定性"之间的矛盾。

在常规开发中,逻辑是写死在类和方法中的。一旦规则改变,就必须修改代码、重新编译、部署。然而在某些特定领域,规则是由业务人员在运行时高频、随机组合 出来的。解释器模式通过引入一门 DSL,将变化的业务逻辑从不变的执行引擎中彻底剥离。系统不再硬编码业务逻辑,而是硬编码一个"解释器引擎"。其本质是用数据的组合(语法树)代替了代码的逻辑分支(if-else)

3.2 How(怎么做):通过哪些对象关系和交互机制?

解释器模式的核心机制是将语法的"递归定义"直接映射为对象的"递归组合"

  • 对象关系:利用组合关系,非终结符节点内部持有其他抽象表达式的引用。这种"自己包含自己"的树状结构,完美对应了语法的嵌套。
  • 交互机制 :调用根节点的 interpret(Context) 触发整棵树的**深度优先搜索(DFS)**递归遍历。表达式节点是无状态的,所有的状态、变量映射都封装在 Context 中,确保同一棵语法树可以在不同数据集下复用。

3.3 Trade-off(代价):牺牲了什么换取什么?

换取(收益) 牺牲(代价)
极致的动态扩展性:拼装不同语法树就能组合出无限业务逻辑 性能下降:大量递归调用导致方法栈极深
极高的维护边界:每个语法节点职责单一,易测试 调试困难:Call Stack 极深,难以追踪
运行时动态组合 类爆炸:复杂文法需要大量类
前端解析成本:需额外编写词法/语法解析器

3.4 Evolution(演化):如何逐步演化而来?

  1. 硬编码 :一个巨大的 eval() 方法嵌套无数 switch-case
  2. 策略模式组合:把加法、减法抽成策略类,但无法解决嵌套问题。
  3. 标准解释器模式:引入组合模式,形成标准 AST。
  4. 访问者模式变体:将语法树结构与求值行为彻底分离。
  5. 虚拟机/字节码变体:将树压平为线性指令集(Bytecode),由高效率循环执行(如 Lua 虚拟机、Spring SpEL 字节码生成)。

3.5 Modern Practice(现代实践):是否被取代?

大部分被专业工具取代: 现在通常使用 ANTLR、JavaCC 等工具,或 Drools、Groovy、Aviator 等开箱即用引擎。

但在现代技术栈中融合新生:

  • 函数式编程Expression 接口变成 Function<Context, Object>,非终结符变成高阶函数,直接消灭类爆炸。
  • JIT 编译变体:Spring SpEL 在 4.1+ 引入运行时编译,高频表达式直接编译为原生字节码,性能提升百倍。
  • 微服务 + 配置中心:规则文本从 Nacos/Apollo 动态下发,在服务内部通过解释器模式构建 AST,结合 IoC 无缝调用 Spring 容器中的基础服务。

结论:不需要刻意模仿 90 年代繁琐的类结构,但"用对象树/函数组合来抽象、分离多变文法"的核心思想,依然是指导现代动态规则引擎设计的不二法门。


4. 总结

核心要点

  • 解释器模式将语言的文法映射为类结构,核心是组合模式 + 递归调用
  • 适用范���有限:仅适用于文法简单的场景
  • 三个工业级应用:JDK Pattern(正则链表 AST)、Spring SpEL(字节码编译 JIT)、MyBatis SqlNode(SQL 树状过滤)
  • 与组合模式的关系:语法树结构本质上就是组合模式
  • 现代演进:Java 16+ sealed + record + 模式匹配、函数式 Combinator、JIT 编译

一句话

解释器模式就是"让程序读懂自定义语言"------定义一套语法规则,把"规则"映射为"对象树",让 interpret() 沿着树递归执行。


5. 参考文献

1 GAMMA E, HELM R, JOHNSON R, et al. Design Patterns: Elements of Reusable Object-Oriented SoftwareM. Boston: Addison-Wesley, 1994.

2 Oracle. Java Pattern Source CodeEB/OL. https://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html, 2024.

3 SPRING. Spring Expression Language (SpEL)EB/OL. https://docs.spring.io/spring-framework/reference/core/expressions.html, 2024.

4 MyBatis. MyBatis 3 Dynamic SQLEB/OL. https://mybatis.org/mybatis-3/dynamic-sql.html, 2024.

相关推荐
智码看视界1 小时前
Tomcat架构深度拆解:Connector和Container到底怎么配合的?
java·servlet·架构·tomcat·web服务器
浪客川1 小时前
idea 技巧 region 的使用
java·ide·intellij-idea
CHANG_THE_WORLD1 小时前
逐层拆解:C++ 虚函数从对象内存到手工调用的完整过程
java·开发语言·c++
我登哥MVP1 小时前
走进 Gang of Four 设计模式:过滤器模式
java·设计模式·过滤器模式
吃饱了得干活2 小时前
实时弹幕应该怎么搞?一篇带你从入门到生产级实战
java·后端
tianyuanwo2 小时前
深入掌握 java -jar 命令:从基础到 Jenkins Agent 实战
java·jenkins·jar
Full Stack Developme2 小时前
MyBatis-Plus(MP)AST 抽象语法树 设计原理
java·tomcat·mybatis
宠友信息2 小时前
消息撤回与已读状态如何在即时通讯源码中统一管理
java·spring boot·websocket·mysql·uni-app
小小放舟、2 小时前
Windows 本地安装 Elasticsearch 8.10.0 与 IK 分词器(2026)
java·大数据·windows·spring boot·elasticsearch·搜索引擎·全文检索