配套代码:GitHub 仓库 · 本篇完整源码 · 行为测试。使用 JDK 25 与 Gradle,包名为 com.hanserwei.patterns.interpreter

系列导航:Java 25 设计模式学习指南

上一篇:命令 · 下一篇:迭代器

文章筛选规则需要表达“带 java 标签,并且带 oop 或 gradle 标签”。散落在页面里的布尔表达式无法作为配置组合、复用或逐节点检查。我们可以把规则的语法结构直接建成对象树。

本例只实现一个非常小的标签语言:标签、与、或。暂时由 Java 代码构建语法树,不实现从字符串解析规则的解析器,避免把词法和语法分析的复杂性掩盖成几行 split。

从问题提炼设计意图

解释器为语言中的语法规则定义对象表示,并通过这些对象在上下文中解释句子。终结符处理基本事实,非终结符组合子表达式。

对象职责与协作关系

示例角色 职责
Expression 抽象表达式,统一 interpret 上下文契约
TagExpression 终结符,检查一个标签
AndExpression、OrExpression 非终结符,递归组合布尔结果
Set<String> 上下文,提供本次文章标签事实
classDiagram
    Expression <|.. TagExpression
    Expression <|.. AndExpression
    Expression <|.. OrExpression
    AndExpression o-- Expression : left and right
    OrExpression o-- Expression : left and right

代码思路:变化应该落在哪个对象上

可以把抽象语法写成 E := Tag(name) | And(E, E) | Or(E, E)。这里的语法描述对象结构,不承诺存在一个能解析用户字符串的前端。

TagExpression 保存规则中的标签名,interpret 接收文章的 Set 并调用 contains。规则对象不把本次上下文存成字段,因此同一棵规则树可以先后解释不同文章。

AndExpression 和 OrExpression 分别保存 left 与 right,递归解释子节点再用布尔运算组合。Java 的 && 与 || 保留短路语义。括号层次来自树的嵌套:And(java, Or(oop, gradle)) 与 Or(And(java, oop), gradle) 对只有 gradle 的文章会给出不同结果。

关键实现与独立运行

配套仓库中的包名是 com.hanserwei.patterns.interpreter,源码目录为 src/main/java/com/hanserwei/patterns/interpreter/。仓库地址统一见系列导航。以下展示关键文件的完整内容;其余角色和测试在同一仓库中,每个顶级类型各占一个文件。

Expression.java

package com.hanserwei.patterns.interpreter;

import java.util.Set;

/** 标签规则语言中的表达式节点. */
public interface Expression {
  /** 在给定非空标签集合中求值. */
  boolean interpret(Set<String> tags);
}

TagExpression.java

package com.hanserwei.patterns.interpreter;

import java.util.Objects;
import java.util.Set;

/** 判断单个标签是否存在的终结符表达式. */
public final class TagExpression implements Expression {
  /** 本节点要匹配的标签. */
  private final String tag;

  /** 指定非空标签名称. */
  public TagExpression(String tag) {
    this.tag = Objects.requireNonNull(tag, "tag");
  }

  /** 当前上下文包含目标标签时为真. */
  @Override
  public boolean interpret(Set<String> tags) {
    return tags.contains(tag);
  }
}

AndExpression.java

package com.hanserwei.patterns.interpreter;

import java.util.Objects;
import java.util.Set;

/** 把两个子表达式组合为布尔规则的非终结符. */
public final class AndExpression implements Expression {
  /** 左侧子表达式,先求值. */
  private final Expression left;

  /** 右侧子表达式,按布尔运算短路求值. */
  private final Expression right;

  /** 构造表达式树的二元节点. */
  public AndExpression(Expression left, Expression right) {
    this.left = Objects.requireNonNull(left, "left");
    this.right = Objects.requireNonNull(right, "right");
  }

  /** 递归解释子节点,并按当前运算符组合结果. */
  @Override
  public boolean interpret(Set<String> tags) {
    return left.interpret(tags) && right.interpret(tags);
  }
}

Demo.java 展示调用方如何装配这些对象:

package com.hanserwei.patterns.interpreter;

import java.util.Set;

/** 演示本章对象的装配方式和可观察结果. */
public final class Demo {
  /** 禁止实例化演示入口. */
  private Demo() {}

  /** 运行独立示例;args 为未使用的命令行参数. */
  public static void main(String[] args) {
    Expression rule =
        new AndExpression(
            new TagExpression("java"),
            new OrExpression(new TagExpression("oop"), new TagExpression("gradle")));
    System.out.println(rule.interpret(Set.of("java", "gradle")));
  }
}

在配套代码仓库根目录运行;Windows 使用 gradlew.bat 替换 ./gradlew

./gradlew runInterpreter
./gradlew test --tests 'com.hanserwei.patterns.interpreter.PatternTest'

示例的业务输出如下,省略 Gradle 自身的任务提示:

true

用测试确认模式的行为

java 与 oop、java 与 gradle 都匹配;仅 gradle 和空上下文不匹配;同一规则树在不同上下文中没有残留状态。

对应测试位于 src/test/java/com/hanserwei/patterns/interpreter/PatternTest.java。建议先运行现有测试,再改动一个协作环节,观察哪个断言能够发现问题。

常见用法

  • 规则很小、语法稳定的权限条件或标签筛选语言。
  • 需要组合和复用表达式对象的配置系统。
  • 教学或工具中希望把语法树与求值过程直接关联。

适用边界与容易踩的坑

解释器模式不提供解析器。要支持“java AND (oop OR gradle)”这样的输入,还需定义词法、优先级、括号、转义和错误位置;不能简单按空格切分就认为支持完整语法。

语法规则数量持续增长时,类数量和递归成本也增长。复杂 DSL 更适合成熟解析器、编译器或专门规则引擎。需要限制不可信规则的深度和工作量,防止极深嵌套耗尽调用栈。

本例节点是无副作用的查询,短路只影响计算次数。如果未来节点会访问网络或修改状态,求值顺序就有可见后果,应重新考虑是否还应把这些动作放进布尔表达式。

与相近模式比较

组合模式可以表达这棵树的结构;解释器额外规定节点对应语法规则以及如何求值。访问者也可以把求值操作从节点中移出去,适合需要多个独立操作时使用。

动手练习

新增 NotExpression,并测试 Not(java) 与 And(java, Not(oop))。然后列出字符串解析器必须处理的五类错误输入,先设计错误模型,再决定是否值得手写解析器。

系列导航:Java 25 设计模式学习指南

上一篇:命令 · 下一篇:迭代器