解释器模式 (Interpreter)
配置里写 rate * amount + fee、报表里填 a + b * 2,或者内部工具里用一句「条件表达式」筛数据——这类需求本质都是在跑一段很小的、自己定义的语言。如果每次都拿正则或一长串 if-else 去拆字符串,很快就会又难读又难扩。
解释器模式的做法是:先把这种小语言的文法用类结构表示出来(谁是最小单元、谁由谁组合),再让每个「语法单元」都会算自己那一份,最后从根往下递归算完,得到结果。适合文法简单、变动不多、但对可读和可扩展有要求的场景;文法特别复杂时再考虑用解析器生成器。
一、问题从哪来?
Section titled “一、问题从哪来?”你很可能遇到过:配置里支持「公式」,例如 score * 1.2 + bonus,或者某个内部筛选器支持 level:ERROR AND app:api。最直接的写法是:拿到字符串,按空格和符号 split,再一堆 if 判断、拼接成最终要的数值或布尔。这样做有两个麻烦:一是加一种运算或一种条件就要改一大段分支;二是「式子长什么样」和「式子怎么算」搅在一起,很难单独测试或复用。
更稳妥的做法是:先给这种小语言定一个简单文法(哪些是原子、哪些由别的组合而成),用树形结构把一句输入表示成一棵「表达式树」,树上的每个节点都知道自己该怎么算,最后从根节点一路算下去。这就是解释器模式在干的事——用面向对象的方式把文法写出来,并让每个节点负责解释自己。
二、文法、终结符和非终结符
Section titled “二、文法、终结符和非终结符”很多小语言都可以拆成两类东西:
- 终结符:不能再往下拆的原子,例如数字字面量
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
五、最小示例:1 + 2
Section titled “五、最小示例:1 + 2”不依赖框架,用「数字 + 加法」把上面的关系跑通。
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 + 2AbstractExpression exp = new AddExpression( new TerminalExpression(1), new TerminalExpression(2));System.out.println(exp.interpret()); // 3这里还没有 Context,也没有变量;只是把「加法」和「数字」两种节点组成树并递归求值。实际项目里通常会加 Context 传变量表,再加乘、减、括号等节点。
六、实战:带变量的表达式求值服务
Section titled “六、实战:带变量的表达式求值服务”很多报表、配置或规则里需要「用户写一个式子,系统按变量取值再算出来」,例如 rate * amount + fee,其中 rate、amount、fee 由调用方传入。下面用解释器模式做一个表达式求值:支持加减乘、数字字面量和变量名,解析用简单的手写 token 方式(避免引入 parser 生成器),树建好后用 interpret(context) 求值。求值逻辑以 Bean 形式提供,方便在需要算公式的地方注入使用。
6.1 上下文:变量表
Section titled “6.1 上下文:变量表”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); }}6.2 抽象表达式与终结符
Section titled “6.2 抽象表达式与终结符”所有节点都实现 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); }}6.3 非终结符:加减乘
Section titled “6.3 非终结符:加减乘”每个二元运算持有一个左子、一个右子,解释时先算左右再运算。
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); }}6.5 求值服务(Bean)
Section titled “6.5 求值服务(Bean)”把「解析 + 求值」封装成一个服务,接收公式字符串和变量表,返回结果。业务层只依赖这个服务,不关心树的结构。
package com.example.demo.interpreter;
import org.springframework.stereotype.Service;import java.util.Map;
@Servicepublic 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); }}6.6 在接口或别的 Bean 里用
Section titled “6.6 在接口或别的 Bean 里用”例如某个配置或报表接口需要「按公式算出一个值」:注入 ExpressionEvalService,传入用户填的公式和当前变量(如 rate、amount),拿到结果再继续后续逻辑。
package com.example.demo.interpreter;
import lombok.RequiredArgsConstructor;import org.springframework.web.bind.annotation.*;import java.util.Map;
@RestController@RequestMapping("/api/calc")@RequiredArgsConstructorpublic 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 提供,解析出树后只依赖 Expr 和 EvalContext,和具体业务解耦;若要支持除、括号或更多运算,只需扩展解析和对应的 Expr 实现即可。