设计模式详解-解释器模式
设计模式详解:解释器模式
一、模式概述
解释器模式(Interpreter Pattern)是行为型设计模式中最具理论深度与领域建模价值的模式,其核心意图在于为特定语言定义文法表示,并构建解释器来解释该语言中的句子。该模式将问题域抽象为自定义语言(DSL),通过构建抽象语法树(AST)实现"代码即数据"的范式转换,使复杂规则从硬编码程序中解放出来,转化为可配置、可扩展的声明式结构。
解释器模式的深层价值在于将变化频繁的业务逻辑从代码中剥离,下沉为数据层面的配置。当业务规则需要频繁调整时,传统方式要求修改代码、编译、测试、部署,周期冗长且风险累积;而基于解释器模式的规则系统,仅需修改规则文本即可生效。在促销规则引擎、工作流条件、权限表达式、监控告警规则等场景中,解释器模式是支撑业务自助化的关键架构模式。
二、模式结构
解释器模式包含四个核心角色:
| 角色 | 职责 | 对应AST节点 |
|---|---|---|
| 抽象表达式 | 声明统一的interpret(Context)接口 |
节点基类 |
| 终结符表达式 | 文法原子符号,不可再分 | 叶子节点 |
| 非终结符表达式 | 复合规则,由子表达式组合 | 内部节点 |
| 上下文 | 变量绑定、函数定义、运行时状态 | 遍历状态载体 |
关键特征在于语法规则与执行语义分离。文法定义合法结构,表达式类实现计算逻辑,上下文承载运行时环境。
三、深度案例:电商促销规则引擎
3.1 问题域分析
java
// 反模式:促销规则硬编码
@Service
public class NaivePromotionService {
public DiscountResult calculate(Order order, List<Promotion> promotions) {
BigDecimal total = BigDecimal.ZERO;
for (Promotion p : promotions) {
boolean ok = false;
if ("FIXED_AMOUNT_OFF".equals(p.getType())) {
if (order.getTotalAmount().compareTo(new BigDecimal("100")) >= 0) ok = true;
} else if ("MEMBER_DISCOUNT".equals(p.getType())) {
if ("VIP".equals(order.getUser().getMemberLevel())) ok = true;
}
// 每新增规则修改此处,条件组合指数级膨胀
if (ok) total = total.add(p.apply(order));
}
return new DiscountResult(total);
}
}
规则与引擎深度耦合,新增规则需改核心代码,补偿逻辑散落各处。解释器模式通过DSL将定义、存储、解释、执行彻底分离。
3.2 文法定义(BNF)
text
<expression> ::= <or_expr>
<or_expr> ::= <and_expr> ( "OR" <and_expr> )*
<and_expr> ::= <not_expr> ( "AND" <not_expr> )*
<not_expr> ::= "NOT" <not_expr> | <comparison>
<comparison> ::= <arithmetic> ( ("=="|"!="|">"|">="|"<"|"<=") <arithmetic>
| "IN" <list> | "BETWEEN" <arithmetic> "AND" <arithmetic> )
<arithmetic> ::= <term> ( ("+"|"-") <term> )*
<term> ::= <factor> ( ("*"|"/"|"%") <factor> )*
<factor> ::= <number> | <string> | <boolean> | <identifier>
| <function> | "(" <expression> ")"
<function> ::= <identifier> "(" <arg_list>? ")"
<list> ::= "[" <expression> ( "," <expression> )* "]"
3.3 核心实现
抽象表达式与上下文:
java
public interface Expression {
Value interpret(EvaluationContext ctx);
String toExpressionString();
}
public class EvaluationContext {
private final Map<String, Value> vars = new HashMap<>();
private final Map<String, FunctionDef> funcs = new HashMap<>();
public Value resolvePath(String path) {
String[] segs = path.split("\\.");
Value cur = vars.get(segs[0]);
for (int i = 1; i < segs.length && cur != null; i++) {
cur = cur.isMap() ? cur.asMap().getOrDefault(segs[i], Value.NULL) : Value.NULL;
}
return cur != null ? cur : Value.NULL;
}
public void bind(String name, Value v) { vars.put(name, v); }
public void register(String name, FunctionDef f) { funcs.put(name, f); }
public FunctionDef resolveFunc(String name) { return funcs.get(name); }
}
值对象(统一类型系统):
java
public class Value {
public enum Type { NUMBER, STRING, BOOLEAN, DATETIME, LIST, MAP, NULL }
private final Object raw;
private final Type type;
public static Value num(BigDecimal v) { return new Value(v, Type.NUMBER); }
public static Value str(String v) { return new Value(v, Type.STRING); }
public static Value bool(boolean v) { return new Value(v, Type.BOOLEAN); }
public static Value list(List<Value> v) { return new Value(v, Type.LIST); }
public static Value map(Map<String, Value> v) { return new Value(v, Type.MAP); }
public static final Value NULL = new Value(null, Type.NULL);
public BigDecimal asNum() { return (BigDecimal) raw; }
public String asStr() { return raw == null ? "null" : raw.toString(); }
public boolean asBool() {
return switch(type) {
case BOOLEAN -> (Boolean) raw;
case NUMBER -> asNum().compareTo(BigDecimal.ZERO) != 0;
case STRING -> !asStr().isEmpty();
default -> false;
};
}
@SuppressWarnings("unchecked")
public List<Value> asList() { return (List<Value>) raw; }
@SuppressWarnings("unchecked")
public Map<String, Value> asMap() { return (Map<String, Value>) raw; }
public boolean isNull() { return type == Type.NULL; }
public boolean isMap() { return type == Type.MAP; }
public Type type() { return type; }
}
终结符表达式:
java
public class NumberLiteral implements Expression {
private final BigDecimal value;
public NumberLiteral(BigDecimal v) { this.value = v; }
public Value interpret(EvaluationContext ctx) { return Value.num(value); }
public String toExpressionString() { return value.toPlainString(); }
}
public class StringLiteral implements Expression {
private final String value;
public StringLiteral(String v) { this.value = v; }
public Value interpret(EvaluationContext ctx) { return Value.str(value); }
public String toExpressionString() { return "\"" + value + "\""; }
}
public class VariableRef implements Expression {
private final String path;
public VariableRef(String path) { this.path = path; }
public Value interpret(EvaluationContext ctx) { return ctx.resolvePath(path); }
public String toExpressionString() { return path; }
}
非终结符表达式(逻辑与算术):
java
public class AndExpr implements Expression {
private final List<Expression> operands;
public AndExpr(List<Expression> ops) { this.operands = ops; }
public Value interpret(EvaluationContext ctx) {
for (Expression e : operands) {
if (!e.interpret(ctx).asBool()) return Value.bool(false);
}
return Value.bool(!operands.isEmpty());
}
public String toExpressionString() {
return "(" + operands.stream().map(Expression::toExpressionString)
.collect(Collectors.joining(" AND ")) + ")";
}
}
public class ComparisonExpr implements Expression {
public enum Op { EQ("=="), NE("!="), GT(">"), GE(">="), LT("<"), LE("<=");
public final String sym; Op(String s) { this.sym = s; }
}
private final Expression left, right;
private final Op op;
public ComparisonExpr(Expression l, Op op, Expression r) {
this.left = l; this.op = op; this.right = r;
}
public Value interpret(EvaluationContext ctx) {
Value lv = left.interpret(ctx), rv = right.interpret(ctx);
if (lv.isNull() || rv.isNull()) return handleNull(lv, rv);
boolean res = switch(op) {
case EQ -> compareEqual(lv, rv);
case NE -> !compareEqual(lv, rv);
case GT -> compareOrder(lv, rv) > 0;
case GE -> compareOrder(lv, rv) >= 0;
case LT -> compareOrder(lv, rv) < 0;
case LE -> compareOrder(lv, rv) <= 0;
};
return Value.bool(res);
}
private boolean compareEqual(Value a, Value b) {
if (a.type() != b.type()) {
if (a.type() == Type.NUMBER && b.type() == Type.STRING) {
try { return a.asNum().compareTo(new BigDecimal(b.asStr())) == 0; } catch (Exception e) { return false; }
}
return false;
}
return switch(a.type()) {
case NUMBER -> a.asNum().compareTo(b.asNum()) == 0;
case STRING -> a.asStr().equals(b.asStr());
case BOOLEAN -> a.asBool() == b.asBool();
default -> false;
};
}
private int compareOrder(Value a, Value b) {
if (a.type() != b.type()) throw new TypeMismatchException();
return switch(a.type()) {
case NUMBER -> a.asNum().compareTo(b.asNum());
case STRING -> a.asStr().compareTo(b.asStr());
default -> throw new TypeMismatchException();
};
}
private Value handleNull(Value a, Value b) {
return switch(op) {
case EQ -> Value.bool(a.isNull() && b.isNull());
case NE -> Value.bool(a.isNull() != b.isNull());
default -> Value.bool(false);
};
}
public String toExpressionString() {
return left.toExpressionString() + " " + op.sym + " " + right.toExpressionString();
}
}
public class ArithmeticExpr implements Expression {
public enum Op { ADD("+"), SUB("-"), MUL("*"), DIV("/"), MOD("%");
public final String sym; Op(String s) { this.sym = s; }
}
private final Expression left, right;
private final Op op;
public ArithmeticExpr(Expression l, Op op, Expression r) {
this.left = l; this.op = op; this.right = r;
}
public Value interpret(EvaluationContext ctx) {
Value lv = left.interpret(ctx), rv = right.interpret(ctx);
// 字符串拼接
if (op == Op.ADD && (lv.type() == Type.STRING || rv.type() == Type.STRING)) {
return Value.str(lv.asStr() + rv.asStr());
}
BigDecimal ln = lv.asNum(), rn = rv.asNum();
BigDecimal res = switch(op) {
case ADD -> ln.add(rn);
case SUB -> ln.subtract(rn);
case MUL -> ln.multiply(rn);
case DIV -> rn.compareTo(BigDecimal.ZERO) == 0 ?
throw new EvaluationException("Divide by zero") :
ln.divide(rn, 10, RoundingMode.HALF_UP);
case MOD -> ln.remainder(rn);
};
return Value.num(res);
}
public String toExpressionString() {
return "(" + left.toExpressionString() + " " + op.sym + " " + right.toExpressionString() + ")";
}
}
函数调用与内置函数:
java
public class FunctionCall implements Expression {
private final String name;
private final List<Expression> args;
public FunctionCall(String name, List<Expression> args) {
this.name = name; this.args = args;
}
public Value interpret(EvaluationContext ctx) {
FunctionDef fn = ctx.resolveFunc(name);
if (fn == null) throw new EvaluationException("Unknown function: " + name);
List<Value> vals = args.stream().map(a -> a.interpret(ctx)).toList();
return fn.invoke(vals);
}
public String toExpressionString() {
return name + "(" + args.stream().map(Expression::toExpressionString)
.collect(Collectors.joining(", ")) + ")";
}
}
public interface FunctionDef {
String name();
Value invoke(List<Value> args);
}
@Component
public class BuiltInFunctions {
public Map<String, FunctionDef> all() {
Map<String, FunctionDef> m = new HashMap<>();
m.put("ABS", args -> Value.num(args.get(0).asNum().abs()));
m.put("MAX", args -> Value.num(args.stream()
.map(Value::asNum).max(BigDecimal::compareTo).orElse(BigDecimal.ZERO)));
m.put("NOW", args -> Value.of(LocalDateTime.now())); // 需扩展Value支持
m.put("DAYS_DIFF", args -> {
long days = ChronoUnit.DAYS.between(
(LocalDateTime)args.get(0).raw, (LocalDateTime)args.get(1).raw);
return Value.num(BigDecimal.valueOf(days));
});
m.put("CONTAINS", args -> {
List<Value> list = args.get(0).asList();
Value target = args.get(1);
return Value.bool(list.stream().anyMatch(v -> compareEqual(v, target)));
});
m.put("COALESCE", args -> args.stream().filter(v -> !v.isNull()).findFirst().orElse(Value.NULL));
return m;
}
private boolean compareEqual(Value a, Value b) {
// 简化实现
return a.type() == b.type() && a.asStr().equals(b.asStr());
}
}
3.4 解析器:DSL → AST
java
public class Lexer {
private final String input;
private int pos;
public Lexer(String input) { this.input = input; }
public List<Token> tokenize() {
List<Token> tokens = new ArrayList<>();
while (pos < input.length()) {
skipSpace();
if (pos >= input.length()) break;
tokens.add(next());
}
tokens.add(new Token(TokenType.EOF, ""));
return tokens;
}
private Token next() {
char c = peek();
if (isIdStart(c)) return readIdOrKw();
if (isDigit(c)) return readNum();
if (c == '"' || c == '\'') return readStr(c);
return switch(c) {
case '(' -> advance("(");
case ')' -> advance(")");
case '[' -> advance("[");
case ']' -> advance("]");
case ',' -> advance(",");
case '+' -> advance("+");
case '-' -> advance("-");
case '*' -> advance("*");
case '/' -> advance("/");
case '%' -> advance("%");
case '=' -> matchNext('=', "==", "=");
case '!' -> matchNext('=', "!=", "!");
case '>' -> matchNext('=', ">=", ">");
case '<' -> matchNext('=', "<=", "<");
default -> throw new LexicalException("Unexpected: " + c);
};
}
private Token readIdOrKw() {
StringBuilder sb = new StringBuilder();
while (pos < input.length() && isIdPart(peek())) sb.append(advance());
String s = sb.toString();
return switch(s.toUpperCase()) {
case "AND" -> new Token(TokenType.AND, s);
case "OR" -> new Token(TokenType.OR, s);
case "NOT" -> new Token(TokenType.NOT, s);
case "IN" -> new Token(TokenType.IN, s);
case "BETWEEN" -> new Token(TokenType.BETWEEN, s);
case "TRUE" -> new Token(TokenType.BOOLEAN, "true");
case "FALSE" -> new Token(TokenType.BOOLEAN, "false");
case "NULL" -> new Token(TokenType.NULL, s);
default -> new Token(TokenType.IDENT, s);
};
}
private Token readNum() {
StringBuilder sb = new StringBuilder();
while (pos < input.length() && (isDigit(peek()) || peek() == '.')) sb.append(advance());
return new Token(TokenType.NUMBER, sb.toString());
}
private Token readStr(char quote) {
advance(); // skip opening
StringBuilder sb = new StringBuilder();
while (peek() != quote) {
if (peek() == '\\') { advance(); sb.append(escape(advance())); }
else sb.append(advance());
}
advance(); // skip closing
return new Token(TokenType.STRING, sb.toString());
}
// helpers...
private char peek() { return pos < input.length() ? input.charAt(pos) : '\0'; }
private char advance() { return input.charAt(pos++); }
private Token advance(String s) { pos++; return new Token(TokenType.symbolOf(s), s); }
private Token matchNext(char expected, String yes, String no) {
pos++;
if (peek() == expected) { pos++; return new Token(TokenType.symbolOf(yes), yes); }
return new Token(TokenType.symbolOf(no), no);
}
private void skipSpace() { while (pos < input.length() && Character.isWhitespace(peek())) pos++; }
private boolean isIdStart(char c) { return Character.isLetter(c) || c == '_'; }
private boolean isIdPart(char c) { return Character.isLetterOrDigit(c) || c == '_' || c == '.'; }
private boolean isDigit(char c) { return Character.isDigit(c); }
private char escape(char c) { return c == 'n' ? '\n' : c == 't' ? '\t' : c; }
}
public class Parser {
private final List<Token> tokens;
private int pos;
public Parser(List<Token> tokens) { this.tokens = tokens; }
public Expression parse() {
Expression e = parseOr();
if (!match(TokenType.EOF)) throw new ParseException("Unexpected trailing tokens");
return e;
}
private Expression parseOr() {
Expression left = parseAnd();
while (match(TokenType.OR)) {
Expression right = parseAnd();
if (left instanceof OrExpr) ((OrExpr)left).add(right);
else left = new OrExpr(new ArrayList<>(List.of(left, right)));
}
return left;
}
private Expression parseAnd() {
Expression left = parseNot();
while (match(TokenType.AND)) {
Expression right = parseNot();
if (left instanceof AndExpr) ((AndExpr)left).add(right);
else left = new AndExpr(new ArrayList<>(List.of(left, right)));
}
return left;
}
private Expression parseNot() {
if (match(TokenType.NOT)) return new NotExpr(parseNot());
return parseComparison();
}
private Expression parseComparison() {
Expression left = parseArithmetic();
if (match(TokenType.EQ_EQ)) return new ComparisonExpr(left, ComparisonExpr.Op.EQ, parseArithmetic());
if (match(TokenType.NOT_EQ)) return new ComparisonExpr(left, ComparisonExpr.Op.NE, parseArithmetic());
if (match(TokenType.GT)) return new ComparisonExpr(left, ComparisonExpr.Op.GT, parseArithmetic());
if (match(TokenType.GE)) return new ComparisonExpr(left, ComparisonExpr.Op.GE, parseArithmetic());
if (match(TokenType.LT)) return new ComparisonExpr(left, ComparisonExpr.Op.LT, parseArithmetic());
if (match(TokenType.LE)) return new ComparisonExpr(left, ComparisonExpr.Op.LE, parseArithmetic());
if (match(TokenType.IN)) return new InExpr(left, parseList());
if (match(TokenType.BETWEEN)) {
Expression low = parseArithmetic();
consume(TokenType.AND);
return new BetweenExpr(left, low, parseArithmetic(), true);
}
return left;
}
private Expression parseArithmetic() {
Expression left = parseTerm();
while (true) {
if (match(TokenType.PLUS)) left = new ArithmeticExpr(left, ArithmeticExpr.Op.ADD, parseTerm());
else if (match(TokenType.MINUS)) left = new ArithmeticExpr(left, ArithmeticExpr.Op.SUB, parseTerm());
else break;
}
return left;
}
private Expression parseTerm() {
Expression left = parseFactor();
while (true) {
if (match(TokenType.STAR)) left = new ArithmeticExpr(left, ArithmeticExpr.Op.MUL, parseFactor());
else if (match(TokenType.SLASH)) left = new ArithmeticExpr(left, ArithmeticExpr.Op.DIV, parseFactor());
else if (match(TokenType.PERCENT)) left = new ArithmeticExpr(left, ArithmeticExpr.Op.MOD, parseFactor());
else break;
}
return left;
}
private Expression parseFactor() {
if (match(TokenType.LPAREN)) {
Expression e = parseOr();
consume(TokenType.RPAREN);
return e;
}
if (match(TokenType.NUMBER)) return new NumberLiteral(new BigDecimal(prev().value));
if (match(TokenType.STRING)) return new StringLiteral(prev().value);
if (match(TokenType.BOOLEAN)) return new BooleanLiteral(Boolean.parseBoolean(prev().value));
if (match(TokenType.NULL)) return new LiteralExpr(Value.NULL);
if (match(TokenType.LBRACKET)) {
List<Expression> elems = new ArrayList<>();
if (!check(TokenType.RBRACKET)) {
do { elems.add(parseOr()); } while (match(TokenType.COMMA));
}
consume(TokenType.RBRACKET);
return new ListLiteral(elems);
}
if (match(TokenType.IDENT)) {
String name = prev().value;
if (match(TokenType.LPAREN)) {
List<Expression> args = new ArrayList<>();
if (!check(TokenType.RPAREN)) {
do { args.add(parseOr()); } while (match(TokenType.COMMA));
}
consume(TokenType.RPAREN);
return new FunctionCall(name, args);
}
return new VariableRef(name);
}
throw new ParseException("Unexpected: " + current());
}
private Expression parseList() {
if (!check(TokenType.LBRACKET)) return parseOr(); // 子表达式作为列表
consume(TokenType.LBRACKET);
List<Expression> elems = new ArrayList<>();
if (!check(TokenType.RBRACKET)) {
do { elems.add(parseOr()); } while (match(TokenType.COMMA));
}
consume(TokenType.RBRACKET);
return new ListLiteral(elems);
}
// helpers
private boolean match(TokenType t) { if (check(t)) { pos++; return true; } return false; }
private boolean check(TokenType t) { return current().type == t; }
private Token current() { return tokens.get(pos); }
private Token prev() { return tokens.get(pos - 1); }
private void consume(TokenType t) { if (!match(t)) throw new ParseException("Expected " + t); }
}
3.5 规则引擎集成
java
@Component
public class PromotionEngine {
@Autowired private BuiltInFunctions functions;
private final Cache<String, Expression> cache = Caffeine.newBuilder()
.maximumSize(10000).expireAfterWrite(Duration.ofHours(1)).build();
public boolean evaluate(String dsl, Map<String, Object> data) {
Expression ast = cache.get(dsl, k -> {
List<Token> tokens = new Lexer(k).tokenize();
return new Parser(tokens).parse();
});
EvaluationContext ctx = new EvaluationContext();
functions.all().forEach(ctx::register);
ctx.bind("order", convertToValue(data));
return ast.interpret(ctx).asBool();
}
private Value convertToValue(Object obj) {
if (obj instanceof Map m) {
Map<String, Value> res = new HashMap<>();
m.forEach((k, v) -> res.put(k.toString(), convertToValue(v)));
return Value.map(res);
}
if (obj instanceof Collection c) {
return Value.list((List<Value>) c.stream().map(this::convertToValue).toList());
}
if (obj instanceof BigDecimal bd) return Value.num(bd);
if (obj instanceof Number n) return Value.num(new BigDecimal(n.toString()));
if (obj instanceof String s) return Value.str(s);
if (obj instanceof Boolean b) return Value.bool(b);
return Value.NULL;
}
}
运营配置的DSL规则示例:
java
public class RuleSamples {
// VIP满100减20
public static final String R1 = "order.user.level IN ['VIP','SVIP'] AND order.amount >= 100";
// 新用户首单(7天内注册)
public static final String R2 = "order.user.orders == 0 AND DAYS_DIFF(order.user.regTime, NOW()) <= 7";
// 双11 3C满500
public static final String R3 =
"order.time BETWEEN '2024-11-01T00:00:00' AND '2024-11-11T23:59:59' AND " +
"CONTAINS(order.items[*].category, '3C') AND order.amount >= 500";
// 复杂组合:高价值用户买苹果,或SVIP任意消费
public static final String R4 =
"(order.user.spent > 10000 AND CONTAINS(order.items[*].brand, 'APPLE')) " +
"OR order.user.level == 'SVIP'";
}
四、模式辨析与陷阱
| 对比维度 | 解释器模式 | 策略模式 | 规则引擎(Drools) |
|---|---|---|---|
| 规则表达 | DSL文本 | 代码类 | 专用规则语言 |
| 修改方式 | 改文本 | 改代码重编译 | 改规则文件 |
| 适用复杂度 | 简单-中等文法 | 算法替换 | 复杂业务规则 |
| 性能 | AST遍历,可优化 | 直接调用 | Rete算法,高性能 |
| 学习成本 | 中(自定义) | 低 | 高(专用工具) |
三大陷阱:
- 文法复杂度过高 → 超过20个规则时改用ANTLR或嵌入Groovy/Lua
- 性能瓶颈 → 引入表达式缓存、JIT编译、字节码生成
- 错误诊断困难 → 保留源码位置信息,实现详细错误堆栈
五、结语
解释器模式的精髓在于承认自然语言与业务规则的表达力优势,以形式化语言的精确性换取系统的灵活性与可维护性。它将变化频繁的业务规则从代码中解放,转化为可配置、可审阅、可版本控制的声明式语言。掌握递归下降解析、AST求值、访问者增强等核心技术,警惕文法复杂度与性能瓶颈,是构建企业级规则引擎的能力核心。