设计模式详解-解释器模式

设计模式详解:解释器模式

一、模式概述

解释器模式(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算法,高性能
学习成本 中(自定义) 高(专用工具)

三大陷阱:

  1. 文法复杂度过高 → 超过20个规则时改用ANTLR或嵌入Groovy/Lua
  2. 性能瓶颈 → 引入表达式缓存、JIT编译、字节码生成
  3. 错误诊断困难 → 保留源码位置信息,实现详细错误堆栈

五、结语

解释器模式的精髓在于承认自然语言与业务规则的表达力优势,以形式化语言的精确性换取系统的灵活性与可维护性。它将变化频繁的业务规则从代码中解放,转化为可配置、可审阅、可版本控制的声明式语言。掌握递归下降解析、AST求值、访问者增强等核心技术,警惕文法复杂度与性能瓶颈,是构建企业级规则引擎的能力核心。

返回知识中心