你写的规则引擎 if-else 堆成山——解释器模式三行代码就能让表达式自己解析自己

你写的规则引擎 if-else 堆成山——解释器模式三行代码就能让表达式自己解析自己

有个真实的场景:产品经理说我们要做一个「动态规则」功能,运营可以在后台配规则,比如「注册超过 30 天 AND 消费金额 > 500 OR VIP 等级 >= 3」的用户自动发优惠券。

你心想:这不就是个 if-else 吗。于是:

java if (user.getRegisterDays() > 30) { if (user.getTotalAmount() > 500) { sendCoupon(user); } else if (user.getVipLevel() >= 3) { sendCoupon(user); } } else if (user.getVipLevel() >= 3) { sendCoupon(user); }

一周后产品说:「再加个条件:最近 7 天有登录。」你的 if-else 开始往右缩进。

两周后产品说:「再加个括号:"(A AND B) OR (C AND NOT D)"。」你开始写 SQL 拼接。

一个月后产品说:「能不能支持自定义函数?比如 distance(user.location, store.location) < 5km。」你看着那段已经三百行的 if-else,开始怀疑人生。

解释器模式没有你想的那么玄

解释器模式在 GoF 书里占的篇幅不长,但代码是最难读的。不是因为概念难,是因为所有教程一上来就教你怎么写 BNF 范式、怎么画语法树、怎么实现终结符和非终结符。看完你就觉得这东西属于大学编译原理课,跟我写业务有什么关系。

我用一个你每天都接触的东西来解释:SQL

你写 SELECT * FROM users WHERE age > 18 AND city = 'Beijing',MySQL 怎么理解这行字符串?它需要一个「解释器」来把这段文本翻译成它能执行的指令。这个解释器里面用了词法分析(把字符串拆成 token)、语法分析(把 token 组织成树)、语义分析(检查表名和字段名是否存在),最后生成执行计划。

解释器模式的本质:为一种语言定义文法,然后写一个解释器来执行该语言表示的句子

这里的「语言」不是编程语言,而是任何有规则的字符串表达方式——SQL、正则表达式、数学公式、JSONPath、SpEL,甚至是你产品经理定义的「规则表达」。

一个能跑的解释器,50 行代码就够了

先放下书上那些抽象类图。我们直接实现一个最简单的算术表达式解释器,支持加减乘除和括号:

```java // Token 定义 record Token(TokenType type, String value) {} enum TokenType { NUMBER, PLUS, MINUS, MULTIPLY, DIVIDE, LPAREN, RPAREN, EOF }

// 词法分析器:把字符串拆成 Token 流 class Lexer { private final String input; private int pos = 0;

Lexer(String input) { this.input = input; }

Token nextToken() {
    while (pos < input.length() && Character.isWhitespace(input.charAt(pos))) pos++;
    if (pos >= input.length()) return new Token(TokenType.EOF, "");

    char c = input.charAt(pos);
    if (Character.isDigit(c)) {  // 数字
        int start = pos;
        while (pos < input.length() && Character.isDigit(input.charAt(pos))) pos++;
        return new Token(TokenType.NUMBER, input.substring(start, pos));
    }
    // 运算符和括号
    pos++;
    return switch (c) {
        case '+' -> new Token(TokenType.PLUS, "+");
        case '-' -> new Token(TokenType.MINUS, "-");
        case '*' -> new Token(TokenType.MULTIPLY, "*");
        case '/' -> new Token(TokenType.DIVIDE, "/");
        case '(' -> new Token(TokenType.LPAREN, "(");
        case ')' -> new Token(TokenType.RPAREN, ")");
        default -> throw new IllegalArgumentException("非法字符: " + c);
    };
}

} ```

词法分析器做了把字符串变成 Token 这一件事。你给 "3 + 5 * 2",它返回 [NUMBER(3), PLUS, NUMBER(5), MULTIPLY, NUMBER(2), EOF]

接下来是语法分析。我们用递归下降,每个文法规则对应一个方法:

```java class Parser { private final Lexer lexer; private Token currentToken;

Parser(Lexer lexer) {
    this.lexer = lexer;
    this.currentToken = lexer.nextToken();
}

// 表达式 → 项 (('+' | '-') 项)*
int parseExpression() {
    int result = parseTerm();
    while (currentToken.type() == TokenType.PLUS || currentToken.type() == TokenType.MINUS) {
        Token op = currentToken;
        eat(op.type());
        int right = parseTerm();
        result = op.type() == TokenType.PLUS ? result + right : result - right;
    }
    return result;
}

// 项 → 因子 (('*' | '/') 因子)*
int parseTerm() {
    int result = parseFactor();
    while (currentToken.type() == TokenType.MULTIPLY || currentToken.type() == TokenType.DIVIDE) {
        Token op = currentToken;
        eat(op.type());
        int right = parseFactor();
        result = op.type() == TokenType.MULTIPLY ? result * right : result / right;
    }
    return result;
}

// 因子 → NUMBER | '(' 表达式 ')'
int parseFactor() {
    if (currentToken.type() == TokenType.NUMBER) {
        int value = Integer.parseInt(currentToken.value());
        eat(TokenType.NUMBER);
        return value;
    }
    if (currentToken.type() == TokenType.LPAREN) {
        eat(TokenType.LPAREN);
        int result = parseExpression();
        eat(TokenType.RPAREN);
        return result;
    }
    throw new IllegalArgumentException("意外的 token: " + currentToken);
}

void eat(TokenType type) {
    if (currentToken.type() == type) currentToken = lexer.nextToken();
    else throw new IllegalArgumentException("期望 " + type + ",实际 " + currentToken.type());
}

} ```

验证:

java int result = new Parser(new Lexer("(3 + 5) * 2 - 10 / 2")).parseExpression(); // result = 11 即 (8) * 2 - 5 = 16 - 5 = 11

50 行代码实现了一个支持四则运算和括号的解释器。解释器模式不是什么神秘的东西,就是把「解析规则」从「执行逻辑」里拆出来。

SpEL:Spring 对解释器模式最务实的应用

Spring 表达式语言(SpEL)就是解释器模式在业务系统里最典型的落地。你写过这个:

```java @Value("#{systemProperties['user.dir']}") private String userDir;

@PreAuthorize("hasRole('ADMIN') and #user.id == authentication.principal.id") public void deleteUser(User user) { ... }

// 动态查询条件 ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression("name == '张三' and age > 18"); boolean result = exp.getValue(context, Boolean.class); ```

SpEL 把你写的那段文本 "name == '张三' and age > 18" 解析成抽象语法树,然后在运行时执行。你不需要自己写词法分析和语法分析——Spring 帮你做了。

回到开头产品经理那个需求,真正的解法不是写 if-else,而是:

java ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression(ruleStringFromDatabase); boolean shouldSend = exp.getValue(userContext, Boolean.class); if (shouldSend) { sendCoupon(user); }

规则存在数据库里,运营在后台配置,你一行代码都不用改。这就是解释器模式让你不用改代码就能扩展「语言」的威力。

但有个重要的提醒:不要直接让用户输入的字符串变成 SpEL 表达式然后执行。SpEL 能调用方法、访问静态字段、创建对象。T(java.lang.Runtime).getRuntime().exec('rm -rf /') 这种恶意表达式如果被执行,整个服务器就没了。

安全的做法: 1. 限制 SpEL 的能力(用 SimpleEvaluationContext 替代默认的 StandardEvaluationContext) 2. 只暴露你允许的变量和方法 3. 如果规则语法是给外部用户用的,自己写个更受限的解释器

解释器模式的边界

解释器模式最大的弱点:文法越复杂,类就越多。一个完整的 SQL 解析器可能需要几百个类,正则表达式引擎的语法树深度可能让递归爆栈。所以 GoF 书上明确说了——只适合简单的文法

复杂的文法怎么办?用现成的。ANTLR、JavaCC 这种编译器生成工具,你定义文法规则,它帮你生成解释器代码。但那是另一篇文章的事了。

重要的是你怎么选: - 简单的表达式(数学、布尔逻辑、简单规则)→ 手写递归下降解释器,50-200 行代码 - 中等复杂(类 SQL 查询、业务规则引擎)→ 用 SpEL 或 MVEL - 复杂文法(完整编程语言、SQL 全量解析)→ ANTLR / JavaCC

解释器模式最被低估的价值不是「让你能写自己的解释器」,而是「让你理解 Spring 的 @Value、MyBatis 的 OGNL、Security 的 SpEL 权限表达式到底在干什么」。你不需要自己写一个,但你必须认识它。


说起来,我在做一个叫「爪爪代码冒险记」的微信小程序,把 23 个设计模式用卡皮巴拉漫画的方式讲。解释器模式这一章就用了一个规则引擎从 if-else 变成表达式驱动的真实案例。你要是觉得这类内容有用,微信搜一下「爪爪代码冒险记」就能找到。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值