跳到主要内容

解释器模式-Interpreter Pattern

序言​

我们在日常开发中经常要与"表达式"打交道:SQL 查询语句、正则表达式、SpEL 表达式、数学公式……这些都可以看作一门"小语言"。语言的句子按照一定的**文法(Grammar)**组织,比如正则表达式 \d{4}-\d{2}-\d{2} 表示"四位数字-两位数字-两位数字"的日期格式。

解释器模式(Interpreter Pattern)就是用来处理这类问题的:为一种简单的语言定义文法表示,并提供一个解释器来执行(解释)用这种语言书写的句子。

一个非常贴近生活的类比是翻译:你(客户端)用一门"语言"说出一个句子,翻译官(解释器)按照这门语言的"语法规则"理解句子,然后转换成对方能听懂的动作。语言的规则是固定的,翻译官负责按规则理解任意句子。

定义​

Given a language, define a represention for its grammar along with an interpreter that uses the representation to interpret sentences in the language.

给定一门语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。

"文法"通常可以表示为一棵语法树(Syntax Tree):句子中的每个元素对应树中的一个节点,叶子节点是终结符表达式(Terminal Expression),内部节点是非终结符表达式(Nonterminal Expression)。解释的过程就是递归地遍历这棵语法树,对每个节点执行它定义的"解释"操作。

结构​

让我们从一个问题开始​

假设我们要做一个简单的权限系统,需要判断一个用户是否满足某个条件表达式,例如:

(user.age > 18) AND (user.vip == true)

如果不使用任何模式,最直接的做法就是写一堆 if-else 去判断表达式的每一部分:

public boolean evaluate(String expression, Map<String, Object> context) {
if (expression.contains("AND")) {
String[] parts = expression.split("AND");
return evaluate(parts[0], context) && evaluate(parts[1], context);
} else if (expression.contains("OR")) {
String[] parts = expression.split("OR");
return evaluate(parts[0], context) || evaluate(parts[1], context);
} else if (expression.startsWith("NOT ")) {
return !evaluate(expression.substring(4), context);
} else {
// 判断变量取值...
}
}

随着运算符越来越多(AND、OR、NOT、比较运算、括号……),这个方法的 if-else 会越来越长,越来越难以维护。更糟糕的是,条件表达式可能需要被重复解释(比如在运行时动态传入不同的变量值),而每次都要重新解析字符串。

解释器模式给出的方案是:先把"语言"的结构固定下来(文法),再把表达式解析成一棵语法树,解释过程就是遍历这棵树。

解释器模式的结构​

各个角色的职责如下:

  • AbstractExpression(抽象表达式):声明一个抽象的解释操作 interpret(),是所有节点的公共接口,对应图中的 Expression。
  • TerminalExpression(终结符表达式):文法中的叶子节点,没有子表达式,直接返回结果。对应图中的 VariableExpression——它只负责从上下文中取出某个变量的值。
  • NonterminalExpression(非终结符表达式):文法中的内部节点,包含一个或多个子表达式,解释时递归调用子表达式的 interpret() 并组合结果。对应图中的 AndExpression、OrExpression、NotExpression。
  • Context(上下文):包含解释器之外的一些全局信息,例如变量的取值,在解释过程中被各个节点读取。
  • Client(客户端):构建语法树并调用根节点的 interpret()。

代码实现​

我们来实现一个布尔表达式解释器,支持三种运算:AND、OR、NOT,以及由变量组成的"句子",例如 (a AND b) OR (NOT c)。

定义抽象表达式接口​

public interface Expression {
boolean interpret(Context context);
}

定义上下文​

上下文保存变量的取值,解释器在执行过程中通过上下文读取变量的值:

import java.util.HashMap;
import java.util.Map;

public class Context {

private final Map<String, Boolean> variables = new HashMap<>();

public void setVariable(String name, boolean value) {
variables.put(name, value);
}

public boolean getVariable(String name) {
return variables.getOrDefault(name, false);
}
}

定义终结符表达式​

终结符表达式是语法树的叶子节点,它只做一件事:从上下文中读取变量的值。

public class VariableExpression implements Expression {

private final String name;

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

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

@Override
public String toString() {
return name;
}
}

定义非终结符表达式​

非终结符表达式包含子表达式,解释时递归调用子表达式的 interpret() 并组合结果。

AND 表达式:左右两个子表达式都为 true 时结果为 true。

public class AndExpression implements Expression {

private final Expression left;
private final Expression right;

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

@Override
public boolean interpret(Context context) {
return left.interpret(context) && right.interpret(context);
}

@Override
public String toString() {
return "(" + left + " AND " + right + ")";
}
}

OR 表达式:左右两个子表达式任意一个为 true 时结果为 true。

public class OrExpression implements Expression {

private final Expression left;
private final Expression right;

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

@Override
public boolean interpret(Context context) {
return left.interpret(context) || right.interpret(context);
}

@Override
public String toString() {
return "(" + left + " OR " + right + ")";
}
}

NOT 表达式:对子表达式的求值结果取反。

public class NotExpression implements Expression {

private final Expression expression;

public NotExpression(Expression expression) {
this.expression = expression;
}

@Override
public boolean interpret(Context context) {
return !expression.interpret(context);
}

@Override
public String toString() {
return "(NOT " + expression + ")";
}
}

客户端:构建语法树并解释​

语法树可以由客户端手工组装,也可以由一个解析器把字符串表达式解析成树。这里先手工组装一棵树,演示解释器模式的核心思想:

public class Main {

public static void main(String[] args) {
// 上下文:为变量赋值
Context context = new Context();
context.setVariable("a", true);
context.setVariable("b", false);
context.setVariable("c", true);

// 构建表达式 (a AND b) OR (NOT c) 的语法树
Expression a = new VariableExpression("a");
Expression b = new VariableExpression("b");
Expression c = new VariableExpression("c");

Expression notC = new NotExpression(c);
Expression ab = new AndExpression(a, b);
Expression expression = new OrExpression(ab, notC);

System.out.println("表达式:" + expression);
System.out.println("求值结果:" + expression.interpret(context));

// 同一个语法树,换个变量值可以重新解释
context.setVariable("a", false);
System.out.println("改变 a 的值后:" + expression.interpret(context));
}
}

输出结果:

表达式:(a AND b) OR (NOT c)
求值结果:true
改变 a 的值后:true

这里有一个很重要的特性:语法树一旦构建完成,可以针对不同的上下文反复解释。变量值变了,解释结果也会跟着变化,但树的结构不需要重建。这正是解释器模式适合"规则引擎"、"表达式引擎"这类场景的原因。

从字符串到语法树:一个简单的解析器​

上面的语法树是手工组装的。实际使用中,表达式通常以字符串形式传入,我们需要一个解析器把字符串解析成语法树。下面是一个支持 AND / OR / NOT / 括号的简单解析器(此处只展示解析思想,代码做了简化):

public class BooleanExpressionParser {

private String expression;
private int pos;

public BooleanExpressionParser(String expression) {
this.expression = expression;
this.pos = 0;
}

public Expression parse() {
return parseOr();
}

// 处理 OR:orExpr := andExpr (OR andExpr)*
private Expression parseOr() {
Expression left = parseAnd();
while (match("OR")) {
left = new OrExpression(left, parseAnd());
}
return left;
}

// 处理 AND:andExpr := notExpr (AND notExpr)*
private Expression parseAnd() {
Expression left = parseNot();
while (match("AND")) {
left = new AndExpression(left, parseNot());
}
return left;
}

// 处理 NOT 和括号、变量
private Expression parseNot() {
if (match("NOT")) {
return new NotExpression(parseNot());
}
if (match("(")) {
Expression inner = parseOr();
match(")");
return inner;
}
return parseVariable();
}

private Expression parseVariable() {
// 读取一个变量名,如 a、b、c
String name = readName();
return new VariableExpression(name);
}

private boolean match(String token) {
// 跳过空白后,判断下一个 token 是否匹配,匹配则消费
// ... 实现略
return false;
}

private String readName() {
// ... 实现略
return "";
}
}

有了解析器,客户端就可以从字符串直接获得语法树:

Expression expression = new BooleanExpressionParser("(a AND b) OR (NOT c)").parse();

这样,"语言"(表达式字符串)和"解释"(语法树求值)就被清晰地区分开来:解析器负责把字符串变成树,解释器负责把树变成结果。

在开源框架中的应用​

Spring SpEL:源码视角的表达式语法树求值​

Spring 框架中的 SpEL 是解释器模式在工业界的经典应用。从源码层面看,SpEL 的实现与解释器模式的结构一一对应:

  • 抽象表达式(AbstractExpression) = org.springframework.expression.spel.ast.SpelNodeImpl——所有语法树节点的抽象基类,声明了 getValueInternal() 求值方法;
  • 终结符表达式(TerminalExpression) = Literal(字面量)、PropertyOrFieldReference(属性引用)等叶子节点;
  • 非终结符表达式(NonterminalExpression) = OpAnd(&&)、OpOr(||)、OpEQ(==)、Ternary(三元)等运算节点;
  • 上下文(Context) = ExpressionState,保存变量表、根对象等求值环境。

解析:从字符串到语法树​

SpelExpressionParser#parseExpression 的核心是 doParseExpression,它把表达式字符串解析为一棵 SpelNodeImpl 语法树,并包装成 SpelExpression:

// org.springframework.expression.spel.standard.SpelExpressionParser(源码骨架)
protected SpelExpression doParseExpression(String expressionString, ParserContext context) throws ParseException {
// 内部解析器:递归下降,构建 SpelNodeImpl 语法树
SpelNodeImpl ast = internalParseExpression(expressionString);
// 用语法树构造表达式对象
return new SpelExpression(expressionString, ast, this.configuration);
}

求值:递归遍历语法树​

SpelExpression 持有语法树根节点,对外求值时把调用委托给根节点,由根节点递归求值整棵树:

// org.springframework.expression.spel.standard.SpelExpression(源码骨架)
public class SpelExpression implements Expression {

private final String expression; // 原始表达式字符串
private final SpelNodeImpl ast; // 语法树根节点

@Override
public <T> T getValue(EvaluationContext context, Class<T> expectedResultType) {
// 构造求值上下文 ExpressionState,调用根节点递归求值
ExpressionState state = new ExpressionState((StandardEvaluationContext) context, ...);
Object result = this.ast.getValueInternal(state); // <-- 递归求值的入口
...
}
}

非终结符:短路求值的运算节点​

以 OpAnd 为例,它持有左右两个子表达式节点,求值时先求左子节点,再按布尔短路逻辑决定是否求右子节点——这正是解释器模式中"非终结符表达式的解释依赖于子表达式的解释":

// org.springframework.expression.spel.ast.OpAnd(源码骨架)
public class OpAnd extends Operator {

// getValueInternal 是解释器模式中 Expression 接口的核心方法
@Override
public BooleanTypedValue getValueInternal(ExpressionState state) throws EvaluationException {
// 解释左操作数
BooleanTypedValue left = (BooleanTypedValue) getLeftOperand().getValueInternal(state);
// 短路:左为 false 则不再解释右操作数
if (!left.getBooleanValue()) {
return BooleanTypedValue.FALSE;
}
// 解释右操作数
BooleanTypedValue right = (BooleanTypedValue) getRightOperand().getValueInternal(state);
return BooleanTypedValue.forValue(right.getBooleanValue());
}
}

OpEQ、OpOr、Ternary 等节点结构相同:每个节点都实现 getValueInternal,组合节点的求值递归地委托给子节点——与我们在代码实现中写的 AndExpression/OrExpression 完全同构。

Spring 中的 @Value("${...}")、@Cacheable(key = "...")、Spring Security 的表达式权限控制,底层都是这一棵"解释器树"在求值。

Java 正则表达式:Pattern 源码中的语法节点树​

java.util.regex 是解释器模式最底层的实现:Pattern 把正则表达式字符串解析成一棵 Node 语法树,Matcher 在这棵树上递归匹配。

抽象表达式:Pattern.Node​

// java.util.regex.Pattern 内部抽象类(源码骨架)
private abstract static class Node {

// 抽象求值方法:从位置 i 开始匹配
abstract boolean match(Matcher matcher, int i, CharSequence seq) throws Exception;
}

Node 就是解释器模式的"抽象表达式",match 就是它的"解释"方法。它的子类们构成语法树:

  • 终结符节点:CharProperty(单字符匹配)、Literal(字面量)等,直接判断当前字符;
  • 非终结符节点:GroupHead/GroupTail(分组)、Curly(量词 */+/?/{n,m})、Branch(多选 |)、Loop(循环)等,把匹配委托给子节点。

终结符:CharProperty 匹配单个字符​

// java.util.regex.Pattern.CharProperty(源码骨架)
private static class CharProperty extends Node {

CharPredicate predicate;

// 终结符的解释:判断位置 i 的字符是否满足谓词
boolean match(Matcher matcher, int i, CharSequence seq) throws Exception {
if (i < matcher.to) {
if (predicate.isSatisfiedBy(seq.charAt(i))) {
return next.match(matcher, i + 1, seq); // 匹配成功后交给后继节点
}
}
return false;
}
}

非终结符:Curly 处理量词​

// java.util.regex.Pattern.Curly(源码骨架,量词节点)
static final class Curly extends Node {

Node atom; // 被量化的子表达式(如 \d 对应的节点)
int cmin, cmax; // 最少/最多重复次数

// 非终结符的解释:尝试让 atom 匹配 cmin~cmax 次
boolean match(Matcher matcher, int i, CharSequence seq) throws Exception {
int j;
for (j = 0; j < cmin; j++) {
if (atom.match(matcher, i, seq)) {
i = matcher.last;
} else {
return false;
}
}
// 贪婪/懒惰/占有模式的后续匹配……
...
}
}

解析入口与匹配入口​

// Pattern.compile 解析入口(源码骨架)
public static Pattern compile(String regex) {
return new Pattern(regex, 0);
}

private Pattern(String p, int f) {
...
compile(); // 内部:把正则字符串解析为 Node 语法树
}

private void compile() {
...
Node root = new Start(lastnode); // 语法树根节点(Start 包装最后一个节点)
...
}
// Matcher.search 匹配入口(源码骨架)
boolean search(int from) {
...
return parentPattern.root.match(this, from, text); // 从根节点开始递归匹配
}

Pattern.compile("(\\d{4})-(\\d{2})") 在内部就是构建了一棵 Start → GroupHead → Curly(\\d{4}) → ... → GroupTail 的节点树;matcher.find() 从根节点 match 开始,在这棵树上递归推进。正则表达式、SQL、模板引擎等"小语言"的解析与执行,本质上都是解释器模式对语法树的递归解释。

适用场景​

  • 当需要解释执行一种简单且语法稳定的语言或表达式时,可以使用解释器模式。例如配置规则、权限表达式、布尔逻辑表达式、数学公式等。
  • 当表达式需要被多次解释、并且每次解释时变量值不同时,可以将表达式预先解析成语法树,复用同一棵树对不同上下文求值。
  • 当语言的文法简单、类的数量可控时,解释器模式非常直观;但一旦文法变得复杂(运算符、优先级、括号层级很多),语法树节点的类数量会迅速膨胀,这时更适合使用成熟的表达式引擎(如 ANTLR、SpEL)而不是自己实现。
  • 在规则引擎、报表系统、模板引擎中,解释器模式可以作为"规则/表达式"的底层求值模型。

与其他设计模式的关系​

  • 解释器模式与**组合模式(Composite)**关系最为密切:语法树本身就是一种组合结构,终结符对应叶节点,非终结符对应容器节点,interpret() 的递归调用与组合模式对子节点的递归遍历如出一辙。
  • 语法树中的终结符表达式在整棵树中可能被多处引用(例如同一个变量出现多次),可以通过**享元模式(Flyweight)**共享终结符节点,减少对象数量。
  • 如果希望在不修改语法节点类的前提下,为语法树增加新的操作(比如打印、类型检查、代码生成),可以配合**访问者模式(Visitor)**遍历语法树。
  • 在命令模式的宏命令中,命令的组合结构与语法树类似,两者经常结合使用。

总结​

解释器模式把"语言"的解析与执行建模为一棵语法树:

  • 文法(终结符与非终结符)被建模为树上的节点,每个节点实现 interpret() 负责解释自己;
  • 解释过程是递归遍历语法树的过程,终结符直接返回结果,非终结符组合子节点的结果;
  • 上下文与语法树分离,同一棵树可以针对不同的上下文反复解释,具备很强的复用性。

解释器模式的优点是易于扩展文法(新增一个运算符只需新增一个节点类)、语法与执行逻辑清晰;缺点是当文法复杂时,节点类数量会爆炸,且每一层递归都会带来一定的性能开销。因此解释器模式适合语法简单、表达式会被复用的场景;对于复杂语言,业界通常选择成熟的解析器生成器或表达式引擎来实现。