Java 23 种设计模式:从踩坑到精通 | 解释器模式 —— 自己动手写一个小语言解释器
Java 23 种设计模式从踩坑到精通 | Interpreter —— 自己动手写一个小语言解释器摘要当需要解析特定格式的语言或表达式如数学公式、SQL 语句、规则引擎且语法规则可能频繁变化时手写字符串处理会让代码迅速膨胀且难以扩展。解释器模式将每个语法规则表示为一个类通过这些类的组合构建出抽象语法树AST从而将“解释”这个复杂过程拆解为一棵可递归求值的对象树。本文从四则运算计算器出发完整讲解解释器模式的原理、UML、代码实现、优缺点并与组合模式进行对比结合 Java 正则表达式、Spring SpEL 等框架应用帮你掌握“语言即对象树”的设计思想。️本文阅读地图3 分钟速览为什么switch解析表达式一定会炸终结符 vs 非终结符语法树的两类节点手写四则运算解释器支持变量和括号扩展乘法和括号如何零侵入添加JDK 正则 / Spring SpEL 如何体现解释器模式面试必问解释器 vs 组合模式有什么区别《Java 23 种设计模式从踩坑到精通》开篇系列介绍与目录 上一篇命令模式 当前解释器模式 下一篇迭代器模式 返回系列总目录文章目录Java 23 种设计模式从踩坑到精通 | Interpreter —— 自己动手写一个小语言解释器1. 从“计算器”的需求说起1.1 你的场景该不该用解释器2. 模式定义与 UML 结构图文解析3. 代码实现四则运算表达式解析器3.1 上下文变量表3.2 抽象表达式3.3 终结符表达式变量3.4 非终结符表达式加法、减法3.5 客户端构建语法树并求值4. 扩展加入乘法和括号5. 优缺点一览6. 解释器模式 vs 组合模式7. 框架与实践中的应用7.1 Java 正则表达式java.util.regex.Pattern7.2 Spring Expression Language (SpEL)7.3 规则引擎Drools、QLExpress8. 面试必问 面试官追问连环炮9. 六大设计原则在解释器模式中的体现 《Java 23 种设计模式从踩坑到精通》快速导航1. 从“计算器”的需求说起想象你要实现一个简单的表达式计算器能够解析并计算如a b - c这样的表达式。如果采用最直白的字符串分割和switch判断if(expression.contains()){// 处理加法}elseif(expression.contains(-)){// 处理减法}当扩展到乘除、括号、函数调用时代码会迅速变成一坨难以维护的“意大利面条”。更麻烦的是如果表达式语法发生变化如加入幂运算原有解析逻辑很可能需要重写。解释器模式Interpreter Pattern正是为这类“小语言”解释场景而生它将每个语法规则表示为一个类通过这些类的组合构建出抽象语法树再由客户端输入上下文递归求值。1.1 你的场景该不该用解释器判断标准是 → 用解释器否 → 用其他方式需要自定义简单的语法规则或表达式✅❌语法规则可能频繁变化需要灵活扩展✅❌语法结构非常复杂如完整编程语言❌使用 ANTLR 等专业解析器只需要简单的字符串处理或正则❌直接用String或Pattern2. 模式定义与 UML 结构解释器模式给定一种语言定义它的文法的一种表示并定义一个解释器这个解释器使用该表示来解释语言中的句子。它属于行为型设计模式。图文解析解释器模式的核心角色抽象表达式Expression声明一个抽象的解释操作interpret(context)是所有终结符和非终结符表达式的公共父类。终结符表达式TerminalExpression实现与文法中的终结符相关的解释操作通常是变量或常量例如a、5。非终结符表达式NonTerminalExpression组合多个表达式表示语法规则如加法、减法内部持有左右两个Expression引用递归调用子表达式的interpret()完成计算。核心机制解释器模式把“解释语言”这件事拆分为一棵由对象组成的语法树每个节点自己负责自己的求值逻辑。客户端只需构建语法树并调用顶层节点的interpret()计算自动递归完成。3. 代码实现四则运算表达式解析器3.1 上下文变量表publicclassContext{privateMapString,IntegervariablesnewHashMap();publicvoidsetVariable(Stringname,intvalue){variables.put(name,value);}publicintgetVariable(Stringname){returnvariables.getOrDefault(name,0);}}白话上下文就是“备忘录”记录表达式里每个变量代表什么值比如a 10。3.2 抽象表达式publicinterfaceExpression{intinterpret(Contextcontext);}白话所有表达式节点都必须能“求值”——给定上下文返回计算结果。3.3 终结符表达式变量publicclassVariableimplementsExpression{privateStringname;publicVariable(Stringname){this.namename;}Overridepublicintinterpret(Contextcontext){returncontext.getVariable(name);}}白话变量节点没有子节点它直接从上下文中取出对应的数值。3.4 非终结符表达式加法、减法publicclassAddimplementsExpression{privateExpressionleft;privateExpressionright;publicAdd(Expressionleft,Expressionright){this.leftleft;this.rightright;}Overridepublicintinterpret(Contextcontext){returnleft.interpret(context)right.interpret(context);}}publicclassSubtractimplementsExpression{privateExpressionleft;privateExpressionright;publicSubtract(Expressionleft,Expressionright){this.leftleft;this.rightright;}Overridepublicintinterpret(Contextcontext){returnleft.interpret(context)-right.interpret(context);}}白话加法/减法节点持有一个左子表达式和一个右子表达式求值时先递归求出左右两边的值再执行加减。3.5 客户端构建语法树并求值publicclassCalculator{publicstaticvoidmain(String[]args){ContextctxnewContext();ctx.setVariable(a,10);ctx.setVariable(b,5);ctx.setVariable(c,2);// 构建语法树a b - cExpressionexpressionnewSubtract(newAdd(newVariable(a),newVariable(b)),newVariable(c));intresultexpression.interpret(ctx);System.out.println(a b - c result);// 13}}表达式a b - c被构建为一棵语法树Subtract / \ Add c / \ a b客户端只需创建语法树并调用interpret()递归求值自动完成。4. 扩展加入乘法和括号乘法与加法结构相同括号则需要构建一棵子树。新增运算符无需修改已有类只需新增一个NonTerminalExpression子类。publicclassMultiplyimplementsExpression{privateExpressionleft,right;publicMultiply(Expressionleft,Expressionright){this.leftleft;this.rightright;}publicintinterpret(Contextcontext){returnleft.interpret(context)*right.interpret(context);}}白话想增加新运算加一个新类就好不用动原来的加法、减法代码。构建(a b) * cExpressionexprnewMultiply(newAdd(newVariable(a),newVariable(b)),newVariable(c));System.out.println(expr.interpret(ctx));// (105)*2 30每加入一个新运算符只需新增一个NonTerminalExpression子类完全符合开闭原则。5. 优缺点一览优点缺点易于扩展新增语法规则只需加新表达式类遵循开闭原则类数量膨胀每个语法规则一个类复杂语法会导致类爆炸易于实现语法规则与代码类一一对应直观执行效率低递归调用大量对象解释型求值比原生编译器慢可组合通过组合形成复杂的语法树调试困难语法树深时调用栈复杂排查问题成本高适合简单语法特别适合正则、表达式等小语言不适合复杂语法更推荐使用解析器生成工具如 ANTLR6. 解释器模式 vs 组合模式这两种模式都使用树形结构但目的完全不同对比维度解释器模式组合模式目的解释语言句子求值统一处理部分与整体节点行为interpret()计算不同类型节点行为差异大operation()统一叶子与容器行为相似典型应用表达式求值、正则、SpEL文件系统、组织架构 解释器模式的树是“计算树”每个节点执行不同的计算逻辑组合模式的树是“结构树”强调客户一致访问。7. 框架与实践中的应用7.1 Java 正则表达式java.util.regex.PatternJava 正则引擎内部将正则表达式字符串解析为一棵语法树每个元字符如*、、?对应不同的节点类型这个过程就是解释器模式的应用。7.2 Spring Expression Language (SpEL)Spring 的 SpEL 可以解析并求值复杂的表达式例如ExpressionParserparsernewSpelExpressionParser();Expressionexpparser.parseExpression(Hello world);Stringvalueexp.getValue(context,String.class);SpEL 内部将表达式字符串解析为 AST然后递归求值这正是解释器模式的体现。7.3 规则引擎Drools、QLExpress规则引擎将业务规则以 DSL 形式编写内部通过解析器构建 AST然后注入事实数据执行规则。其核心就是解释器模式。8. 面试必问 面试官追问连环炮基础必问解释器模式由哪些角色组成→ 抽象表达式、终结符表达式、非终结符表达式、上下文。解释器模式如何遵循开闭原则→ 新增语法规则只需新增表达式类无需修改原有表达式类。解释器模式的缺点→ 类膨胀、执行效率低复杂语法不推荐。面试官追问“解释器模式和组合模式有什么区别” 组合关注“结构统一”解释器关注“递归求值”。解释器的语法树往往建立在组合模式之上。“为什么复杂语法不推荐解释器模式” 因为每个语法规则都要对应一个类类数量会爆炸且递归求值性能远低于直接编译。此时应用 ANTLR 等专业工具。“Spring SpEL 是解释器模式吗” 是。它把表达式字符串解析为 AST然后递归求值是解释器模式在企业框架中的典型应用。恭喜如果你能立刻画出a b - c的语法树并说出终结符与非终结符的区别你已经掌握了行为型模式中“语言即对象树”的设计精髓。9. 六大设计原则在解释器模式中的体现设计原则在解释器模式中的体现单一职责原则SRP每个表达式类只负责一种语法规则的解释开闭原则OCP新增运算符只需加新类无需修改现有代码里氏替换原则LSP所有表达式都可替换抽象Expression依赖倒置原则DIP客户端依赖抽象Expression构建语法树接口隔离原则ISPExpression接口仅定义interpret()精简迪米特法则LoD客户端只与顶级表达式交互不知内部树结构 《Java 23 种设计模式从踩坑到精通》快速导航开篇系列介绍与目录上一篇命令模式 —— 把操作封装成对象实现撤销与排队当前解释器模式 —— 自己动手写一个小语言解释器你在这里下一篇迭代器模式 —— 遍历集合为什么不直接暴露内部结构 即将发布创建型模式汇总单例、工厂、建造者、原型结构型模式汇总适配器、装饰器、代理……行为型模式汇总观察者、策略、模板方法…… 关注《Java 23 种设计模式从踩坑到精通》用 25 篇文章彻底吃透设计模式。福利预告全系列代码及 UML 源码将在完结时统一打包开放点击「关注」「收藏」第一时间获取。下一篇迭代器模式遍历集合为什么不直接暴露内部结构 即将发布敬请关注 除了设计模式我也在深挖智能物流实战WMS、托盘调度、机器学习落地。欢迎点击头像看看专栏 《出版社物流WMS智能调度实战》。技术相通思路可鉴。