17|解释器:用对象树表达一门小语言
配套代码: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 设计模式学习指南