Java 23 种设计模式:从踩坑到精通 | 解释器模式 —— 自己动手写一个小语言解释器

Java 23 种设计模式:从踩坑到精通 | Interpreter ------ 自己动手写一个小语言解释器

摘要:当需要解析特定格式的语言或表达式(如数学公式、SQL 语句、规则引擎),且语法规则可能频繁变化时,手写字符串处理会让代码迅速膨胀且难以扩展。解释器模式将每个语法规则表示为一个类,通过这些类的组合构建出抽象语法树(AST),从而将"解释"这个复杂过程拆解为一棵可递归求值的对象树。本文从四则运算计算器出发,完整讲解解释器模式的原理、UML、代码实现、优缺点,并与组合模式进行对比,结合 Java 正则表达式、Spring SpEL 等框架应用,帮你掌握"语言即对象树"的设计思想。
🗺️ 本文阅读地图(3 分钟速览)

  • 为什么 switch 解析表达式一定会炸?
  • 终结符 vs 非终结符:语法树的两类节点
  • 手写四则运算解释器(支持变量和括号)
  • 扩展:乘法和括号如何零侵入添加?
  • JDK 正则 / Spring SpEL 如何体现解释器模式
  • 面试必问:解释器 vs 组合模式,有什么区别?
    📖 《Java 23 种设计模式:从踩坑到精通》

开篇:系列介绍与目录 | 上一篇:命令模式 | 当前:解释器模式 | 下一篇:迭代器模式

🔗 返回系列总目录


文章目录

  • [Java 23 种设计模式:从踩坑到精通 | Interpreter ------ 自己动手写一个小语言解释器](#Java 23 种设计模式:从踩坑到精通 | Interpreter —— 自己动手写一个小语言解释器)
    • [1. 从"计算器"的需求说起](#1. 从“计算器”的需求说起)
      • [1.1 你的场景该不该用解释器?](#1.1 你的场景该不该用解释器?)
    • [2. 模式定义与 UML 结构](#2. 模式定义与 UML 结构)
    • [3. 代码实现:四则运算表达式解析器](#3. 代码实现:四则运算表达式解析器)
      • [3.1 上下文:变量表](#3.1 上下文:变量表)
      • [3.2 抽象表达式](#3.2 抽象表达式)
      • [3.3 终结符表达式:变量](#3.3 终结符表达式:变量)
      • [3.4 非终结符表达式:加法、减法](#3.4 非终结符表达式:加法、减法)
      • [3.5 客户端:构建语法树并求值](#3.5 客户端:构建语法树并求值)
    • [4. 扩展:加入乘法和括号](#4. 扩展:加入乘法和括号)
    • [5. 优缺点一览](#5. 优缺点一览)
    • [6. 解释器模式 vs 组合模式](#6. 解释器模式 vs 组合模式)
    • [7. 框架与实践中的应用](#7. 框架与实践中的应用)
      • [7.1 Java 正则表达式:`java.util.regex.Pattern`](#7.1 Java 正则表达式:java.util.regex.Pattern)
      • [7.2 Spring Expression Language (SpEL)](#7.2 Spring Expression Language (SpEL))
      • [7.3 规则引擎(Drools、QLExpress)](#7.3 规则引擎(Drools、QLExpress))
    • [8. 面试必问 + 面试官追问连环炮](#8. 面试必问 + 面试官追问连环炮)
    • [9. 六大设计原则在解释器模式中的体现](#9. 六大设计原则在解释器模式中的体现)
    • [🧭 《Java 23 种设计模式:从踩坑到精通》快速导航](#🧭 《Java 23 种设计模式:从踩坑到精通》快速导航)

1. 从"计算器"的需求说起

想象你要实现一个简单的表达式计算器,能够解析并计算如 a + b - c 这样的表达式。如果采用最直白的字符串分割和 switch 判断:

java 复制代码
if (expression.contains("+")) {
    // 处理加法
} else if (expression.contains("-")) {
    // 处理减法
}

当扩展到乘除、括号、函数调用时,代码会迅速变成一坨难以维护的"意大利面条"。更麻烦的是,如果表达式语法发生变化(如加入幂运算),原有解析逻辑很可能需要重写。

解释器模式(Interpreter Pattern)正是为这类"小语言"解释场景而生:它将每个语法规则表示为一个类,通过这些类的组合构建出抽象语法树,再由客户端输入上下文递归求值。

1.1 你的场景该不该用解释器?

判断标准 是 → 用解释器 否 → 用其他方式
需要自定义简单的语法规则或表达式
语法规则可能频繁变化,需要灵活扩展
语法结构非常复杂,如完整编程语言 使用 ANTLR 等专业解析器
只需要简单的字符串处理或正则 直接用 StringPattern

2. 模式定义与 UML 结构

解释器模式 给定一种语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。它属于 行为型设计模式

图文解析

解释器模式的核心角色:

  • 抽象表达式(Expression :声明一个抽象的解释操作 interpret(context),是所有终结符和非终结符表达式的公共父类。
  • 终结符表达式(TerminalExpression :实现与文法中的终结符相关的解释操作,通常是变量或常量,例如 a5
  • 非终结符表达式(NonTerminalExpression :组合多个表达式,表示语法规则(如加法、减法),内部持有左右两个 Expression 引用,递归调用子表达式的 interpret() 完成计算。

核心机制 :解释器模式把"解释语言"这件事拆分为一棵由对象组成的语法树,每个节点自己负责自己的求值逻辑。客户端只需构建语法树并调用顶层节点的 interpret(),计算自动递归完成。


3. 代码实现:四则运算表达式解析器

3.1 上下文:变量表

java 复制代码
public 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);
    }
}

💬 白话 :上下文就是"备忘录",记录表达式里每个变量代表什么值,比如 a = 10

3.2 抽象表达式

java 复制代码
public interface Expression {
    int interpret(Context context);
}

💬 白话:所有表达式节点都必须能"求值"------给定上下文,返回计算结果。

3.3 终结符表达式:变量

java 复制代码
public class Variable implements Expression {
    private String name;

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

    @Override
    public int interpret(Context context) {
        return context.getVariable(name);
    }
}

💬 白话:变量节点没有子节点,它直接从上下文中取出对应的数值。

3.4 非终结符表达式:加法、减法

java 复制代码
public class Add implements Expression {
    private Expression left;
    private Expression right;

    public Add(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 Subtract implements Expression {
    private Expression left;
    private Expression right;

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

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

💬 白话:加法/减法节点持有一个左子表达式和一个右子表达式,求值时先递归求出左右两边的值,再执行加减。

3.5 客户端:构建语法树并求值

java 复制代码
public class Calculator {
    public static void main(String[] args) {
        Context ctx = new Context();
        ctx.setVariable("a", 10);
        ctx.setVariable("b", 5);
        ctx.setVariable("c", 2);

        // 构建语法树:a + b - c
        Expression expression = new Subtract(
                new Add(new Variable("a"), new Variable("b")),
                new Variable("c")
        );

        int result = expression.interpret(ctx);
        System.out.println("a + b - c = " + result);  // 13
    }
}

表达式 a + b - c 被构建为一棵语法树:

复制代码
       Subtract
       /      \
     Add       c
    /   \
   a     b

客户端只需创建语法树并调用 interpret(),递归求值自动完成。


4. 扩展:加入乘法和括号

乘法与加法结构相同,括号则需要构建一棵子树。新增运算符无需修改已有类,只需新增一个 NonTerminalExpression 子类。

java 复制代码
public class Multiply implements Expression {
    private Expression left, right;
    public Multiply(Expression left, Expression right) {
        this.left = left; this.right = right;
    }
    public int interpret(Context context) {
        return left.interpret(context) * right.interpret(context);
    }
}

💬 白话:想增加新运算?加一个新类就好,不用动原来的加法、减法代码。

构建 (a + b) * c

java 复制代码
Expression expr = new Multiply(
        new Add(new Variable("a"), new Variable("b")),
        new Variable("c")
);
System.out.println(expr.interpret(ctx));  // (10+5)*2 = 30

每加入一个新运算符,只需新增一个 NonTerminalExpression 子类,完全符合开闭原则。


5. 优缺点一览

优点 缺点
易于扩展:新增语法规则只需加新表达式类,遵循开闭原则 类数量膨胀:每个语法规则一个类,复杂语法会导致类爆炸
易于实现:语法规则与代码类一一对应,直观 执行效率低:递归调用大量对象,解释型求值比原生编译器慢
可组合:通过组合形成复杂的语法树 调试困难:语法树深时调用栈复杂,排查问题成本高
适合简单语法:特别适合正则、表达式等小语言 不适合复杂语法:更推荐使用解析器生成工具(如 ANTLR)

6. 解释器模式 vs 组合模式

这两种模式都使用树形结构,但目的完全不同:

对比维度 解释器模式 组合模式
目的 解释语言句子,求值 统一处理部分与整体
节点行为 interpret() 计算,不同类型节点行为差异大 operation() 统一,叶子与容器行为相似
典型应用 表达式求值、正则、SpEL 文件系统、组织架构

💡 解释器模式的树是"计算树",每个节点执行不同的计算逻辑;组合模式的树是"结构树",强调客户一致访问。


7. 框架与实践中的应用

7.1 Java 正则表达式:java.util.regex.Pattern

Java 正则引擎内部将正则表达式字符串解析为一棵语法树,每个元字符(如 *+?)对应不同的节点类型,这个过程就是解释器模式的应用。

7.2 Spring Expression Language (SpEL)

Spring 的 SpEL 可以解析并求值复杂的表达式,例如:

java 复制代码
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello ' + world");
String value = exp.getValue(context, String.class);

SpEL 内部将表达式字符串解析为 AST,然后递归求值,这正是解释器模式的体现。

7.3 规则引擎(Drools、QLExpress)

规则引擎将业务规则以 DSL 形式编写,内部通过解析器构建 AST,然后注入事实数据执行规则。其核心就是解释器模式。


8. 面试必问 + 面试官追问连环炮

基础必问

  • 解释器模式由哪些角色组成? → 抽象表达式、终结符表达式、非终结符表达式、上下文。
  • 解释器模式如何遵循开闭原则? → 新增语法规则只需新增表达式类,无需修改原有表达式类。
  • 解释器模式的缺点? → 类膨胀、执行效率低,复杂语法不推荐。

面试官追问

  • "解释器模式和组合模式有什么区别?"
    👉 组合关注"结构统一",解释器关注"递归求值"。解释器的语法树往往建立在组合模式之上。
  • "为什么复杂语法不推荐解释器模式?"
    👉 因为每个语法规则都要对应一个类,类数量会爆炸;且递归求值性能远低于直接编译。此时应用 ANTLR 等专业工具。
  • "Spring SpEL 是解释器模式吗?"
    👉 是。它把表达式字符串解析为 AST,然后递归求值,是解释器模式在企业框架中的典型应用。

🎉 恭喜 :如果你能立刻画出 a + b - c 的语法树,并说出终结符与非终结符的区别,你已经掌握了行为型模式中"语言即对象树"的设计精髓。


9. 六大设计原则在解释器模式中的体现

设计原则 在解释器模式中的体现
单一职责原则(SRP) 每个表达式类只负责一种语法规则的解释
开闭原则(OCP) 新增运算符只需加新类,无需修改现有代码
里氏替换原则(LSP) 所有表达式都可替换抽象 Expression
依赖倒置原则(DIP) 客户端依赖抽象 Expression 构建语法树
接口隔离原则(ISP) Expression 接口仅定义 interpret(),精简
迪米特法则(LoD) 客户端只与顶级表达式交互,不知内部树结构

🧭 《Java 23 种设计模式:从踩坑到精通》快速导航

🔔 关注《Java 23 种设计模式:从踩坑到精通》,用 25 篇文章彻底吃透设计模式。

📦 福利预告 :全系列代码及 UML 源码将在完结时统一打包开放,点击「关注」「收藏」第一时间获取。

🚀 下一篇:迭代器模式:遍历集合,为什么不直接暴露内部结构?🚧 即将发布,敬请关注!
📌 除了设计模式,我也在深挖智能物流实战 (WMS、托盘调度、机器学习落地)。欢迎点击头像,看看专栏 《出版社物流WMS智能调度实战》。技术相通,思路可鉴。