解释器模式-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()负责解释自己; - 解释过程是递归遍历语法树的过程,终结符直接返回结果,非终结符组合子节点的结果;
- 上下文与语法树分离,同一棵树可以针对不同的上下文反复解释,具备很强的复用性。
解释器模式的优点是易于扩展文法(新增一个运算符只需新增一个节点类)、语法与执行逻辑清晰;缺点是当文法复杂时,节点类数量会爆炸,且每一层递归都会带来一定的性能开销。因此解释器模式适合语法简单、表达式会被复用的场景;对于复杂语言,业界通常选择成熟的解析器生成器或表达式引擎来实现。