跳转到内容

解释器模式 (Interpreter)

配置里写 rate * amount + fee、报表里填 a + b * 2,或者内部工具里用一句「条件表达式」筛数据——这类需求本质都是在跑一段很小的、自己定义的语言。如果每次都拿正则或一长串 if-else 去拆字符串,很快就会又难读又难扩。

解释器模式的做法是:先把这种小语言的文法用类结构表示出来(谁是最小单元、谁由谁组合),再让每个「语法单元」都会算自己那一份,最后从根往下递归算完,得到结果。适合文法简单、变动不多、但对可读和可扩展有要求的场景;文法特别复杂时再考虑用解析器生成器。


你很可能遇到过:配置里支持「公式」,例如 score * 1.2 + bonus,或者某个内部筛选器支持 level:ERROR AND app:api。最直接的写法是:拿到字符串,按空格和符号 split,再一堆 if 判断、拼接成最终要的数值或布尔。这样做有两个麻烦:一是加一种运算或一种条件就要改一大段分支;二是「式子长什么样」和「式子怎么算」搅在一起,很难单独测试或复用。

更稳妥的做法是:先给这种小语言定一个简单文法(哪些是原子、哪些由别的组合而成),用树形结构把一句输入表示成一棵「表达式树」,树上的每个节点都知道自己该怎么算,最后从根节点一路算下去。这就是解释器模式在干的事——用面向对象的方式把文法写出来,并让每个节点负责解释自己


很多小语言都可以拆成两类东西:

  • 终结符:不能再往下拆的原子,例如数字字面量 42、变量名 rate、或者关键字 AND。在表达式树里,它们就是叶子节点,解释时直接查上下文或返回值。
  • 非终结符:由别的符号组合而成的规则,例如「加法 = 左边表达式 + 右边表达式」「与条件 = 左条件 AND 右条件」。在树里对应有子节点的节点,解释时先让子节点算完,再根据规则(加、乘、与、或)算出自己的结果。

一句话对应一棵树。例如 1 + 2 * 3 会变成:根是加法,左子是数字 1,右子是一棵「乘法」树(左 2 右 3)。解释时先算乘法得 6,再算加法得 7。解释器模式里的「解释」,就是让这棵树的根节点执行「解释」:它需要子结果就递归调用子节点的解释方法,自己只做组合运算。


三、四个角色在代码里长什么样?

Section titled “三、四个角色在代码里长什么样?”
  • 抽象表达式 (AbstractExpression):所有「可被解释」的节点共有的接口或抽象类,通常有一个方法,例如 interpret(Context ctx),返回这一棵(子)树的结果。
  • 终结符表达式 (TerminalExpression):对应文法里的终结符。例如「数字」节点在 interpret 里直接返回数字;「变量」节点在 interpret 里从 Context 里按变量名取值再返回。
  • 非终结符表达式 (NonterminalExpression):对应一条组合规则,内部持有子表达式(一个或多个)。在 interpret 里先对子表达式调用 interpret(ctx),再按规则(加、乘、与等)算出自己的结果。
  • 上下文 (Context):解释过程中共用的数据,例如变量表(变量名 → 值)、输入字符串、或者当前解析位置。一般作为参数在 interpret(context) 里一路传下去。

客户端(或解析器)负责:把输入串解析成一棵由上述节点组成的树,然后把 Context 准备好,对根节点调用 interpret(context),得到最终结果。树的结构 = 文法在内存里的表示每个节点的 interpret = 这条规则如何参与计算


抽象表达式是根类型;终结符没有子节点,非终结符持有子表达式,解释时组合子结果。

classDiagram
    class AbstractExpression {
        <<abstract>>
        +interpret(context)* int
    }
    class TerminalExpression {
        -value int
        +interpret(context) int
    }
    class NonterminalExpression {
        -left AbstractExpression
        -right AbstractExpression
        +interpret(context) int
    }
    class Context
    AbstractExpression <|-- TerminalExpression
    AbstractExpression <|-- NonterminalExpression
    NonterminalExpression o-- AbstractExpression

不依赖框架,用「数字 + 加法」把上面的关系跑通。

public abstract class AbstractExpression {
public abstract int interpret();
}
public class TerminalExpression extends AbstractExpression {
private final int value;
public TerminalExpression(int value) { this.value = value; }
@Override
public int interpret() { return value; }
}
public class AddExpression extends AbstractExpression {
private final AbstractExpression left, right;
public AddExpression(AbstractExpression l, AbstractExpression r) {
left = l;
right = r;
}
@Override
public int interpret() {
return left.interpret() + right.interpret();
}
}
// 解释 1 + 2
AbstractExpression exp = new AddExpression(
new TerminalExpression(1),
new TerminalExpression(2)
);
System.out.println(exp.interpret()); // 3

这里还没有 Context,也没有变量;只是把「加法」和「数字」两种节点组成树并递归求值。实际项目里通常会加 Context 传变量表,再加乘、减、括号等节点。


六、实战:带变量的表达式求值服务

Section titled “六、实战:带变量的表达式求值服务”

很多报表、配置或规则里需要「用户写一个式子,系统按变量取值再算出来」,例如 rate * amount + fee,其中 rateamountfee 由调用方传入。下面用解释器模式做一个表达式求值:支持加减乘、数字字面量和变量名,解析用简单的手写 token 方式(避免引入 parser 生成器),树建好后用 interpret(context) 求值。求值逻辑以 Bean 形式提供,方便在需要算公式的地方注入使用。

Context 只存「变量名 → 数值」,解释时终结符里的「变量」节点从这里取值。

package com.example.demo.interpreter;
import java.util.Map;
public class EvalContext {
private final Map<String, Double> variables;
public EvalContext(Map<String, Double> variables) {
this.variables = Map.copyOf(variables);
}
public double get(String name) {
if (!variables.containsKey(name)) {
throw new IllegalArgumentException("unknown variable: " + name);
}
return variables.get(name);
}
}

所有节点都实现 interpret(EvalContext ctx),返回 double。终结符有两种:字面量(直接返回值)和变量(从 ctx 取值)。

package com.example.demo.interpreter;
public interface Expr {
double interpret(EvalContext ctx);
}
package com.example.demo.interpreter;
public class LiteralExpr implements Expr {
private final double value;
public LiteralExpr(double value) { this.value = value; }
@Override
public double interpret(EvalContext ctx) { return value; }
}
package com.example.demo.interpreter;
public class VariableExpr implements Expr {
private final String name;
public VariableExpr(String name) { this.name = name; }
@Override
public double interpret(EvalContext ctx) { return ctx.get(name); }
}

每个二元运算持有一个左子、一个右子,解释时先算左右再运算。

package com.example.demo.interpreter;
public class AddExpr implements Expr {
private final Expr left, right;
public AddExpr(Expr left, Expr right) { this.left = left; this.right = right; }
@Override
public double interpret(EvalContext ctx) {
return left.interpret(ctx) + right.interpret(ctx);
}
}
public class SubExpr implements Expr {
private final Expr left, right;
public SubExpr(Expr left, Expr right) { this.left = left; this.right = right; }
@Override
public double interpret(EvalContext ctx) {
return left.interpret(ctx) - right.interpret(ctx);
}
}
public class MulExpr implements Expr {
private final Expr left, right;
public MulExpr(Expr left, Expr right) { this.left = left; this.right = right; }
@Override
public double interpret(EvalContext ctx) {
return left.interpret(ctx) * right.interpret(ctx);
}
}

6.4 简单解析:字符串 → 表达式树

Section titled “6.4 简单解析:字符串 → 表达式树”

这里用「按空格拆 token、再按优先级组树」的简化方式,只支持 + - *、数字和变量名,方便理解。实际若式子更复杂可以上递归下降或解析器生成器。

package com.example.demo.interpreter;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
public class SimpleExprParser {
private static final Pattern NUMBER = Pattern.compile("^[0-9]+(\\.[0-9]+)?$");
public static Expr parse(String input) {
String s = input == null ? "" : input.trim();
if (s.isEmpty()) throw new IllegalArgumentException("empty expression");
List<String> tokens = tokenize(s);
return parseAddSub(tokens, new int[]{0});
}
private static List<String> tokenize(String s) {
List<String> list = new ArrayList<>();
StringBuilder cur = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '+' || c == '-' || c == '*' || c == '(' || c == ')') {
if (cur.length() > 0) {
list.add(cur.toString());
cur.setLength(0);
}
list.add(String.valueOf(c));
} else if (!Character.isWhitespace(c)) {
cur.append(c);
}
}
if (cur.length() > 0) list.add(cur.toString());
return list;
}
private static Expr parseAddSub(List<String> tokens, int[] idx) {
Expr n = parseMul(tokens, idx);
while (idx[0] < tokens.size()) {
String op = tokens.get(idx[0]);
if (op.equals("+")) {
idx[0]++;
n = new AddExpr(n, parseMul(tokens, idx));
} else if (op.equals("-")) {
idx[0]++;
n = new SubExpr(n, parseMul(tokens, idx));
} else break;
}
return n;
}
private static Expr parseMul(List<String> tokens, int[] idx) {
Expr n = parsePrimary(tokens, idx);
while (idx[0] < tokens.size() && "*".equals(tokens.get(idx[0]))) {
idx[0]++;
n = new MulExpr(n, parsePrimary(tokens, idx));
}
return n;
}
private static Expr parsePrimary(List<String> tokens, int[] idx) {
if (idx[0] >= tokens.size()) throw new IllegalArgumentException("unexpected end");
String t = tokens.get(idx[0]++);
if (NUMBER.matcher(t).matches()) {
return new LiteralExpr(Double.parseDouble(t));
}
return new VariableExpr(t);
}
}

把「解析 + 求值」封装成一个服务,接收公式字符串和变量表,返回结果。业务层只依赖这个服务,不关心树的结构。

package com.example.demo.interpreter;
import org.springframework.stereotype.Service;
import java.util.Map;
@Service
public class ExpressionEvalService {
public double eval(String expression, Map<String, Double> variables) {
Expr root = SimpleExprParser.parse(expression);
EvalContext ctx = new EvalContext(variables);
return root.interpret(ctx);
}
}

例如某个配置或报表接口需要「按公式算出一个值」:注入 ExpressionEvalService,传入用户填的公式和当前变量(如 rateamount),拿到结果再继续后续逻辑。

package com.example.demo.interpreter;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/calc")
@RequiredArgsConstructor
public class CalcController {
private final ExpressionEvalService expressionEvalService;
@PostMapping("/eval")
public double eval(@RequestBody EvalRequest req) {
return expressionEvalService.eval(req.getExpression(), req.getVariables());
}
}
// EvalRequest: expression (String), variables (Map<String, Double>)

调用方传 {"expression": "rate * amount + fee", "variables": {"rate": 0.1, "amount": 100, "fee": 5}},返回 15.0。公式的增删(例如以后加除、括号)主要改解析和新增 Expr 子类,求值逻辑仍然是一棵树递归 interpret,符合开闭原则。


解释器模式用树形结构表示一句「小语言」:叶子是终结符(数字、变量),中间节点是非终结符(加减乘、与或等),每个节点实现 interpret(context),从根算到叶就得到结果。适合文法简单、需要清晰可扩展的实现、又不想上重量级解析器生成器的场景。上面示例里,表达式求值服务用 Bean 提供,解析出树后只依赖 ExprEvalContext,和具体业务解耦;若要支持除、括号或更多运算,只需扩展解析和对应的 Expr 实现即可。