[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"site-settings":3,"\u002Farticles\u002Fdesign-pattern-25":26,"\u002Farticles\u002Fdesign-pattern-25\u002Frelated":43,"\u002Farticles\u002Fdesign-pattern-25\u002Fadjacent":72,"\u002Fcategories":84,"render-design-pattern-25":110},{"code":4,"message":5,"data":6},0,"ok",{"author_city":7,"author_years":8,"site_tagline":9,"site_description":10,"skill_one_desc":11,"author_status":12,"skill_two_title":13,"about_hero_image":9,"skill_one_title":14,"icp":15,"author_role":16,"og_default_image":9,"closing_quote_date":17,"toolkit":18,"skill_two_desc":19,"author_bio":9,"site_footer":9,"site_url":20,"author_tagline":21,"weekly_thought":9,"tagline_line_one":22,"author_bio_two":9,"closing_quote":23,"site_name":24,"tagline_line_two":25,"home_hero_image":9},"西安","10","","花开空白 - 个人博客","熟悉Java、Python、Go等后端语言。","努力工作中","前端开发","后端开发","陕ICP备2021009682号-1","开发者","2022年·秋","Java,JavaScript,TypeScript,Python,Go","熟悉Js、Ts语言以及Vue、uniapp等框架。","https:\u002F\u002Firule.cn","很高兴遇见你。","把复杂的事，","把每次一发布，都当成一次和陌生人的握手。","花开空白","写成简单的故事。",{"code":4,"message":5,"data":27},{"id":28,"slug":29,"title":30,"summary":31,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":34,"likeCount":4,"wordCount":35,"readingMinutes":36,"publishedAt":37,"contentMd":38,"contentHtml":39,"tags":40,"metaTitle":9,"metaDescription":9,"keywords":9,"canonicalUrl":9,"robots":41,"ogImage":9,"faq":42,"aiSummary":42},"2113934683157827585","design-pattern-25","设计模式修炼之路 ㉕ 解释器模式——让程序读懂规则","GoF 收官、规则即数据零发版、文法即代码、递归下降解析器、短路求值白送、解释 vs 编译 vs 配置表","2113897628365230081",1,3389,8660,22,"2025-01-19T13:11:38","> 行为型第十二站（终章）。\n\n㉔ 的文档能把结构导出成 Markdown 文本——那是\"结构到文本\"的单行道。这一篇开反向车道：给程序一段文本，比如促销规则 `\"vip && age >= 18\"`，让它自己解析出结构、算出 true \u002F false。\n\n先看痛点。促销规则是全公司变得最勤的东西：周一\"VIP 且成年可买\"，周三\"新用户零单送券\"，周五运营半夜拍脑袋\"60 岁以上或 VIP 打八折\"。规则硬编码在 Java 里，意味着**每改一条规则都要走完提需求 → 排期 → 开发 → 测试 → 发版的流水线**——程序员在给运营当人肉规则编辑器。\n\n**把规则写成一行字符串，让程序自己读懂它、算出结果**——自己实现一个小规则解释器——解释器模式，GoF 二十三式的最后一位，压轴登场。\n\n## 一、解释器模式是什么\n\nGoF 的原始定义：\n\n> Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language.\n> （给定一门语言，定义它的文法的一种表示，并定义一个解释器，该解释器使用该表示来解释语言中的句子。）\n\n拆开读，定义里藏着三件事：先发明一门**小语言**（本篇的规则语法：`vip && age >= 18`）；再给这门语言一个**文法表示**（把字符串解析成语法树）；最后让**解释器**拿着语法树对具体数据求值。所谓\"解释\"，就是**递归地走一遍这棵树**。\n\n五个角色：\n\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>角色\u003C\u002Fth>\u003Cth>干什么\u003C\u002Fth>\u003Cth>本文对应\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\u003Ctr>\u003Ctd>AbstractExpression（抽象表达式）\u003C\u002Ftd>\u003Ctd>声明 interpret 操作\u003C\u002Ftd>\u003Ctd>Expression 接口\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>TerminalExpression（终结符表达式）\u003C\u002Ftd>\u003Ctd>语法树的叶子：直接查环境、算出结果\u003C\u002Ftd>\u003Ctd>FlagExpression \u002F ComparisonExpression\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>NonterminalExpression（非终结符表达式）\u003C\u002Ftd>\u003Ctd>语法树的枝：组合子表达式\u003C\u002Ftd>\u003Ctd>AndExpression \u002F OrExpression \u002F NotExpression\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Context（环境）\u003C\u002Ftd>\u003Ctd>解释现场，装着所有变量的值\u003C\u002Ftd>\u003Ctd>Map\\\u003CString, Object\\>\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Client（客户端）\u003C\u002Ftd>\u003Ctd>构建（解析）语法树并启动解释\u003C\u002Ftd>\u003Ctd>RuleParser + InterpreterTest\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\n\n眼熟吗？终结符是叶子、非终结符是枝干、统一接口递归求值——**解释器模式几乎总是组合模式的实例**：Expression 树就是 ⑨ 里那棵 Component 树，只是把\"display 挨个执行\"换成了\"interpret 求值\"。⑨ 的回头客，这次是真的要动手建树了。\n\n## 二、优点与缺点\n\n**优点：**\n\n1. **规则即数据**：加一条规则 = 传一个字符串，零代码、零编译、零发版——运营半夜上线，程序员的枕头是干的。\n2. **文法与代码一一对应**：一条文法规则一个类（或一个解析函数），看 BNF 就知道代码长什么样，改文法就是改那一处。\n3. **扩展运算符零侵入**：想支持 `>`、`\u003C` 之外再来个 `contains`？加一个终结符类 + 文法里添一笔，现有表达式类全部不动——开闭原则。\n4. **语法错误前置**：`\"vip && & age\"` 这种手滑，解析当场报\"位于 7\"，而不是默默跑出一个诡异的业务结果。\n\n**缺点：**\n\n1. **文法复杂则类爆炸**：本篇 `&&`、`||`、`!`、比较、括号就 6 个表达式类；再上算术、函数调用、字符串操作……类文件能铺满一屏。GoF 原文自己就劝退：**文法复杂时，别手写，用解析器生成器**（ANTLR 一类）。\n2. **性能有税**：每次求值都要递归走整棵树，每次解析都要从字符串建树。高频调用场景这笔账得算清楚（可缓存 AST 缓解，见第六节）。\n3. **等于开了个迷你编程接口**：规则文本若是外部可传入的，`\"a && a && a && ……（一万层）\"` 就能把你递归栈干爆——解释器天生要做长度与深度防护。\n4. **团队门槛**：出了 bug 得有人会看语法树、会读文法。全组只会 CRUD 的日子，一去不返。\n\n## 三、什么时候用\n\n判断标准：**文法简单且稳定、规则文本变化频繁、求值频率不至于性能敏感。**\n\n- **促销 \u002F 风控 \u002F 权限规则引擎**：规则天天变，语法十年不变——本文场景\n- **日志\u002F监控查询语言**：Kibana 的 Lucene 查询串、Grafana 的告警条件表达式\n- **模板与占位符**：`\"亲爱的${name}，您的订单${orderId}已发货\"`——最朴素的单遍解释器\n- **配置里的动态表达式**：开关平台\"满足条件的用户才灰度\"那串表达式\n\n反面对照：**规则只是参数在变**（满 100 减 20 变成满 200 减 50）——一张配置表就够，别上解释器，自由度过剩也是浪费；**规则复杂到本身就是业务**（信贷审批全流程）——那是业务代码，写 Java 去；**每秒百万次求值**——递归树撑不住，编译执行或者硬编码。\n\n## 四、实现方式：迷你规则引擎\n\n### 4.1 反面教材：规则硬编码\n\n每条规则一个 case：\n\n```java\npublic class BadRuleEngine {\n\n    public static boolean check(User user, String ruleName) {\n        switch (ruleName) {\n            case \"vip-adult\" -> { return user.isVip() && user.getAge() >= 18; }\n            case \"new-user\" -> { return !user.isVip() && user.getOrderCount() == 0; }\n            default -> throw new IllegalArgumentException(\n                    \"规则 \" + ruleName + \" 不存在：请提需求、排期、开发、测试、发版\");\n        }\n    }\n}\n```\n\n规则库里两条规则，第三条\"black-friday\"上线时，default 分支就是程序员的周末。更要命的是：**规则的本质是数据，却被写成了代码**——数据和代码的区别，就是\"改起来要不要发版\"的区别。\n\n### 4.2 正解：表达式树 + 递归下降解析器\n\n目标效果：规则是字符串，规则是数据：\n\n```java\nUser zhangsan = new User(true, 35, 42, 3);   \u002F\u002F vip、35 岁、42 单、level 3\nUser tianqi   = new User(false, 16, 0, 1);   \u002F\u002F 非 vip、16 岁、0 单、level 1\n\nMap\u003CString, Object> zhangsanCtx =\n        Map.of(\"vip\", true, \"age\", 35, \"orderCount\", 42, \"level\", 3);\n\nExpression rule = RuleParser.parse(\"(vip || level >= 3) && age >= 18\");\nrule.interpret(zhangsanCtx);   \u002F\u002F → true\n```\n\n**第一步：定义小语言。** 四条文法，用 BNF 写出来：\n\n```\norExpr   := andExpr ('||' andExpr)*      \u002F\u002F 或，优先级最低\nandExpr  := unary ('&&' unary)*          \u002F\u002F 与\nunary    := '!' unary | primary          \u002F\u002F 非\nprimary  := '(' orExpr ')' | 标识符 运算符 数字 | 标识符    \u002F\u002F 括号 \u002F 比较 \u002F 标志位\n```\n\n**第二步：表达式族。** 接口只有一行：\n\n```java\npublic interface Expression {\n    boolean interpret(Map\u003CString, Object> context);\n}\n```\n\n终结符两个。`vip` 这种标志位：\n\n```java\nclass FlagExpression implements Expression {\n    private final String name;\n    FlagExpression(String name) { this.name = name; }\n\n    @Override\n    public boolean interpret(Map\u003CString, Object> context) {\n        Object value = context.get(name);\n        if (value instanceof Boolean b) {\n            return b;\n        }\n        throw new IllegalArgumentException(\"属性 \" + name + \" 不是布尔值\");\n    }\n\n    @Override\n    public String toString() { return name; }\n}\n```\n\n`age >= 18` 这种比较：\n\n```java\nclass ComparisonExpression implements Expression {\n    private final String name;\n    private final String op;      \u002F\u002F \">\"、\">=\"、\"\u003C\"、\"\u003C=\"、\"==\"\n    private final int target;\n    \u002F\u002F 构造器略\n\n    @Override\n    public boolean interpret(Map\u003CString, Object> context) {\n        int actual = (Integer) context.get(name);\n        return switch (op) {\n            case \">\"  -> actual >  target;\n            case \">=\" -> actual >= target;\n            case \"\u003C\"  -> actual \u003C  target;\n            case \"\u003C=\" -> actual \u003C= target;\n            case \"==\" -> actual == target;\n            default -> throw new IllegalArgumentException(\"未知运算符 \" + op);\n        };\n    }\n\n    @Override\n    public String toString() { return name + \" \" + op + \" \" + target; }\n}\n```\n\n非终结符三个，全是一个模子：**抱着两棵（或一棵）子树，求值时递归下问**。以 AndExpression 为例：\n\n```java\nclass AndExpression implements Expression {\n    private final Expression left;\n    private final Expression right;\n\n    AndExpression(Expression left, Expression right) {\n        this.left = left;\n        this.right = right;\n    }\n\n    @Override\n    public boolean interpret(Map\u003CString, Object> context) {\n        return left.interpret(context) && right.interpret(context);\n    }\n\n    @Override\n    public String toString() { return \"(\" + left + \" && \" + right + \")\"; }\n}\n```\n\nOrExpression 同款换成 `||`；NotExpression 抱一棵子树返回 `!inner.interpret(context)`。至此，语法树的\"词汇表\"集齐——`\"(vip || level >= 3) && age >= 18\"` 解析出来是这么一棵树：\n\n```\nAndExpression\n├── OrExpression\n│   ├── FlagExpression(\"vip\")\n│   └── ComparisonExpression(\"level >= 3\")\n└── ComparisonExpression(\"age >= 18\")\n```\n\n**第三步：解析器，把文本变树。** RuleParser 是个迷你递归下降解析器，核心思路：**每条文法规则，翻译成一个解析函数**：\n\n```java\n\u002F**\n * 迷你递归下降解析器：把规则文本变成语法树。\n * 文法（优先级从低到高）：\n *   orExpr   := andExpr ('||' andExpr)*\n *   andExpr  := unary ('&&' unary)*\n *   unary    := '!' unary | primary\n *   primary  := '(' orExpr ')' | 标识符 运算符 数字 | 标识符\n * 每个文法规则对应一个解析函数，解析层次越深优先级越高。\n *\u002F\npublic class RuleParser {\n\n    private final String input;\n    private int pos;\n\n    public static Expression parse(String rule) { \u002F* 见下 *\u002F }\n\n    private Expression parseOr() {\n        Expression left = parseAnd();\n        while (match(\"||\")) {\n            left = new OrExpression(left, parseAnd());\n        }\n        return left;\n    }\n\n    private Expression parseAnd() {\n        Expression left = parseUnary();\n        while (match(\"&&\")) {\n            left = new AndExpression(left, parseUnary());\n        }\n        return left;\n    }\n\n    private Expression parseUnary() {\n        if (match(\"!\")) {\n            return new NotExpression(parseUnary());\n        }\n        return parsePrimary();\n    }\n\n    private Expression parsePrimary() {\n        if (match(\"(\")) {\n            Expression expression = parseOr();\n            expect(\")\");\n            return expression;\n        }\n        String name = readIdentifier();\n        skipWhitespace();\n        String op = tryReadOp();          \u002F\u002F 读得到运算符就是比较式\n        if (op == null) {\n            return new FlagExpression(name);   \u002F\u002F 读不到就是标志位\n        }\n        skipWhitespace();\n        return new ComparisonExpression(name, op, readInt());\n    }\n    \u002F\u002F match\u002Fexpect\u002FreadIdentifier\u002FreadInt 等词法小工具略\n}\n```\n\n`parse(\"vip && age >= 18\")` 走一遍：parseOr 调 parseAnd，parseAnd 读到 `vip`（FlagExpression），match(\"&&\") 命中，再读 `age >= 18`（ComparisonExpression），包成 AndExpression 还给 parseOr——树建好了。\n\n**第四步：收钱。** 跑测试：\n\n```java\nevaluate(\"vip && age >= 18\", zhangsanCtx, tianqiCtx);\nevaluate(\"!vip && orderCount == 0\", zhangsanCtx, tianqiCtx);\nevaluate(\"(vip || level >= 3) && age >= 18\", zhangsanCtx, tianqiCtx);\n\n\u002F\u002F 运营半夜上线新规则：\nevaluate(\"age >= 60 || vip\", zhangsanCtx, tianqiCtx);\n\nprivate static void evaluate(String rule, Map\u003CString, Object> ctx1, Map\u003CString, Object> ctx2) {\n    Expression expression = RuleParser.parse(rule);\n    System.out.println(\"  [规则] \\\"\" + rule + \"\\\"\");\n    System.out.println(\"    语法树：\" + expression);\n    System.out.println(\"    张三 → \" + expression.interpret(ctx1)\n            + \"，田七 → \" + expression.interpret(ctx2));\n}\n```\n\n运行输出：\n\n```\n========== 反面教材：规则硬编码在代码里 ==========\n  [vip-adult] 张三 → true\n  [new-user] 田七 → true\n  [翻车] 规则 black-friday 不存在：请提需求、排期、开发、测试、发版\n\n========== 正解：规则即文本，解释器读懂它 ==========\n  [规则] \"vip && age >= 18\"\n    语法树：(vip && age >= 18)\n    张三 → true，田七 → false\n  [规则] \"!vip && orderCount == 0\"\n    语法树：(!vip && orderCount == 0)\n    张三 → false，田七 → true\n  [规则] \"(vip || level >= 3) && age >= 18\"\n    语法树：((vip || level >= 3) && age >= 18)\n    张三 → true，田七 → false\n  —— 运营半夜上线新规则「age >= 60 || vip」：零代码、零发版，传个字符串就行 ——\n  [规则] \"age >= 60 || vip\"\n    语法树：(age >= 60 || vip)\n    张三 → true，田七 → false\n\n  [语法报错] 规则 \"vip && & age\" →\n    语法错误：期待属性名，位于 7\n```\n\n第四条规则上线时，程序员在睡觉。最后一行的语法报错也接住了手滑——**错误在解析期暴露，带着坐标**，而不是求值期给出一个莫名其妙的结果。\n\n### 4.3 三处细节\n\n**细节一：优先级 = 解析函数的嵌套深度。** 为什么 `vip || level >= 3 && age >= 18` 里 `&&` 比 `||` 结合得紧？因为 parseOr（处理 `||`）在最外层，它先调 parseAnd（处理 `&&`）——**越深层的函数，抱得越紧**。文法四行、函数四个、优先级三层，一张嵌套结构图就说明白，不需要任何\"优先级表\"这种额外数据结构。想让某个子表达式脱离默认优先级？`primary` 里的 `('(' orExpr ')')` 规则兜住括号，直接回到最外层重新解析——括号的本质，就是**人工指定解析层次**。\n\n**细节二：短路求值是白送的。** AndExpression 的求值就一行 `left.interpret(context) && right.interpret(context)`——右边复用了 Java 自己的 `&&`。左边 false，右边那棵子树**根本不会被碰**。解释器模式的\"递归求值\"天然对齐宿主语言的求值语义：你想短路，就借用宿主的短路；你想严格求值，换成 `&` 即可。求值策略藏在那一行字眼里。\n\n**细节三：双字符运算符必须排在单字符前面。** tryReadOp 的候选顺序是 `\">=\", \"\u003C=\", \"==\", \">\", \"\u003C\"`。若把 `\">\"` 放前面，`\"age >= 18\"` 会先吃掉 `>`，剩下 `= 18` 直接语法报错。这叫**最长匹配（贪心匹配）**，是所有词法分析器的第一课——同一个位置可能有多种读法时，永远选最长的那个。一个字符顺序，决定引擎能不能跑。\n\n## 五、在框架里找它\n\n1. **Spring Expression Language（SpEL）**：`@Value(\"#{user.age > 18}\")`、`@PreAuthorize(\"hasRole('ADMIN') and #id == authentication.principal.id\")`——注解里那串字符串就是一门小语言，Spring 启动时解析成 AST，调用时对着上下文求值。本文的 ComparisonExpression、AndExpression 在 SpEL 里全有对应物，只是词法豪华了一百倍。\n\n2. **java.util.regex.Pattern**：`Pattern.compile(\"\\\\d+\")` 干的事就是本文的 RuleParser.parse——把文本编译成内部表示（NFA\u002FDFA），之后 `matcher.matches()` 反复解释输入串。**每个正则引擎都是一台解释器**，而且演示了正确的性能姿势：compile 一次，匹配多次。\n\n3. **MyBatis 的动态 SQL 与 OGNL**：`\u003Cif test=\"name != null and name != ''\">`——test 属性里那串表达式由 OGNL 解释执行，决定 SQL 片段拼不拼进来。全国最大的\"配置里的规则解释器\"部署现场。\n\n4. **Drools \u002F ANTLR**：文法复杂时的正规军。Drools 是成熟的规则引擎（`when … then …` 整门语言）；ANTLR 是解析器生成器——文法文件进，Java 解析器代码出。GoF 定义里那句\"文法复杂请绕行\"，指的就是把方向盘交给它们。\n\n## 六、解释 vs 编译 vs 配置表——规则该放哪\n\n同一个需求（规则要常变）的三种解法摊开对账：\n\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>方案\u003C\u002Fth>\u003Cth>改一条规则的成本\u003C\u002Fth>\u003Cth>能表达什么\u003C\u002Fth>\u003Cth>性能\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\u003Ctr>\u003Ctd>硬编码\u003C\u002Ftd>\u003Ctd>发版全流程\u003C\u002Ftd>\u003Ctd>任意 Java 逻辑\u003C\u002Ftd>\u003Ctd>最好\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>配置表\u003C\u002Ftd>\u003Ctd>改一行数据\u003C\u002Ftd>\u003Ctd>只能填模板的空（满 X 减 Y）\u003C\u002Ftd>\u003Ctd>好\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>解释器\u003C\u002Ftd>\u003Ctd>传一个字符串\u003C\u002Ftd>\u003Ctd>**文法内任意组合**\u003C\u002Ftd>\u003Ctd>每次解析 + 递归求值\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\n\n看清差别了吗？**配置表只能变参数，解释器能变逻辑结构**。满 100 减 20 变满 200 减 50，配置表足矣；\"VIP 且成年，或高等级且成年\"这种结构变化，只有解释器接得住。自由度每升一级，实现成本也升一级——按需购买，不要囤货。\n\n最后补一个生产必修课：本文 `RuleParser.parse(rule)` 每次调用都重新建树，等于**每个请求都在重新编译**。正确姿势是把 parse 结果（AST）缓存进 Map——规则字符串不变，树就不变，这其实就是最朴素的\"编译\"：**解释器把解析与求值拆成两步，中间那段就是留给缓存的缓冲带**。Pattern.compile 的 API 设计（compile 与 matches 分离）早把这条路铺好了，本文的 parse 与 interpret 同款。再进一步，把 AST 直接翻译成字节码，就从解释器进化成了编译器——JIT、V8 走的都是这条路。**解释和编译不是两个世界，是同一条路的两个站点。**\n\n## 写在最后\n\n- 文法简单、规则常变、性能不敏感——解释器的三张准入证\n- 一条文法规则一个类\u002F一个函数，文法即代码，看 BNF 就能读出实现\n- 终结符查环境，非终结符抱子树——AST 是一棵组合树（⑨ 的回头客）\n- 短路求值白送：递归求值对齐宿主语言语义\n- 双字符运算符排前面——贪心匹配，词法分析第一课\n- 解析与求值分离，中间留给缓存——解释器到编译器只差一步\n\n至此，GoF 二十三式全部集齐：创建五式（②③④⑤⑥）、结构七式（⑦—⑬）、行为十一式（⑭—㉕），二十三颗龙珠在手里了。可回头看看真实的企业应用：请求从浏览器涌进来，谁在门口分发？业务逻辑谁替前端挡脏活？数据落地谁牵线搭桥？——这些问题，单个模式接不住，得一群模式排兵布阵。从 ㉖ 起，本系列进入最后一片大陆：**J2EE 企业应用模式**——第一站，MVC。\n","\u003Cblockquote>\n \u003Cp>行为型第十二站（终章）。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>㉔ 的文档能把结构导出成 Markdown 文本——那是\"结构到文本\"的单行道。这一篇开反向车道：给程序一段文本，比如促销规则 \u003Ccode>\"vip &amp;&amp; age &gt;= 18\"\u003C\u002Fcode>，让它自己解析出结构、算出 true \u002F false。\u003C\u002Fp>\n\u003Cp>先看痛点。促销规则是全公司变得最勤的东西：周一\"VIP 且成年可买\"，周三\"新用户零单送券\"，周五运营半夜拍脑袋\"60 岁以上或 VIP 打八折\"。规则硬编码在 Java 里，意味着\u003Cstrong>每改一条规则都要走完提需求 → 排期 → 开发 → 测试 → 发版的流水线\u003C\u002Fstrong>——程序员在给运营当人肉规则编辑器。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>把规则写成一行字符串，让程序自己读懂它、算出结果\u003C\u002Fstrong>——自己实现一个小规则解释器——解释器模式，GoF 二十三式的最后一位，压轴登场。\u003C\u002Fp>\n\u003Ch2>一、解释器模式是什么\u003C\u002Fh2>\n\u003Cp>GoF 的原始定义：\u003C\u002Fp>\n\u003Cblockquote>\n \u003Cp>Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language. （给定一门语言，定义它的文法的一种表示，并定义一个解释器，该解释器使用该表示来解释语言中的句子。）\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>拆开读，定义里藏着三件事：先发明一门\u003Cstrong>小语言\u003C\u002Fstrong>（本篇的规则语法：\u003Ccode>vip &amp;&amp; age &gt;= 18\u003C\u002Fcode>）；再给这门语言一个\u003Cstrong>文法表示\u003C\u002Fstrong>（把字符串解析成语法树）；最后让\u003Cstrong>解释器\u003C\u002Fstrong>拿着语法树对具体数据求值。所谓\"解释\"，就是\u003Cstrong>递归地走一遍这棵树\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Cp>五个角色：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>角色\u003C\u002Fth>\n   \u003Cth>干什么\u003C\u002Fth>\n   \u003Cth>本文对应\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>AbstractExpression（抽象表达式）\u003C\u002Ftd>\n   \u003Ctd>声明 interpret 操作\u003C\u002Ftd>\n   \u003Ctd>Expression 接口\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>TerminalExpression（终结符表达式）\u003C\u002Ftd>\n   \u003Ctd>语法树的叶子：直接查环境、算出结果\u003C\u002Ftd>\n   \u003Ctd>FlagExpression \u002F ComparisonExpression\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>NonterminalExpression（非终结符表达式）\u003C\u002Ftd>\n   \u003Ctd>语法树的枝：组合子表达式\u003C\u002Ftd>\n   \u003Ctd>AndExpression \u002F OrExpression \u002F NotExpression\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Context（环境）\u003C\u002Ftd>\n   \u003Ctd>解释现场，装着所有变量的值\u003C\u002Ftd>\n   \u003Ctd>Map\\\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Client（客户端）\u003C\u002Ftd>\n   \u003Ctd>构建（解析）语法树并启动解释\u003C\u002Ftd>\n   \u003Ctd>RuleParser + InterpreterTest\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>眼熟吗？终结符是叶子、非终结符是枝干、统一接口递归求值——\u003Cstrong>解释器模式几乎总是组合模式的实例\u003C\u002Fstrong>：Expression 树就是 ⑨ 里那棵 Component 树，只是把\"display 挨个执行\"换成了\"interpret 求值\"。⑨ 的回头客，这次是真的要动手建树了。\u003C\u002Fp>\n\u003Ch2>二、优点与缺点\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>优点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>规则即数据\u003C\u002Fstrong>：加一条规则 = 传一个字符串，零代码、零编译、零发版——运营半夜上线，程序员的枕头是干的。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>文法与代码一一对应\u003C\u002Fstrong>：一条文法规则一个类（或一个解析函数），看 BNF 就知道代码长什么样，改文法就是改那一处。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>扩展运算符零侵入\u003C\u002Fstrong>：想支持 \u003Ccode>&gt;\u003C\u002Fcode>、\u003Ccode>&lt;\u003C\u002Fcode> 之外再来个 \u003Ccode>contains\u003C\u002Fcode>？加一个终结符类 + 文法里添一笔，现有表达式类全部不动——开闭原则。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>语法错误前置\u003C\u002Fstrong>：\u003Ccode>\"vip &amp;&amp; &amp; age\"\u003C\u002Fcode> 这种手滑，解析当场报\"位于 7\"，而不是默默跑出一个诡异的业务结果。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>\u003Cstrong>缺点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>文法复杂则类爆炸\u003C\u002Fstrong>：本篇 \u003Ccode>&amp;&amp;\u003C\u002Fcode>、\u003Ccode>||\u003C\u002Fcode>、\u003Ccode>!\u003C\u002Fcode>、比较、括号就 6 个表达式类；再上算术、函数调用、字符串操作……类文件能铺满一屏。GoF 原文自己就劝退：\u003Cstrong>文法复杂时，别手写，用解析器生成器\u003C\u002Fstrong>（ANTLR 一类）。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>性能有税\u003C\u002Fstrong>：每次求值都要递归走整棵树，每次解析都要从字符串建树。高频调用场景这笔账得算清楚（可缓存 AST 缓解，见第六节）。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>等于开了个迷你编程接口\u003C\u002Fstrong>：规则文本若是外部可传入的，\u003Ccode>\"a &amp;&amp; a &amp;&amp; a &amp;&amp; ……（一万层）\"\u003C\u002Fcode> 就能把你递归栈干爆——解释器天生要做长度与深度防护。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>团队门槛\u003C\u002Fstrong>：出了 bug 得有人会看语法树、会读文法。全组只会 CRUD 的日子，一去不返。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>三、什么时候用\u003C\u002Fh2>\n\u003Cp>判断标准：\u003Cstrong>文法简单且稳定、规则文本变化频繁、求值频率不至于性能敏感。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>促销 \u002F 风控 \u002F 权限规则引擎\u003C\u002Fstrong>：规则天天变，语法十年不变——本文场景\u003C\u002Fli>\n \u003Cli>\u003Cstrong>日志\u002F监控查询语言\u003C\u002Fstrong>：Kibana 的 Lucene 查询串、Grafana 的告警条件表达式\u003C\u002Fli>\n \u003Cli>\u003Cstrong>模板与占位符\u003C\u002Fstrong>：\u003Ccode>\"亲爱的${name}，您的订单${orderId}已发货\"\u003C\u002Fcode>——最朴素的单遍解释器\u003C\u002Fli>\n \u003Cli>\u003Cstrong>配置里的动态表达式\u003C\u002Fstrong>：开关平台\"满足条件的用户才灰度\"那串表达式\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>反面对照：\u003Cstrong>规则只是参数在变\u003C\u002Fstrong>（满 100 减 20 变成满 200 减 50）——一张配置表就够，别上解释器，自由度过剩也是浪费；\u003Cstrong>规则复杂到本身就是业务\u003C\u002Fstrong>（信贷审批全流程）——那是业务代码，写 Java 去；\u003Cstrong>每秒百万次求值\u003C\u002Fstrong>——递归树撑不住，编译执行或者硬编码。\u003C\u002Fp>\n\u003Ch2>四、实现方式：迷你规则引擎\u003C\u002Fh2>\n\u003Ch3>4.1 反面教材：规则硬编码\u003C\u002Fh3>\n\u003Cp>每条规则一个 case：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class BadRuleEngine {\n\n    public static boolean check(User user, String ruleName) {\n        switch (ruleName) {\n            case \"vip-adult\" -&gt; { return user.isVip() &amp;&amp; user.getAge() &gt;= 18; }\n            case \"new-user\" -&gt; { return !user.isVip() &amp;&amp; user.getOrderCount() == 0; }\n            default -&gt; throw new IllegalArgumentException(\n                    \"规则 \" + ruleName + \" 不存在：请提需求、排期、开发、测试、发版\");\n        }\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>规则库里两条规则，第三条\"black-friday\"上线时，default 分支就是程序员的周末。更要命的是：\u003Cstrong>规则的本质是数据，却被写成了代码\u003C\u002Fstrong>——数据和代码的区别，就是\"改起来要不要发版\"的区别。\u003C\u002Fp>\n\u003Ch3>4.2 正解：表达式树 + 递归下降解析器\u003C\u002Fh3>\n\u003Cp>目标效果：规则是字符串，规则是数据：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">User zhangsan = new User(true, 35, 42, 3);   \u002F\u002F vip、35 岁、42 单、level 3\nUser tianqi   = new User(false, 16, 0, 1);   \u002F\u002F 非 vip、16 岁、0 单、level 1\n\nMap&lt;String, Object&gt; zhangsanCtx =\n        Map.of(\"vip\", true, \"age\", 35, \"orderCount\", 42, \"level\", 3);\n\nExpression rule = RuleParser.parse(\"(vip || level &gt;= 3) &amp;&amp; age &gt;= 18\");\nrule.interpret(zhangsanCtx);   \u002F\u002F → true\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>第一步：定义小语言。\u003C\u002Fstrong> 四条文法，用 BNF 写出来：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>orExpr   := andExpr ('||' andExpr)*      \u002F\u002F 或，优先级最低\nandExpr  := unary ('&amp;&amp;' unary)*          \u002F\u002F 与\nunary    := '!' unary | primary          \u002F\u002F 非\nprimary  := '(' orExpr ')' | 标识符 运算符 数字 | 标识符    \u002F\u002F 括号 \u002F 比较 \u002F 标志位\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>第二步：表达式族。\u003C\u002Fstrong> 接口只有一行：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public interface Expression {\n    boolean interpret(Map&lt;String, Object&gt; context);\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>终结符两个。\u003Ccode>vip\u003C\u002Fcode> 这种标志位：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">class FlagExpression implements Expression {\n    private final String name;\n    FlagExpression(String name) { this.name = name; }\n\n    @Override\n    public boolean interpret(Map&lt;String, Object&gt; context) {\n        Object value = context.get(name);\n        if (value instanceof Boolean b) {\n            return b;\n        }\n        throw new IllegalArgumentException(\"属性 \" + name + \" 不是布尔值\");\n    }\n\n    @Override\n    public String toString() { return name; }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>age &gt;= 18\u003C\u002Fcode> 这种比较：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">class ComparisonExpression implements Expression {\n    private final String name;\n    private final String op;      \u002F\u002F \"&gt;\"、\"&gt;=\"、\"&lt;\"、\"&lt;=\"、\"==\"\n    private final int target;\n    \u002F\u002F 构造器略\n\n    @Override\n    public boolean interpret(Map&lt;String, Object&gt; context) {\n        int actual = (Integer) context.get(name);\n        return switch (op) {\n            case \"&gt;\"  -&gt; actual &gt;  target;\n            case \"&gt;=\" -&gt; actual &gt;= target;\n            case \"&lt;\"  -&gt; actual &lt;  target;\n            case \"&lt;=\" -&gt; actual &lt;= target;\n            case \"==\" -&gt; actual == target;\n            default -&gt; throw new IllegalArgumentException(\"未知运算符 \" + op);\n        };\n    }\n\n    @Override\n    public String toString() { return name + \" \" + op + \" \" + target; }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>非终结符三个，全是一个模子：\u003Cstrong>抱着两棵（或一棵）子树，求值时递归下问\u003C\u002Fstrong>。以 AndExpression 为例：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">class AndExpression implements Expression {\n    private final Expression left;\n    private final Expression right;\n\n    AndExpression(Expression left, Expression right) {\n        this.left = left;\n        this.right = right;\n    }\n\n    @Override\n    public boolean interpret(Map&lt;String, Object&gt; context) {\n        return left.interpret(context) &amp;&amp; right.interpret(context);\n    }\n\n    @Override\n    public String toString() { return \"(\" + left + \" &amp;&amp; \" + right + \")\"; }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>OrExpression 同款换成 \u003Ccode>||\u003C\u002Fcode>；NotExpression 抱一棵子树返回 \u003Ccode>!inner.interpret(context)\u003C\u002Fcode>。至此，语法树的\"词汇表\"集齐——\u003Ccode>\"(vip || level &gt;= 3) &amp;&amp; age &gt;= 18\"\u003C\u002Fcode> 解析出来是这么一棵树：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>AndExpression\n├── OrExpression\n│   ├── FlagExpression(\"vip\")\n│   └── ComparisonExpression(\"level &gt;= 3\")\n└── ComparisonExpression(\"age &gt;= 18\")\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>第三步：解析器，把文本变树。\u003C\u002Fstrong> RuleParser 是个迷你递归下降解析器，核心思路：\u003Cstrong>每条文法规则，翻译成一个解析函数\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">\u002F**\n * 迷你递归下降解析器：把规则文本变成语法树。\n * 文法（优先级从低到高）：\n *   orExpr   := andExpr ('||' andExpr)*\n *   andExpr  := unary ('&amp;&amp;' unary)*\n *   unary    := '!' unary | primary\n *   primary  := '(' orExpr ')' | 标识符 运算符 数字 | 标识符\n * 每个文法规则对应一个解析函数，解析层次越深优先级越高。\n *\u002F\npublic class RuleParser {\n\n    private final String input;\n    private int pos;\n\n    public static Expression parse(String rule) { \u002F* 见下 *\u002F }\n\n    private Expression parseOr() {\n        Expression left = parseAnd();\n        while (match(\"||\")) {\n            left = new OrExpression(left, parseAnd());\n        }\n        return left;\n    }\n\n    private Expression parseAnd() {\n        Expression left = parseUnary();\n        while (match(\"&amp;&amp;\")) {\n            left = new AndExpression(left, parseUnary());\n        }\n        return left;\n    }\n\n    private Expression parseUnary() {\n        if (match(\"!\")) {\n            return new NotExpression(parseUnary());\n        }\n        return parsePrimary();\n    }\n\n    private Expression parsePrimary() {\n        if (match(\"(\")) {\n            Expression expression = parseOr();\n            expect(\")\");\n            return expression;\n        }\n        String name = readIdentifier();\n        skipWhitespace();\n        String op = tryReadOp();          \u002F\u002F 读得到运算符就是比较式\n        if (op == null) {\n            return new FlagExpression(name);   \u002F\u002F 读不到就是标志位\n        }\n        skipWhitespace();\n        return new ComparisonExpression(name, op, readInt());\n    }\n    \u002F\u002F match\u002Fexpect\u002FreadIdentifier\u002FreadInt 等词法小工具略\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>parse(\"vip &amp;&amp; age &gt;= 18\")\u003C\u002Fcode> 走一遍：parseOr 调 parseAnd，parseAnd 读到 \u003Ccode>vip\u003C\u002Fcode>（FlagExpression），match(\"&amp;&amp;\") 命中，再读 \u003Ccode>age &gt;= 18\u003C\u002Fcode>（ComparisonExpression），包成 AndExpression 还给 parseOr——树建好了。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>第四步：收钱。\u003C\u002Fstrong> 跑测试：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">evaluate(\"vip &amp;&amp; age &gt;= 18\", zhangsanCtx, tianqiCtx);\nevaluate(\"!vip &amp;&amp; orderCount == 0\", zhangsanCtx, tianqiCtx);\nevaluate(\"(vip || level &gt;= 3) &amp;&amp; age &gt;= 18\", zhangsanCtx, tianqiCtx);\n\n\u002F\u002F 运营半夜上线新规则：\nevaluate(\"age &gt;= 60 || vip\", zhangsanCtx, tianqiCtx);\n\nprivate static void evaluate(String rule, Map&lt;String, Object&gt; ctx1, Map&lt;String, Object&gt; ctx2) {\n    Expression expression = RuleParser.parse(rule);\n    System.out.println(\"  [规则] \\\"\" + rule + \"\\\"\");\n    System.out.println(\"    语法树：\" + expression);\n    System.out.println(\"    张三 → \" + expression.interpret(ctx1)\n            + \"，田七 → \" + expression.interpret(ctx2));\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行输出：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>========== 反面教材：规则硬编码在代码里 ==========\n  [vip-adult] 张三 → true\n  [new-user] 田七 → true\n  [翻车] 规则 black-friday 不存在：请提需求、排期、开发、测试、发版\n\n========== 正解：规则即文本，解释器读懂它 ==========\n  [规则] \"vip &amp;&amp; age &gt;= 18\"\n    语法树：(vip &amp;&amp; age &gt;= 18)\n    张三 → true，田七 → false\n  [规则] \"!vip &amp;&amp; orderCount == 0\"\n    语法树：(!vip &amp;&amp; orderCount == 0)\n    张三 → false，田七 → true\n  [规则] \"(vip || level &gt;= 3) &amp;&amp; age &gt;= 18\"\n    语法树：((vip || level &gt;= 3) &amp;&amp; age &gt;= 18)\n    张三 → true，田七 → false\n  —— 运营半夜上线新规则「age &gt;= 60 || vip」：零代码、零发版，传个字符串就行 ——\n  [规则] \"age &gt;= 60 || vip\"\n    语法树：(age &gt;= 60 || vip)\n    张三 → true，田七 → false\n\n  [语法报错] 规则 \"vip &amp;&amp; &amp; age\" →\n    语法错误：期待属性名，位于 7\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>第四条规则上线时，程序员在睡觉。最后一行的语法报错也接住了手滑——\u003Cstrong>错误在解析期暴露，带着坐标\u003C\u002Fstrong>，而不是求值期给出一个莫名其妙的结果。\u003C\u002Fp>\n\u003Ch3>4.3 三处细节\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>细节一：优先级 = 解析函数的嵌套深度。\u003C\u002Fstrong> 为什么 \u003Ccode>vip || level &gt;= 3 &amp;&amp; age &gt;= 18\u003C\u002Fcode> 里 \u003Ccode>&amp;&amp;\u003C\u002Fcode> 比 \u003Ccode>||\u003C\u002Fcode> 结合得紧？因为 parseOr（处理 \u003Ccode>||\u003C\u002Fcode>）在最外层，它先调 parseAnd（处理 \u003Ccode>&amp;&amp;\u003C\u002Fcode>）——\u003Cstrong>越深层的函数，抱得越紧\u003C\u002Fstrong>。文法四行、函数四个、优先级三层，一张嵌套结构图就说明白，不需要任何\"优先级表\"这种额外数据结构。想让某个子表达式脱离默认优先级？\u003Ccode>primary\u003C\u002Fcode> 里的 \u003Ccode>('(' orExpr ')')\u003C\u002Fcode> 规则兜住括号，直接回到最外层重新解析——括号的本质，就是\u003Cstrong>人工指定解析层次\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节二：短路求值是白送的。\u003C\u002Fstrong> AndExpression 的求值就一行 \u003Ccode>left.interpret(context) &amp;&amp; right.interpret(context)\u003C\u002Fcode>——右边复用了 Java 自己的 \u003Ccode>&amp;&amp;\u003C\u002Fcode>。左边 false，右边那棵子树\u003Cstrong>根本不会被碰\u003C\u002Fstrong>。解释器模式的\"递归求值\"天然对齐宿主语言的求值语义：你想短路，就借用宿主的短路；你想严格求值，换成 \u003Ccode>&amp;\u003C\u002Fcode> 即可。求值策略藏在那一行字眼里。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节三：双字符运算符必须排在单字符前面。\u003C\u002Fstrong> tryReadOp 的候选顺序是 \u003Ccode>\"&gt;=\", \"&lt;=\", \"==\", \"&gt;\", \"&lt;\"\u003C\u002Fcode>。若把 \u003Ccode>\"&gt;\"\u003C\u002Fcode> 放前面，\u003Ccode>\"age &gt;= 18\"\u003C\u002Fcode> 会先吃掉 \u003Ccode>&gt;\u003C\u002Fcode>，剩下 \u003Ccode>= 18\u003C\u002Fcode> 直接语法报错。这叫\u003Cstrong>最长匹配（贪心匹配）\u003C\u002Fstrong>，是所有词法分析器的第一课——同一个位置可能有多种读法时，永远选最长的那个。一个字符顺序，决定引擎能不能跑。\u003C\u002Fp>\n\u003Ch2>五、在框架里找它\u003C\u002Fh2>\n\u003Col>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Spring Expression Language（SpEL）\u003C\u002Fstrong>：\u003Ccode>@Value(\"#{user.age &gt; 18}\")\u003C\u002Fcode>、\u003Ccode>@PreAuthorize(\"hasRole('ADMIN') and #id == authentication.principal.id\")\u003C\u002Fcode>——注解里那串字符串就是一门小语言，Spring 启动时解析成 AST，调用时对着上下文求值。本文的 ComparisonExpression、AndExpression 在 SpEL 里全有对应物，只是词法豪华了一百倍。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>java.util.regex.Pattern\u003C\u002Fstrong>：\u003Ccode>Pattern.compile(\"\\\\d+\")\u003C\u002Fcode> 干的事就是本文的 RuleParser.parse——把文本编译成内部表示（NFA\u002FDFA），之后 \u003Ccode>matcher.matches()\u003C\u002Fcode> 反复解释输入串。\u003Cstrong>每个正则引擎都是一台解释器\u003C\u002Fstrong>，而且演示了正确的性能姿势：compile 一次，匹配多次。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>MyBatis 的动态 SQL 与 OGNL\u003C\u002Fstrong>：\u003Ccode>&lt;if test=\"name != null and name != ''\"&gt;\u003C\u002Fcode>——test 属性里那串表达式由 OGNL 解释执行，决定 SQL 片段拼不拼进来。全国最大的\"配置里的规则解释器\"部署现场。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Drools \u002F ANTLR\u003C\u002Fstrong>：文法复杂时的正规军。Drools 是成熟的规则引擎（\u003Ccode>when … then …\u003C\u002Fcode> 整门语言）；ANTLR 是解析器生成器——文法文件进，Java 解析器代码出。GoF 定义里那句\"文法复杂请绕行\"，指的就是把方向盘交给它们。\u003C\u002Fp>\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>六、解释 vs 编译 vs 配置表——规则该放哪\u003C\u002Fh2>\n\u003Cp>同一个需求（规则要常变）的三种解法摊开对账：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>方案\u003C\u002Fth>\n   \u003Cth>改一条规则的成本\u003C\u002Fth>\n   \u003Cth>能表达什么\u003C\u002Fth>\n   \u003Cth>性能\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>硬编码\u003C\u002Ftd>\n   \u003Ctd>发版全流程\u003C\u002Ftd>\n   \u003Ctd>任意 Java 逻辑\u003C\u002Ftd>\n   \u003Ctd>最好\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>配置表\u003C\u002Ftd>\n   \u003Ctd>改一行数据\u003C\u002Ftd>\n   \u003Ctd>只能填模板的空（满 X 减 Y）\u003C\u002Ftd>\n   \u003Ctd>好\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>解释器\u003C\u002Ftd>\n   \u003Ctd>传一个字符串\u003C\u002Ftd>\n   \u003Ctd>**文法内任意组合**\u003C\u002Ftd>\n   \u003Ctd>每次解析 + 递归求值\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>看清差别了吗？\u003Cstrong>配置表只能变参数，解释器能变逻辑结构\u003C\u002Fstrong>。满 100 减 20 变满 200 减 50，配置表足矣；\"VIP 且成年，或高等级且成年\"这种结构变化，只有解释器接得住。自由度每升一级，实现成本也升一级——按需购买，不要囤货。\u003C\u002Fp>\n\u003Cp>最后补一个生产必修课：本文 \u003Ccode>RuleParser.parse(rule)\u003C\u002Fcode> 每次调用都重新建树，等于\u003Cstrong>每个请求都在重新编译\u003C\u002Fstrong>。正确姿势是把 parse 结果（AST）缓存进 Map——规则字符串不变，树就不变，这其实就是最朴素的\"编译\"：\u003Cstrong>解释器把解析与求值拆成两步，中间那段就是留给缓存的缓冲带\u003C\u002Fstrong>。Pattern.compile 的 API 设计（compile 与 matches 分离）早把这条路铺好了，本文的 parse 与 interpret 同款。再进一步，把 AST 直接翻译成字节码，就从解释器进化成了编译器——JIT、V8 走的都是这条路。\u003Cstrong>解释和编译不是两个世界，是同一条路的两个站点。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Ch2>写在最后\u003C\u002Fh2>\n\u003Cul>\n \u003Cli>文法简单、规则常变、性能不敏感——解释器的三张准入证\u003C\u002Fli>\n \u003Cli>一条文法规则一个类\u002F一个函数，文法即代码，看 BNF 就能读出实现\u003C\u002Fli>\n \u003Cli>终结符查环境，非终结符抱子树——AST 是一棵组合树（⑨ 的回头客）\u003C\u002Fli>\n \u003Cli>短路求值白送：递归求值对齐宿主语言语义\u003C\u002Fli>\n \u003Cli>双字符运算符排前面——贪心匹配，词法分析第一课\u003C\u002Fli>\n \u003Cli>解析与求值分离，中间留给缓存——解释器到编译器只差一步\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>至此，GoF 二十三式全部集齐：创建五式（②③④⑤⑥）、结构七式（⑦—⑬）、行为十一式（⑭—㉕），二十三颗龙珠在手里了。可回头看看真实的企业应用：请求从浏览器涌进来，谁在门口分发？业务逻辑谁替前端挡脏活？数据落地谁牵线搭桥？——这些问题，单个模式接不住，得一群模式排兵布阵。从 ㉖ 起，本系列进入最后一片大陆：\u003Cstrong>J2EE 企业应用模式\u003C\u002Fstrong>——第一站，MVC。\u003C\u002Fp>",[],"index,follow",null,{"code":4,"message":5,"data":44},[45,54,63],{"id":46,"slug":47,"title":48,"summary":49,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":50,"likeCount":4,"wordCount":51,"readingMinutes":52,"publishedAt":53},"2113934492581236737","design-pattern-24","设计模式修炼之路 ㉔ 访问者模式——结构与操作分离","双分派、结构与操作分离、visit(this) 不能省、FileVisitor 与 ASM",2450,5892,15,"2024-12-12T14:45:37",{"id":55,"slug":56,"title":57,"summary":58,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":59,"likeCount":4,"wordCount":60,"readingMinutes":61,"publishedAt":62},"2113931965261418497","design-pattern-15","设计模式修炼之路 ⑮ 策略模式——if-else 的解药","一个分支一个类、工厂加 Map 连选择处都不写 if-else、Spring 自动收集、线程池拒绝策略",1681,7953,20,"2024-06-16T18:10:44",{"id":64,"slug":65,"title":66,"summary":67,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":68,"likeCount":4,"wordCount":69,"readingMinutes":70,"publishedAt":71},"2113931511261564929","design-pattern-08","设计模式修炼之路 ⑧ 过滤器模式——与或非条件当积木","非 GoF、与或非条件当积木、Predicate 是 JDK 收编版",3801,7442,19,"2024-01-08T20:51:45",{"code":4,"message":5,"data":73},{"prev":74,"next":75},{"id":46,"slug":47,"title":48,"summary":49,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":50,"likeCount":4,"wordCount":51,"readingMinutes":52,"publishedAt":53},{"id":76,"slug":77,"title":78,"summary":79,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":80,"likeCount":4,"wordCount":81,"readingMinutes":82,"publishedAt":83},"2113935519934058498","design-pattern-26","设计模式修炼之路 ㉖ MVC 模式——改界面不动业务，改业务不动界面","比 GoF 早十五年、依赖方向定生死、业务逻辑永远住 Model、换视图零成本、MVC→MVP→MVVM 进化树",2596,8085,21,"2025-02-16T16:19:17",{"code":4,"message":5,"data":85},[86,92,97,103],{"id":87,"name":88,"slug":89,"description":90,"sortOrder":4,"articleCount":91},"1946000000000000001","antlr4","tech","编程与技术笔记","13",{"id":32,"name":93,"slug":94,"description":95,"sortOrder":4,"articleCount":96},"设计模式","design-pattern","设计模式系列","34",{"id":98,"name":99,"slug":100,"description":101,"sortOrder":33,"articleCount":102},"1946000000000000002","随笔","essay","生活与思考","0",{"id":104,"name":105,"slug":106,"description":107,"sortOrder":108,"articleCount":109},"2105200093764698113","工具","tools","好用的工具推荐",2,"1",{"html":111,"toc":112},"\u003Cblockquote>\n \u003Cp>行为型第十二站（终章）。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>㉔ 的文档能把结构导出成 Markdown 文本——那是\"结构到文本\"的单行道。这一篇开反向车道：给程序一段文本，比如促销规则 \u003Ccode>\"vip &amp;&amp; age &gt;= 18\"\u003C\u002Fcode>，让它自己解析出结构、算出 true \u002F false。\u003C\u002Fp>\n\u003Cp>先看痛点。促销规则是全公司变得最勤的东西：周一\"VIP 且成年可买\"，周三\"新用户零单送券\"，周五运营半夜拍脑袋\"60 岁以上或 VIP 打八折\"。规则硬编码在 Java 里，意味着\u003Cstrong>每改一条规则都要走完提需求 → 排期 → 开发 → 测试 → 发版的流水线\u003C\u002Fstrong>——程序员在给运营当人肉规则编辑器。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>把规则写成一行字符串，让程序自己读懂它、算出结果\u003C\u002Fstrong>——自己实现一个小规则解释器——解释器模式，GoF 二十三式的最后一位，压轴登场。\u003C\u002Fp>\n\u003Ch2 id=\"toc-1\">一、解释器模式是什么\u003C\u002Fh2>\n\u003Cp>GoF 的原始定义：\u003C\u002Fp>\n\u003Cblockquote>\n \u003Cp>Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language. （给定一门语言，定义它的文法的一种表示，并定义一个解释器，该解释器使用该表示来解释语言中的句子。）\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>拆开读，定义里藏着三件事：先发明一门\u003Cstrong>小语言\u003C\u002Fstrong>（本篇的规则语法：\u003Ccode>vip &amp;&amp; age &gt;= 18\u003C\u002Fcode>）；再给这门语言一个\u003Cstrong>文法表示\u003C\u002Fstrong>（把字符串解析成语法树）；最后让\u003Cstrong>解释器\u003C\u002Fstrong>拿着语法树对具体数据求值。所谓\"解释\"，就是\u003Cstrong>递归地走一遍这棵树\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Cp>五个角色：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>角色\u003C\u002Fth>\n   \u003Cth>干什么\u003C\u002Fth>\n   \u003Cth>本文对应\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>AbstractExpression（抽象表达式）\u003C\u002Ftd>\n   \u003Ctd>声明 interpret 操作\u003C\u002Ftd>\n   \u003Ctd>Expression 接口\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>TerminalExpression（终结符表达式）\u003C\u002Ftd>\n   \u003Ctd>语法树的叶子：直接查环境、算出结果\u003C\u002Ftd>\n   \u003Ctd>FlagExpression \u002F ComparisonExpression\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>NonterminalExpression（非终结符表达式）\u003C\u002Ftd>\n   \u003Ctd>语法树的枝：组合子表达式\u003C\u002Ftd>\n   \u003Ctd>AndExpression \u002F OrExpression \u002F NotExpression\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Context（环境）\u003C\u002Ftd>\n   \u003Ctd>解释现场，装着所有变量的值\u003C\u002Ftd>\n   \u003Ctd>Map\\\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Client（客户端）\u003C\u002Ftd>\n   \u003Ctd>构建（解析）语法树并启动解释\u003C\u002Ftd>\n   \u003Ctd>RuleParser + InterpreterTest\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>眼熟吗？终结符是叶子、非终结符是枝干、统一接口递归求值——\u003Cstrong>解释器模式几乎总是组合模式的实例\u003C\u002Fstrong>：Expression 树就是 ⑨ 里那棵 Component 树，只是把\"display 挨个执行\"换成了\"interpret 求值\"。⑨ 的回头客，这次是真的要动手建树了。\u003C\u002Fp>\n\u003Ch2 id=\"toc-2\">二、优点与缺点\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>优点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>规则即数据\u003C\u002Fstrong>：加一条规则 = 传一个字符串，零代码、零编译、零发版——运营半夜上线，程序员的枕头是干的。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>文法与代码一一对应\u003C\u002Fstrong>：一条文法规则一个类（或一个解析函数），看 BNF 就知道代码长什么样，改文法就是改那一处。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>扩展运算符零侵入\u003C\u002Fstrong>：想支持 \u003Ccode>&gt;\u003C\u002Fcode>、\u003Ccode>&lt;\u003C\u002Fcode> 之外再来个 \u003Ccode>contains\u003C\u002Fcode>？加一个终结符类 + 文法里添一笔，现有表达式类全部不动——开闭原则。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>语法错误前置\u003C\u002Fstrong>：\u003Ccode>\"vip &amp;&amp; &amp; age\"\u003C\u002Fcode> 这种手滑，解析当场报\"位于 7\"，而不是默默跑出一个诡异的业务结果。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>\u003Cstrong>缺点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>文法复杂则类爆炸\u003C\u002Fstrong>：本篇 \u003Ccode>&amp;&amp;\u003C\u002Fcode>、\u003Ccode>||\u003C\u002Fcode>、\u003Ccode>!\u003C\u002Fcode>、比较、括号就 6 个表达式类；再上算术、函数调用、字符串操作……类文件能铺满一屏。GoF 原文自己就劝退：\u003Cstrong>文法复杂时，别手写，用解析器生成器\u003C\u002Fstrong>（ANTLR 一类）。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>性能有税\u003C\u002Fstrong>：每次求值都要递归走整棵树，每次解析都要从字符串建树。高频调用场景这笔账得算清楚（可缓存 AST 缓解，见第六节）。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>等于开了个迷你编程接口\u003C\u002Fstrong>：规则文本若是外部可传入的，\u003Ccode>\"a &amp;&amp; a &amp;&amp; a &amp;&amp; ……（一万层）\"\u003C\u002Fcode> 就能把你递归栈干爆——解释器天生要做长度与深度防护。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>团队门槛\u003C\u002Fstrong>：出了 bug 得有人会看语法树、会读文法。全组只会 CRUD 的日子，一去不返。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"toc-3\">三、什么时候用\u003C\u002Fh2>\n\u003Cp>判断标准：\u003Cstrong>文法简单且稳定、规则文本变化频繁、求值频率不至于性能敏感。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>促销 \u002F 风控 \u002F 权限规则引擎\u003C\u002Fstrong>：规则天天变，语法十年不变——本文场景\u003C\u002Fli>\n \u003Cli>\u003Cstrong>日志\u002F监控查询语言\u003C\u002Fstrong>：Kibana 的 Lucene 查询串、Grafana 的告警条件表达式\u003C\u002Fli>\n \u003Cli>\u003Cstrong>模板与占位符\u003C\u002Fstrong>：\u003Ccode>\"亲爱的${name}，您的订单${orderId}已发货\"\u003C\u002Fcode>——最朴素的单遍解释器\u003C\u002Fli>\n \u003Cli>\u003Cstrong>配置里的动态表达式\u003C\u002Fstrong>：开关平台\"满足条件的用户才灰度\"那串表达式\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>反面对照：\u003Cstrong>规则只是参数在变\u003C\u002Fstrong>（满 100 减 20 变成满 200 减 50）——一张配置表就够，别上解释器，自由度过剩也是浪费；\u003Cstrong>规则复杂到本身就是业务\u003C\u002Fstrong>（信贷审批全流程）——那是业务代码，写 Java 去；\u003Cstrong>每秒百万次求值\u003C\u002Fstrong>——递归树撑不住，编译执行或者硬编码。\u003C\u002Fp>\n\u003Ch2 id=\"toc-4\">四、实现方式：迷你规则引擎\u003C\u002Fh2>\n\u003Ch3 id=\"toc-5\">4.1 反面教材：规则硬编码\u003C\u002Fh3>\n\u003Cp>每条规则一个 case：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> BadRuleEngine\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> static\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> boolean\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> check\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(User \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">user\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">ruleName\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        switch\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (ruleName) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            case\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"vip-adult\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> ->\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> { \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> user.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">isVip\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> user.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getAge\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">>=\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 18\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            case\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"new-user\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> ->\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> { \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">return\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> !\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">user.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">isVip\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> user.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getOrderCount\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">==\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            default\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> ->\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> throw\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> IllegalArgumentException\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">                    \"规则 \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ruleName \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" 不存在：请提需求、排期、开发、测试、发版\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>规则库里两条规则，第三条\"black-friday\"上线时，default 分支就是程序员的周末。更要命的是：\u003Cstrong>规则的本质是数据，却被写成了代码\u003C\u002Fstrong>——数据和代码的区别，就是\"改起来要不要发版\"的区别。\u003C\u002Fp>\n\u003Ch3 id=\"toc-6\">4.2 正解：表达式树 + 递归下降解析器\u003C\u002Fh3>\n\u003Cp>目标效果：规则是字符串，规则是数据：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">User zhangsan \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> User\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">true\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">35\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">42\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">3\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);   \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F vip、35 岁、42 单、level 3\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">User tianqi   \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> User\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">false\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">16\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">0\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">1\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);   \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 非 vip、16 岁、0 单、level 1\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">Map&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">String\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Object\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> zhangsanCtx \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        Map.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">of\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"vip\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">true\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"age\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">35\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"orderCount\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">42\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"level\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">3\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">Expression rule \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> RuleParser.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">parse\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"(vip || level >= 3) &#x26;&#x26; age >= 18\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">rule.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">interpret\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(zhangsanCtx);   \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F → true\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>第一步：定义小语言。\u003C\u002Fstrong> 四条文法，用 BNF 写出来：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>orExpr   := andExpr ('||' andExpr)*      \u002F\u002F 或，优先级最低\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>andExpr  := unary ('&#x26;&#x26;' unary)*          \u002F\u002F 与\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>unary    := '!' unary | primary          \u002F\u002F 非\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>primary  := '(' orExpr ')' | 标识符 运算符 数字 | 标识符    \u002F\u002F 括号 \u002F 比较 \u002F 标志位\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>第二步：表达式族。\u003C\u002Fstrong> 接口只有一行：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> interface\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Expression\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    boolean\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> interpret\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(Map&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">String\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Object\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">context\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>终结符两个。\u003Ccode>vip\u003C\u002Fcode> 这种标志位：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> FlagExpression\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> implements\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Expression\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String name;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">    FlagExpression\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">name\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) { \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">this\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">.name \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name; }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    @\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> boolean\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> interpret\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(Map&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">String\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Object\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">context\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        Object value \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> context.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">get\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(name);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (value \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">instanceof\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Boolean b) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> b;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        throw\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> IllegalArgumentException\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"属性 \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" 不是布尔值\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    @\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">toString\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() { \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name; }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>age &gt;= 18\u003C\u002Fcode> 这种比较：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> ComparisonExpression\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> implements\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Expression\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String name;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String op;      \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F \">\"、\">=\"、\"&#x3C;\"、\"&#x3C;=\"、\"==\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> int\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> target;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F 构造器略\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    @\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> boolean\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> interpret\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(Map&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">String\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Object\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">context\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        int\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> actual \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (Integer) context.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">get\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(name);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> switch\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (op) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            case\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \">\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">  ->\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> actual \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">>\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">  target;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            case\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \">=\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> ->\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> actual \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">>=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> target;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            case\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"&#x3C;\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">  ->\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> actual \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">  target;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            case\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"&#x3C;=\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> ->\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> actual \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> target;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            case\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"==\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> ->\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> actual \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">==\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> target;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            default\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> ->\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> throw\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> IllegalArgumentException\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"未知运算符 \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> op);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        };\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    @\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">toString\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() { \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> op \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> target; }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>非终结符三个，全是一个模子：\u003Cstrong>抱着两棵（或一棵）子树，求值时递归下问\u003C\u002Fstrong>。以 AndExpression 为例：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> AndExpression\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> implements\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Expression\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Expression left;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Expression right;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">    AndExpression\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(Expression \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">left\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, Expression \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">right\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">        this\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">.left \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> left;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">        this\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">.right \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> right;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    @\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> boolean\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> interpret\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(Map&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">String\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Object\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">context\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> left.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">interpret\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(context) \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> right.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">interpret\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(context);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    @\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">toString\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() { \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">return\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"(\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> left \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" &#x26;&#x26; \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> right \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \")\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>OrExpression 同款换成 \u003Ccode>||\u003C\u002Fcode>；NotExpression 抱一棵子树返回 \u003Ccode>!inner.interpret(context)\u003C\u002Fcode>。至此，语法树的\"词汇表\"集齐——\u003Ccode>\"(vip || level &gt;= 3) &amp;&amp; age &gt;= 18\"\u003C\u002Fcode> 解析出来是这么一棵树：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>AndExpression\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>├── OrExpression\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>│   ├── FlagExpression(\"vip\")\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>│   └── ComparisonExpression(\"level >= 3\")\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>└── ComparisonExpression(\"age >= 18\")\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>第三步：解析器，把文本变树。\u003C\u002Fstrong> RuleParser 是个迷你递归下降解析器，核心思路：\u003Cstrong>每条文法规则，翻译成一个解析函数\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F**\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> * 迷你递归下降解析器：把规则文本变成语法树。\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> * 文法（优先级从低到高）：\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> *   orExpr   := andExpr ('||' andExpr)*\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> *   andExpr  := unary ('&#x26;&#x26;' unary)*\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> *   unary    := '!' unary | primary\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> *   primary  := '(' orExpr ')' | 标识符 运算符 数字 | 标识符\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> * 每个文法规则对应一个解析函数，解析层次越深优先级越高。\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> *\u002F\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> RuleParser\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String input;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> int\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> pos;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> static\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Expression \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">parse\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">rule\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) { \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F* 见下 *\u002F\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Expression \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">parseOr\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        Expression left \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> parseAnd\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        while\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">match\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"||\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">)) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            left \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> OrExpression\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(left, \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">parseAnd\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> left;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Expression \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">parseAnd\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        Expression left \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> parseUnary\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        while\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">match\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"&#x26;&#x26;\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">)) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            left \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> AndExpression\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(left, \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">parseUnary\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> left;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Expression \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">parseUnary\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">match\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"!\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">)) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            return\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> NotExpression\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">parseUnary\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> parsePrimary\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Expression \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">parsePrimary\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">match\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"(\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">)) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            Expression expression \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> parseOr\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">            expect\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\")\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> expression;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        String name \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> readIdentifier\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">        skipWhitespace\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        String op \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> tryReadOp\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();          \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 读得到运算符就是比较式\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (op \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">==\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> null\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            return\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> FlagExpression\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(name);   \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 读不到就是标志位\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">        skipWhitespace\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> ComparisonExpression\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(name, op, \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">readInt\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F match\u002Fexpect\u002FreadIdentifier\u002FreadInt 等词法小工具略\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>parse(\"vip &amp;&amp; age &gt;= 18\")\u003C\u002Fcode> 走一遍：parseOr 调 parseAnd，parseAnd 读到 \u003Ccode>vip\u003C\u002Fcode>（FlagExpression），match(\"&amp;&amp;\") 命中，再读 \u003Ccode>age &gt;= 18\u003C\u002Fcode>（ComparisonExpression），包成 AndExpression 还给 parseOr——树建好了。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>第四步：收钱。\u003C\u002Fstrong> 跑测试：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">evaluate\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"vip &#x26;&#x26; age >= 18\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, zhangsanCtx, tianqiCtx);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">evaluate\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"!vip &#x26;&#x26; orderCount == 0\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, zhangsanCtx, tianqiCtx);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">evaluate\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"(vip || level >= 3) &#x26;&#x26; age >= 18\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, zhangsanCtx, tianqiCtx);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 运营半夜上线新规则：\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">evaluate\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"age >= 60 || vip\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, zhangsanCtx, tianqiCtx);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> static\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> evaluate\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String rule, Map\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">String, Object\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">>\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx1, Map\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">String, Object\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">>\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx2) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    Expression expression \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> RuleParser.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">parse\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(rule);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"  [规则] \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> rule \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"    语法树：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> expression);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"    张三 → \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> expression.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">interpret\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ctx1)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            +\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"，田七 → \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> expression.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">interpret\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ctx2));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行输出：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>========== 反面教材：规则硬编码在代码里 ==========\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [vip-adult] 张三 → true\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [new-user] 田七 → true\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [翻车] 规则 black-friday 不存在：请提需求、排期、开发、测试、发版\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>========== 正解：规则即文本，解释器读懂它 ==========\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [规则] \"vip &#x26;&#x26; age >= 18\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    语法树：(vip &#x26;&#x26; age >= 18)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    张三 → true，田七 → false\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [规则] \"!vip &#x26;&#x26; orderCount == 0\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    语法树：(!vip &#x26;&#x26; orderCount == 0)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    张三 → false，田七 → true\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [规则] \"(vip || level >= 3) &#x26;&#x26; age >= 18\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    语法树：((vip || level >= 3) &#x26;&#x26; age >= 18)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    张三 → true，田七 → false\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  —— 运营半夜上线新规则「age >= 60 || vip」：零代码、零发版，传个字符串就行 ——\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [规则] \"age >= 60 || vip\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    语法树：(age >= 60 || vip)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    张三 → true，田七 → false\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [语法报错] 规则 \"vip &#x26;&#x26; &#x26; age\" →\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    语法错误：期待属性名，位于 7\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>第四条规则上线时，程序员在睡觉。最后一行的语法报错也接住了手滑——\u003Cstrong>错误在解析期暴露，带着坐标\u003C\u002Fstrong>，而不是求值期给出一个莫名其妙的结果。\u003C\u002Fp>\n\u003Ch3 id=\"toc-7\">4.3 三处细节\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>细节一：优先级 = 解析函数的嵌套深度。\u003C\u002Fstrong> 为什么 \u003Ccode>vip || level &gt;= 3 &amp;&amp; age &gt;= 18\u003C\u002Fcode> 里 \u003Ccode>&amp;&amp;\u003C\u002Fcode> 比 \u003Ccode>||\u003C\u002Fcode> 结合得紧？因为 parseOr（处理 \u003Ccode>||\u003C\u002Fcode>）在最外层，它先调 parseAnd（处理 \u003Ccode>&amp;&amp;\u003C\u002Fcode>）——\u003Cstrong>越深层的函数，抱得越紧\u003C\u002Fstrong>。文法四行、函数四个、优先级三层，一张嵌套结构图就说明白，不需要任何\"优先级表\"这种额外数据结构。想让某个子表达式脱离默认优先级？\u003Ccode>primary\u003C\u002Fcode> 里的 \u003Ccode>('(' orExpr ')')\u003C\u002Fcode> 规则兜住括号，直接回到最外层重新解析——括号的本质，就是\u003Cstrong>人工指定解析层次\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节二：短路求值是白送的。\u003C\u002Fstrong> AndExpression 的求值就一行 \u003Ccode>left.interpret(context) &amp;&amp; right.interpret(context)\u003C\u002Fcode>——右边复用了 Java 自己的 \u003Ccode>&amp;&amp;\u003C\u002Fcode>。左边 false，右边那棵子树\u003Cstrong>根本不会被碰\u003C\u002Fstrong>。解释器模式的\"递归求值\"天然对齐宿主语言的求值语义：你想短路，就借用宿主的短路；你想严格求值，换成 \u003Ccode>&amp;\u003C\u002Fcode> 即可。求值策略藏在那一行字眼里。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节三：双字符运算符必须排在单字符前面。\u003C\u002Fstrong> tryReadOp 的候选顺序是 \u003Ccode>\"&gt;=\", \"&lt;=\", \"==\", \"&gt;\", \"&lt;\"\u003C\u002Fcode>。若把 \u003Ccode>\"&gt;\"\u003C\u002Fcode> 放前面，\u003Ccode>\"age &gt;= 18\"\u003C\u002Fcode> 会先吃掉 \u003Ccode>&gt;\u003C\u002Fcode>，剩下 \u003Ccode>= 18\u003C\u002Fcode> 直接语法报错。这叫\u003Cstrong>最长匹配（贪心匹配）\u003C\u002Fstrong>，是所有词法分析器的第一课——同一个位置可能有多种读法时，永远选最长的那个。一个字符顺序，决定引擎能不能跑。\u003C\u002Fp>\n\u003Ch2 id=\"toc-8\">五、在框架里找它\u003C\u002Fh2>\n\u003Col>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Spring Expression Language（SpEL）\u003C\u002Fstrong>：\u003Ccode>@Value(\"#{user.age &gt; 18}\")\u003C\u002Fcode>、\u003Ccode>@PreAuthorize(\"hasRole('ADMIN') and #id == authentication.principal.id\")\u003C\u002Fcode>——注解里那串字符串就是一门小语言，Spring 启动时解析成 AST，调用时对着上下文求值。本文的 ComparisonExpression、AndExpression 在 SpEL 里全有对应物，只是词法豪华了一百倍。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>java.util.regex.Pattern\u003C\u002Fstrong>：\u003Ccode>Pattern.compile(\"\\\\d+\")\u003C\u002Fcode> 干的事就是本文的 RuleParser.parse——把文本编译成内部表示（NFA\u002FDFA），之后 \u003Ccode>matcher.matches()\u003C\u002Fcode> 反复解释输入串。\u003Cstrong>每个正则引擎都是一台解释器\u003C\u002Fstrong>，而且演示了正确的性能姿势：compile 一次，匹配多次。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>MyBatis 的动态 SQL 与 OGNL\u003C\u002Fstrong>：\u003Ccode>&lt;if test=\"name != null and name != ''\"&gt;\u003C\u002Fcode>——test 属性里那串表达式由 OGNL 解释执行，决定 SQL 片段拼不拼进来。全国最大的\"配置里的规则解释器\"部署现场。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Drools \u002F ANTLR\u003C\u002Fstrong>：文法复杂时的正规军。Drools 是成熟的规则引擎（\u003Ccode>when … then …\u003C\u002Fcode> 整门语言）；ANTLR 是解析器生成器——文法文件进，Java 解析器代码出。GoF 定义里那句\"文法复杂请绕行\"，指的就是把方向盘交给它们。\u003C\u002Fp>\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"toc-9\">六、解释 vs 编译 vs 配置表——规则该放哪\u003C\u002Fh2>\n\u003Cp>同一个需求（规则要常变）的三种解法摊开对账：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>方案\u003C\u002Fth>\n   \u003Cth>改一条规则的成本\u003C\u002Fth>\n   \u003Cth>能表达什么\u003C\u002Fth>\n   \u003Cth>性能\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>硬编码\u003C\u002Ftd>\n   \u003Ctd>发版全流程\u003C\u002Ftd>\n   \u003Ctd>任意 Java 逻辑\u003C\u002Ftd>\n   \u003Ctd>最好\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>配置表\u003C\u002Ftd>\n   \u003Ctd>改一行数据\u003C\u002Ftd>\n   \u003Ctd>只能填模板的空（满 X 减 Y）\u003C\u002Ftd>\n   \u003Ctd>好\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>解释器\u003C\u002Ftd>\n   \u003Ctd>传一个字符串\u003C\u002Ftd>\n   \u003Ctd>**文法内任意组合**\u003C\u002Ftd>\n   \u003Ctd>每次解析 + 递归求值\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>看清差别了吗？\u003Cstrong>配置表只能变参数，解释器能变逻辑结构\u003C\u002Fstrong>。满 100 减 20 变满 200 减 50，配置表足矣；\"VIP 且成年，或高等级且成年\"这种结构变化，只有解释器接得住。自由度每升一级，实现成本也升一级——按需购买，不要囤货。\u003C\u002Fp>\n\u003Cp>最后补一个生产必修课：本文 \u003Ccode>RuleParser.parse(rule)\u003C\u002Fcode> 每次调用都重新建树，等于\u003Cstrong>每个请求都在重新编译\u003C\u002Fstrong>。正确姿势是把 parse 结果（AST）缓存进 Map——规则字符串不变，树就不变，这其实就是最朴素的\"编译\"：\u003Cstrong>解释器把解析与求值拆成两步，中间那段就是留给缓存的缓冲带\u003C\u002Fstrong>。Pattern.compile 的 API 设计（compile 与 matches 分离）早把这条路铺好了，本文的 parse 与 interpret 同款。再进一步，把 AST 直接翻译成字节码，就从解释器进化成了编译器——JIT、V8 走的都是这条路。\u003Cstrong>解释和编译不是两个世界，是同一条路的两个站点。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Ch2 id=\"toc-10\">写在最后\u003C\u002Fh2>\n\u003Cul>\n \u003Cli>文法简单、规则常变、性能不敏感——解释器的三张准入证\u003C\u002Fli>\n \u003Cli>一条文法规则一个类\u002F一个函数，文法即代码，看 BNF 就能读出实现\u003C\u002Fli>\n \u003Cli>终结符查环境，非终结符抱子树——AST 是一棵组合树（⑨ 的回头客）\u003C\u002Fli>\n \u003Cli>短路求值白送：递归求值对齐宿主语言语义\u003C\u002Fli>\n \u003Cli>双字符运算符排前面——贪心匹配，词法分析第一课\u003C\u002Fli>\n \u003Cli>解析与求值分离，中间留给缓存——解释器到编译器只差一步\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>至此，GoF 二十三式全部集齐：创建五式（②③④⑤⑥）、结构七式（⑦—⑬）、行为十一式（⑭—㉕），二十三颗龙珠在手里了。可回头看看真实的企业应用：请求从浏览器涌进来，谁在门口分发？业务逻辑谁替前端挡脏活？数据落地谁牵线搭桥？——这些问题，单个模式接不住，得一群模式排兵布阵。从 ㉖ 起，本系列进入最后一片大陆：\u003Cstrong>J2EE 企业应用模式\u003C\u002Fstrong>——第一站，MVC。\u003C\u002Fp>",[113,116,119,122,125,129,132,135,138,141],{"id":114,"text":115,"level":108},"toc-1","一、解释器模式是什么",{"id":117,"text":118,"level":108},"toc-2","二、优点与缺点",{"id":120,"text":121,"level":108},"toc-3","三、什么时候用",{"id":123,"text":124,"level":108},"toc-4","四、实现方式：迷你规则引擎",{"id":126,"text":127,"level":128},"toc-5","4.1 反面教材：规则硬编码",3,{"id":130,"text":131,"level":128},"toc-6","4.2 正解：表达式树 + 递归下降解析器",{"id":133,"text":134,"level":128},"toc-7","4.3 三处细节",{"id":136,"text":137,"level":108},"toc-8","五、在框架里找它",{"id":139,"text":140,"level":108},"toc-9","六、解释 vs 编译 vs 配置表——规则该放哪",{"id":142,"text":143,"level":108},"toc-10","写在最后"]