首页
/ CS-Notes 设计模式精讲:解释器模式(Interpreter)的原理、类图与规则引擎示例

CS-Notes 设计模式精讲:解释器模式(Interpreter)的原理、类图与规则引擎示例

2026-09-06 19:14:41作者:胡唯隽

解释器模式(Interpreter Pattern)是 CS-Notes 仓库 [设计模式 - 解释器.md](https://gitcode.com/GitHub_Trending/cs/CS-Notes/blob/b70121d377cb6005eb65f12b098cd5decd905669/notes/设计模式 - 解释器.md?utm_source=gitcode_repo_files) 讲解的一种行为型设计模式:它为一门语言定义文法的对象化表示,并提供一个解释器来解释该文法描述的语言句子。本文以文档中"规则检验器(and / or 规则)"完整实现为主线,逐类拆解解析树的构建与递归求值过程,并梳理模式结构与 JDK 中的应用,帮助你在面试与日常设计中准确理解、复现和评价该模式。

一、模式意图:什么是解释器模式

GoF 对解释器模式的经典定义是:给定一门语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。CS-Notes 文档将 Intent 概括为:

为语言创建解释器,通常由语言的语法和语法分析来定义。

这句话揭示了模式的本质工作流:

  1. 先用类对象把一条文法规则(如 D And (A Or (B C)))静态地表示为一棵解析树 / 抽象语法树(AST)
  2. 再通过每个节点上统一的 interpret(...) 方法,对这棵树做递归求值,从而判断一段输入(文本)是否符合文法、能否得到解释结果。

解释器模式适合解决这样一类问题:某类问题重复出现、频率足够高,并且其解法可以抽象成一种简单语言的文法。典型的落地场景包括:

  • 布尔规则 / 权限表达式校验(本文示例即属此类);
  • SQL、正则、模板字符串等小规模专用语言的解析与执行;
  • 数学表达式求值、配置 DSL(领域专用语言)解释等。

仓库将解释器归入"行为型"模式(见 [设计模式 - 目录.md](https://gitcode.com/GitHub_Trending/cs/CS-Notes/blob/b70121d377cb6005eb65f12b098cd5decd905669/notes/设计模式 - 目录.md?utm_source=gitcode_repo_files) 第三节),因为它关注的是文法对象之间如何协作完成"解释"这一职责分配:每个语法节点各自承担对自身子树的解释义务,再通过递归把结果向上汇总。

二、结构剖析:四个核心角色与 UML 类图

CS-Notes 文档为解释器模式绘制的 UML 类图如下:

解释器模式的 UML 类图:AbstractExpression 派生出 TerminalExpression 与 NonTerminalExpression,Client 依赖 Context 与 AbstractExpression

结合类图与本仓库示例代码,模式包含以下角色:

角色 在类图中的职责 在本文示例中的对应
AbstractExpression(抽象表达式) 声明抽象的 interpret(Context) 接口,是所有语法节点的公共父类 抽象类 Expression,声明 interpret(String)
TerminalExpression(终结符表达式) 表示文法中的终结符(字面量、变量),每个终结符都需要一个实例;实现"句子与终结符匹配"的解释逻辑 TerminalExpression,封装 ABCD 等字面量
NonTerminalExpression(非终结符表达式) 表示文法中的组合规则(运算符、复合结构),内部持有其他子表达式并递归调用其 interpret() AndExpressionOrExpression(由文档实现可推断,它们正是对非终结符 And / Or 的角色落地)
Context(上下文) 保存解释器之外的一些全局信息,供解释过程读取 示例中直接简化为 String(文档明确说明"这里的 Context 指的是 String")
Client(客户端) 构建(或由语法分析器生成)抽象语法树,并调用根节点的 interpret() 触发解释 下述 Client.buildInterpreterTree()

值得注意的关键结构事实是:每个终结符需要对应一个 TerminalExpression 对象,而每个非终结符(And / Or 规则)对应一个持有子表达式的组合节点——整棵解析树是自相似的递归组合结构。因此从结构上看,解释器模式与仓库另一篇 [设计模式 - 组合.md](https://gitcode.com/GitHub_Trending/cs/CS-Notes/blob/b70121d377cb6005eb65f12b098cd5decd905669/notes/设计模式 - 组合.md?utm_source=gitcode_repo_files) 天然契合:AST 正是"叶子(终结符)+容器(非终结符)"的组合树。

三、规则检验器实现:从文法到代码的完整复现

仓库文档给出了一个可直接编译运行的规则检验器:它支持 and / or 两种规则,通过规则组合构建一棵解析树,用来检验一段文本是否满足树定义的规则。下面按类逐一拆解,并补齐必要的语义注释,代码可在 JDK 8 及以上版本直接编译运行。

3.1 抽象表达式:定义统一的解释接口

public abstract class Expression {
    public abstract boolean interpret(String str);
}

Expression 扮演 AbstractExpression。这里把抽象方法的入参直接定为 String,正是"Context 即 String"这一简化设计的体现:解释所需的全部外部信息都被压缩进一段文本。

3.2 终结符表达式:判断文本中是否出现某字面量

public class TerminalExpression extends Expression {

    private String literal = null;

    public TerminalExpression(String str) {
        literal = str;
    }

    public boolean interpret(String str) {
        StringTokenizer st = new StringTokenizer(str);
        while (st.hasMoreTokens()) {
            String test = st.nextToken();
            if (test.equals(literal)) {
                return true;
            }
        }
        return false;
    }
}

TerminalExpression 持有唯一的字面量 literal。其解释逻辑很直观:用 java.util.StringTokenizer 把输入文本按空白字符切分成词元(token),只要存在一个词元与 literal 相等就返回 true,否则返回 false

  • new StringTokenizer(str) 采用默认分隔符(空格、制表符、换行等空白字符),等价于"把文本拆成一个个单词";
  • 当文本为空或不含任何目标词元时,循环自然结束并返回 false
  • 工程提示:StringTokenizer 在 JDK 官方文档中被标记为仅保留用于兼容目的的遗留类,新代码建议改用 String.split(...)java.util.Scanner,但本示例用它表达"逐个词元匹配"的语义非常清晰,不影响对模式本身的理解。

3.3 非终结符表达式:AndExpression 与 OrExpression

public class AndExpression extends Expression {

    private Expression expression1 = null;
    private Expression expression2 = null;

    public AndExpression(Expression expression1, Expression expression2) {
        this.expression1 = expression1;
        this.expression2 = expression2;
    }

    public boolean interpret(String str) {
        return expression1.interpret(str) && expression2.interpret(str);
    }
}
public class OrExpression extends Expression {
    private Expression expression1 = null;
    private Expression expression2 = null;

    public OrExpression(Expression expression1, Expression expression2) {
        this.expression1 = expression1;
        this.expression2 = expression2;
    }

    public boolean interpret(String str) {
        return expression1.interpret(str) || expression2.interpret(str);
    }
}

AndExpressionOrExpression成对出现的复合节点,它们并不直接判定文本,而是各自持有两个子表达式,并把自己的解释逻辑定义为对子表达式的布尔组合:

  • AndExpression.interpret():只有当左右两个子表达式都解释为 true 时才返回 true(&& 短路求值);
  • OrExpression.interpret():只要左右任意一个子表达式为 true 即返回 true(|| 短路求值)。

正是这种"节点持有子节点 + 递归调用 interpret()"的写法,让整棵解析树具备了可组合、可无限嵌套的能力——任何一颗子树都可以作为更大表达式的一个操作数。

3.4 Client:手工构建解析树并触发解释

public class Client {

    /**
     * 构建解析树
     */
    public static Expression buildInterpreterTree() {
        // Literal
        Expression terminal1 = new TerminalExpression("A");
        Expression terminal2 = new TerminalExpression("B");
        Expression terminal3 = new TerminalExpression("C");
        Expression terminal4 = new TerminalExpression("D");
        // B C
        Expression alternation1 = new OrExpression(terminal2, terminal3);
        // A Or (B C)
        Expression alternation2 = new OrExpression(terminal1, alternation1);
        // D And (A Or (B C))
        return new AndExpression(terminal4, alternation2);
    }

    public static void main(String[] args) {
        Expression define = buildInterpreterTree();
        String context1 = "D A";
        String context2 = "A B";
        System.out.println(define.interpret(context1));
        System.out.println(define.interpret(context2));
    }
}

Client.buildInterpreterTree()自底向上的方式手工组装解析树,最终得到根表达式 D And (A Or (B Or C)),即逻辑式 D ∧ (A ∨ B ∨ C)。组装步骤为:

  1. 为四个字面量分别创建终结符节点 ABCD
  2. alternation1 = OrExpression(B, C),对应子表达式 B ∨ C
  3. alternation2 = OrExpression(A, alternation1),把结果扩展为 A ∨ B ∨ C
  4. 根节点 AndExpression(D, alternation2),得到 D ∧ (A ∨ B ∨ C)

运行该 main 方法,输出为:

true
false

四、递归求值推演:解释器是如何"跑"起来的

规则检验器的解释过程,本质上是从根节点出发、沿解析树逐层下钻、再把布尔结果逐层回传的递归调用。以文本 "D A" 为例,define.interpret("D A") 的执行轨迹为:

  1. 根节点是 AndExpression,先解释 terminal4(字面量 D):"D A" 分词得到 {D, A},包含 D → 返回 true
  2. 再解释右子树 alternation2OrExpression):其左子 terminal1(字面量 A)在 "D A" 中找到 A → 返回 true|| 短路,无需再判断 B ∨ C);
  3. AndExpression 得到 true && true → 最终输出 true

再看文本 "A B":根节点先解释 terminal4(字面量 D),而 "A B" 中只有 {A, B}、没有 D → 立即返回 false&& 短路求值使右子树不再被解释,整体输出 false。这一结果同时验证了:

  • 非终结符节点不关心子表达式的内部细节,只按自己的规则组合子结果;
  • 解释器的调用链复杂度与树的深度相关,树越深、组合越多,单次求值的间接层就越多,这也是下文要讨论的模式代价之一。

五、使用边界与工程权衡

从实现可以直观总结解释器模式的优点与代价,这与 CS-Notes 笔记面向面试复盘的知识结构一致:

优点

  • 易于改变与扩展文法:每条文法规则对应一个表达式类,新增一种规则只需新增一个表达式子类并接入组合逻辑,符合开闭原则;
  • 文法易实现:只要定义好表达式接口,终结符与非终结符的解释逻辑都可以用极少的代码表达;
  • 天然贴近文法:类结构与文法产生式一一对应,可读性和可维护性都较强。

代价与边界

  • 类数量随文法复杂度爆炸:文档结构明确要求"每个终结符都需要一个 TerminalExpression",文法规则一多,表达式类会迅速膨胀;
  • 执行效率偏低:每次求值都要递归遍历解析树、频繁做字符串分词与匹配,间接层开销明显。文档与示例均未给出任何性能数据,从结构上即可推断——它并不适合高频、大数据量场景;
  • 适用前提苛刻:只有当文法足够简单、且执行性能不敏感时才值得手写解释器;对于表达式语法繁多或规模较大的语言,工程上应改用语法分析器生成器(如 ANTLR、JavaCC)等专门工具生成真正的解析器,而非逐个手写表达式类。

因此,面试与实践中对解释器模式最稳妥的定位是:理解它解决"语言文法对象化 + 递归解释"问题的思路,并清醒地认识到它只适用于小规模语言。

六、JDK 与 Java 生态中的解释器模式身影

CS-Notes 文档在 JDK 一节列出了一批标准库中的解释器模式应用,理解它们有助于把抽象的模式概念锚定到真实的 Java 类库中:

  • java.util.Pattern:正则表达式引擎的核心。正则串先被"编译"为内部状态机表示,再对目标串执行匹配——这与"把一种语言的文法表示为可解释的对象"是同一思想的工程实现。仓库另有 正则表达式.md 笔记对正则的语法细节做系统梳理,可对照阅读;
  • java.text.Format 的所有子类:如 DecimalFormatMessageFormatSimpleDateFormat,它们都接收一个格式模式串(pattern),运行时解析该串并对值做格式化输出,属于典型的"按文法解释输入";
  • java.text.Normalizer:执行 Unicode 文本规范化(如 NFC / NFD),可理解为按 Unicode 规范化文法对字符序列做解释转换;
  • javax.el.ELResolver:Java EE 表达式语言(EL)的解析器抽象,用于把 EL 表达式字符串解释为实际的对象与属性访问。

这几类组件规模不一,但都符合解释器模式"为语言建立对象化文法表示"的内核,可作为论述"该模式在标准库中的位置"时的具体论据。

七、延伸阅读

解释器模式只是 CS-Notes 行为型模式家族中的一员,本仓库的设计模式资料组织如下,便于继续深入:

  • [设计模式 - 目录.md](https://gitcode.com/GitHub_Trending/cs/CS-Notes/blob/b70121d377cb6005eb65f12b098cd5decd905669/notes/设计模式 - 目录.md?utm_source=gitcode_repo_files):完整的设计模式索引,按创建型、行为型、结构型三组分类,解释器位于行为型分组;
  • 设计模式.md:全部模式合并卷,其中第 3 节即解释器模式;
  • [设计模式 - 组合.md](https://gitcode.com/GitHub_Trending/cs/CS-Notes/blob/b70121d377cb6005eb65f12b098cd5decd905669/notes/设计模式 - 组合.md?utm_source=gitcode_repo_files):与解释器模式结构同源(树形递归组合),理解组合模式后再看解析树会更容易;
  • 面向对象思想.md:多态、递归等支撑解释器落地的 OO 基础。

一言以蔽之:解释器模式用"每个语法符号一个类"的文法对象化,换来了文法的易扩展与代码的清晰表达,其实现灵魂是组合树上的递归求值——把握住"文法 → 解析树 → 递归 interpret"这条主线,你就能在任何需要解释执行的小语言场景中自如地复现它。

登录后查看全文
热门项目推荐
相关项目推荐