首页 / 资讯中心 / 文章详情

解释器模式实战:手写规则表达式解析器,让业务过滤告别if-else

解释器模式实战:手写规则表达式解析器,让业务过滤告别if-else ★ FEATURED ARTICLE
写业务代码久了你会发现一个很普遍的规律大部分系统的复杂度不是来自某个高深算法而是来自海量的、纠缠不清的“判断逻辑”。最常见的就是一堆if-else按条件分流今天加一种促销类型明天加一种渠道来源后天再补一个黑名单校验。每次接新需求都小心翼翼生怕在某条分支里多踩一个坑。直到有一天产品跑过来说“我们希望运营能在后台自己配置过滤条件比如满足某些条件的用户才发券。”那一刻就绕不开解释器模式了。解释器模式不是一个高频使用的设计模式但每次用到都是解决真正棘手的问题。SQL解析、正则表达式匹配、规则引擎、数学公式计算、模板语法渲染底层都是解释器模式的思路。它核心就做一件事定义一种语言通常是一串有特定格式的字符串然后把这种语言的句子“翻译”成程序能够执行的结构。这篇文章我会用业务场景里最常见的规则表达式做例子从零手写一个可运行的表达式解释器把解释器模式的组件、设计思路、性能优化和避坑经验全部摊开讲清楚希望能帮那些正准备写自定义规则、可配置过滤条件的同学减少走弯路的时间。1. 解释器模式到底在解决什么问题1.1 一个让代码“活”起来的场景运营要自己配过滤条件我举个具体的例子。假设我们做一个用户运营系统运营每次发优惠券之前都要筛选用户筛选条件大概长这样近30天订单金额大于100并且累计订单数大于3或者用户等级是VIP。刚开始就这么三类固定条件代码很好写if (conditionA conditionB) { sendCoupon(user); }需求一变就麻烦了。运营提出来我们想把“近30天订单金额大于100且累计订单数大于3或用户等级是VIP”这条规则配置成文案不同城市的运营可以自己组合条件。于是后台需要提供一个表达式编辑框运营可以手动输入( amount 100 count 3 ) || level 6这时候你发现原来的硬编码判断逻辑突然不够用了。你需要让程序有能力去“读”这段字符串并且还要“根据用户的实际数值去判断真假”。该怎么办最粗暴的做法是正则表达式解析字符串再按运算符拆分支。但你会发现表达式一复杂括号、与或非嵌套、优先级问题一起来用正则完全扛不住。这时候解释器模式就该登场了。它把表达式解析成一个“语法树”然后在树上一路解释执行。每一个节点负责一小段判断组合起来却表达出完整的规则体系。1.2 解释器模式的准确定义与最通俗的理解解释器模式的官方定义是给定一个语言定义它的文法的一种表示并定义一个解释器这个解释器使用该表示来解释语言中的句子。听着很绕。我用翻译做个类比。你把一句中文“我明天去上海”交给翻译翻译会先做词法分析主语“我”、时间“明天”、动作“去”、地点“上海”然后根据英语语法组装成“I will go to Shanghai tomorrow”。解释器模式做的事就是语法规则定义了怎么把一个句子拆解成短语树解释器遍历这棵树并执行语义。放在代码里这里的“句子”就是用户输入的某段规则字符串“文法”就是我们约定好的表达式规范比如支持大于、小于、与、或支持变量而“解释器”就是那棵节点对象构成的语法树树里每个节点都知道自己该怎么计算。用设计模式的口吻说解释器模式适合解决“某种固定文法的小型语言的解释”问题。注意关键词固定文法、小型语言。如果需求里的语法有数十种运算符且各种扩展千奇百怪优先考虑成熟的解析器生成工具而不是纯手工解释器。模式有它的边界这个后面专门讲。1.3 四个标准角色上下文、抽象表达式、终结符与非终结符解释器模式有四个核心角色先认识它们后面写代码你会直接对应上。抽象表达式Abstract Expression声明一个抽象的interpret(上下文)接口是所有表达式节点的统一入口。后面代码里的Expression类就是它。终结符表达式Terminal Expression语法树里的叶子节点。它不包含其他子节点比如表达式里的变量、数值常量。解释时直接返回一个结果。非终结符表达式Nonterminal Expression语法树里的非叶子节点。比如“大于运算表达式”、“并且运算表达式”。它内部组合了子表达式解释时先解释子表达式再组合出结果。上下文Context承载解释所需的外部信息比如变量名对应的值、常量表甚至共享的临时状态。很多实现里上下文就是普通的MapString, Object。结构上就是一棵树树根是一个非终结符表达式往下层层派生叶子是终结符表达式。解释时从根节点开始递归调用interpret。到这里模式骨架已经清晰了接下来用一个完整项目把它落地。2. 从零手写一个规则表达式解释器2.1 先把需求说清楚为了不让文章停在概念层我们要实际做一个能在业务里跑的配置化过滤表达式解释器。功能设定如下支持数字和字符串变量例如 amount、count、level、city。 比较运算符支持 、、、、、!。 逻辑运算符支持 、||、!。 优先级从高到低依次是 !、比较、、||支持括号覆盖优先级。 能处理形如( amount 100 count 3 ) || level VIP的表达式。这基本是业务规则引擎的常用子集。等写完这个你把它扩展成JSON筛选、灰度策略规则、字段级权限规则都完全可行。先看最终效果context Context({amount: 200, count: 5, level: VIP, city: Beijing}) expr Parser(( amount 100 count 3 ) || level \VIP\).parse() print(expr.interpret(context)) # True这个实现拆分两部分语法解析字符串 - 抽象语法树和解释执行遍历语法树计算布尔结果。这两部分在工程上最好分开否则以后扩展一个新运算符逻辑耦合在一起会乱得想哭。2.2 建立上下文与表达式节点先实现解释器模式最底层的两个角色上下文Context与抽象表达式Expression。class Context: def __init__(self, variables: dict): self.variables variables def get(self, name: str): if name not in self.variables: raise KeyError(f变量 {name} 未在上下文中定义) return self.variables[name]Context做一个薄封装即可。它的本意就是“解释时向外部取数”不要在里面堆业务逻辑。抽象表达式接口class Expression: def interpret(self, context: Context): pass接着写两个终结符表达式变量表达式和常量表达式。class VariableExpression(Expression): def __init__(self, name: str): self.name name def interpret(self, context: Context): return context.get(self.name) class ConstantExpression(Expression): def __init__(self, value): self.value value def interpret(self, context: Context): return self.value这段很简单但它解释了一个关键点为什么终结符表达式也是对象而不是直接取变量值因为统一接口。非终结符在解释时不需要区分子节点是变量还是常量只要调用interpret就能拿到值。这就是多态的价值。2.3 比较与逻辑表达式的实现接下来是非终结符表达式。比较表达式内部组合左右两个子表达式解释时先取两个值然后按运算符比较。class CompareExpression(Expression): def __init__(self, op: str, left: Expression, right: Expression): self.op op self.left left self.right right def interpret(self, context: Context): lv self.left.interpret(context) rv self.right.interpret(context) if self.op : return lv rv if self.op : return lv rv if self.op : return lv rv if self.op : return lv rv if self.op : return lv rv if self.op !: return lv ! rv raise RuntimeError(f未支持比较运算符: {self.op})逻辑运算表达式class AndExpression(Expression): def __init__(self, left: Expression, right: Expression): self.left left self.right right def interpret(self, context: Context): return self.left.interpret(context) and self.right.interpret(context) class OrExpression(Expression): def __init__(self, left: Expression, right: Expression): self.left left self.right right def interpret(self, context: Context): return self.left.interpret(context) or self.right.interpret(context) class NotExpression(Expression): def __init__(self, expr: Expression): self.expr expr def interpret(self, context: Context): return not self.expr.interpret(context)看到没有解释器模式的高级之处在于每个节点只需要管自己的计算逻辑节点之间的组合关系由语法树结构自然表达。以后要扩展一个新逻辑运算比如“异或”加一个XorExpression类即可完全不影响已有节点。2.4 词法分析与语法解析把字符串变成语法树光是表达式节点还不够用户输入的是字符串怎么变成节点对象这需要两个步骤词法分析Tokenizer和语法分析Parser。词法分析把原始字符串拆成语义单元Token比如(,amount,,100,。这里特别注意字符串常量的引号性别容易在这里翻车。import re def tokenize(s: str): spec [ (LPAREN, r\(), (RPAREN, r\)), (OP, r|||!||||\|\|), (NOT, r!), (STRING, r[^]*), (NUMBER, r\d\.?\d*), (VAR, r[a-zA-Z_][a-zA-Z0-9_]*), (SPACE, r\s), ] tokens [] pos 0 while pos len(s): for name, pattern in spec: m re.match(pattern, s[pos:]) if m: if name ! SPACE: value m.group(0) if name STRING: value value[1:-1] tokens.append((name, value)) pos m.end() break return tokens词法分析没太多高深内容关键是Token类型要分清楚运算符、变量、常量、括号。其中正则匹配的顺序会影响结果比如必须放在前面匹配否则会把拆成和这是新手常踩的坑。语法分析用递归下降法实现。层次从上往下并或优先级最低对应OrExpression下一层是“与”表达式AndExpression下一层是非运算符NotExpression最后一层是原子表达式和括号。class Parser: def __init__(self, text: str): self.tokens tokenize(text) self.pos 0 def peek(self): return self.tokens[self.pos] if self.pos len(self.tokens) else None def consume(self, token_typeNone): tk self.peek() if token_type and tk and tk[0] ! token_type: raise SyntaxError(f期望 {token_type}实际 {tk[0]}) if tk: self.pos 1 return tk def parse(self) - Expression: if not self.tokens: raise SyntaxError(表达式为空) expr self.parse_or() if self.peek() is not None: raise SyntaxError(f存在无法解析的Token: {self.peek()}) return expr def parse_or(self) - Expression: left self.parse_and() while self.peek() and self.peek()[0] OP and self.peek()[1] ||: self.consume() right self.parse_and() left OrExpression(left, right) return left def parse_and(self) - Expression: left self.parse_not() while self.peek() and self.peek()[0] OP and self.peek()[1] : self.consume() right self.parse_not() left AndExpression(left, right) return left def parse_not(self) - Expression: if self.peek() and self.peek()[0] NOT: self.consume() inner self.parse_not() return NotExpression(inner) return self.parse_atom() def parse_atom(self) - Expression: tk self.peek() if tk and tk[0] LPAREN: self.consume() expr self.parse_or() self.consume(RPAREN) return expr left self.parse_value() op_tk self.consume() if op_tk[0] ! OP: raise SyntaxError(f期望比较运算符实际 {op_tk}) right self.parse_value() return CompareExpression(op_tk[1], left, right) def parse_value(self) - Expression: tk self.consume() if tk[0] NUMBER: return ConstantExpression(float(tk[1]) if . in tk[1] else int(tk[1])) if tk[0] STRING: return ConstantExpression(tk[1]) if tk[0] VAR: return VariableExpression(tk[1]) raise SyntaxError(f无法识别值类型: {tk})递归下降法本质上是把文法规则直接翻译成一组互相递归调用的函数每个函数对应一个优先级层级。parse_or调用parse_andparse_and调用parse_not以此确保优先级顺序不会错乱。这个过程和解释器模式是绝配语法分析阶段的每种文法规则对应解释器阶段的一个表达式节点。2.5 完整流程测试与踩坑记录把上面所有类放一起跑几个用例。if __name__ __main__: cases [ (amount 100 count 3, True), (amount 50 || level \VIP\, True), (!(amount 100) level ! \normal\, False), (amount 100 (count 5 || level \VIP\), True), ((amount 100 count 3) || level \VIP\, False), ] for text, expected in cases: ast Parser(text).parse() result ast.interpret(Context({amount: 200, count: 5, level: VIP})) print(f{text} {result} | 期望 {expected})跑下来结果全部符合预期。不过我在调试过程中碰到几个容易翻车的地方Token正则顺序不能乱和必须排前面否则会匹配成两个Token。我一开始把写在前面结果解析直接报语法错误。字符串常量去引号要在词法层做掉不要在解释层做。如果留在解释层每次比较前都要判断类型侵入性很强。词法层解决完解释层可以完全无感。这一条建议养成习惯能把语法细节越早消化掉越好别把脏活留给解释器。2.6 解析和解释一定要分家很多人初次接触解释器模式容易把解析和解释混在一起写一边读字符一边算结果。这个做法短小的表达式勉强能跑一旦表达式带括号、带优先级马上失控。我的经验是哪怕是几十行的微型DSL也要严格分成解析阶段和解释阶段。解析阶段返回语法树解释阶段只接受语法树。这样带来三个直观好处语法树可以复用。同一份规则字符串只需要解析一次然后放在缓存里供不同上下文的用户重复执行性能收益非常大。 调试友好。表达式算错时把语法树打出来一眼能看到节点组合关系比在混乱的字符串指针里找bug高效太多。 扩展容易。新增一种运算符只需要修改解析函数和表达式类不需要动解释执行框架。而解释框架其实是稳定的始终是“递归调用interpret”。如果你把解释器模式理解为“一个能自己执行的语法树”那就等同于把“解析和解释”绑死了。正确理解是解释器模式关注的是解释阶段的对象组织方式而解析阶段本质上是独立的前置流程。很多讲设计模式的书把两者混在一起讲是造成大家难落地的主要原因。3. 模式背后的设计玄机与取舍3.1 为什么用类来表达文法规则一门语言有文法规则比如“一个或表达式由一个或两个与表达式通过逻辑或连接”。不用类的话你用一堆if-else也能处理Token流。但一旦文法变丰富分支会爆炸。用类表达文法规则本质上是把“文法规则的种类”变成了“类的种类”。每个节点类负责一种规则的解释行为职责单一组合关系就是对象引用。这是组合优于继承的经典应用。你不需要在一个长函数里维护一堆分支判断而是让对象自己决定自己的行为。这类风格的代码将来加规则时对已有代码的修改量趋近于零只需要加新类和新解析逻辑。3.2 终结符与非终结符的协作本质终结符表达式是树上的叶子它本身不会递归调用子节点只直接返回一个结果。非终结符表达式是内部节点它持有子节点的引用解释时先让子节点各自解释再汇集结果。整棵树的执行过程是一个深度优先遍历。以amount 100 count 3为例根是AndExpression树的左子树是CompareExpression(a 100)树的右子树是CompareExpression(count 3)执行时先解释左子树先解释amount变量得到200解释常量100得到100比较得到true再解释右子树得到true根节点做逻辑与返回true这个执行模型极度直观可推导可单测。如果树中任何节点的结果和预期不一致只要单独执行该节点就能定位问题。3.3 上下文把“外部世界”和“语法树”解耦上下文是解释器模式里最容易被低估的角色。它本质上是语法树与外部数据之间的一层隔离。语法树里的VariableExpression不持有实际数据只持有变量名具体值从Context中按名获取。好处很明显同一个语法树可以作用于不同的Context也就是同一套规则可以批量应用在成千上万条业务数据上。模板渲染引擎能复用编译后的模板正是这个道理。语法树本身不依赖运行环境可以序列化保存下次启动直接加载AST省去解析成本。Context里理论上还能放一些共享中间结果比如子表达式的缓存结果以空间换时间。这个优化在某些递归重的场景特别有效。例如(a b) || (a c)如果Context记下子表达式结果可以避免重复计算。3.4 与组合模式的关系学解释器模式时一定要看组合模式。因为语法树本身就是一棵部分-整体的层次结构这点和组合模式是重叠的。区别在视角组合模式强调的是“部分与整体保持一致”容器节点与叶子节点对客户端暴露同样的接口解释器模式强调的是“通过解释接口来计算复杂表达式的值”。换句话说解释器模式是组合模式在特定语义领域中的应用。理解这一点能帮你把两个模式都掌握得更透。当你写一个解析器时你构造的树本质上就是一个组合结构而每一个子节点解释自身行为就是组合模式里每个叶子执行特定操作。3.5 模式的核心缺陷也要心里有数解释器模式不是银弹。它最大的问题是每个文法规则对应一个类规则越多类越多。如果表达式有20种运算符、5种值类型很容易膨胀出几十个类项目瞬间变重。其次递归解释可能带来性能瓶颈尤其表达式很长、上下文数据量很大时。最后一个缺点是如果没有清晰的分层解析和解释逻辑纠缠在一起整个模块会迅速腐烂。所以选型最重要的问题不是“这个模式怎么用”而是“这个场景该不该用”。接下来专门聊选型。4. 什么时候用什么时候不用做技术选型要清醒4.1 适合使用解释器模式的信号我做了几年业务系统的感受是下列信号同时出现时解释器模式是很好的选择需求里真的出现了一种“语言”。比如运营配置的表达式、审批流配置的条件、报表的过滤条件。这里的语言不一定是很严肃的自然语言哪怕只是几张有限的句子格式也构成了语言。 这种语言的规则频繁变化或高度组合。固定两个条件用if-else就好但允许任意组合并且未来会增加新运算符时硬编码不堪重负。 表达式会被重复解析执行。例如一个规则要批量作用于数万条用户数据一次性构造语法树后续只解释不解析收益很大。 你对语法的扩展方向有预期。如果明确知道三个月后会加入新的比较运算或逻辑运算那么解释器模式的扩展模式非常契合。4.2 什么情况千万别用反过来很多场景用解释器模式其实是过度设计。比如表达式形态固化成几种没有组合需求直接if-else就能解决。 写法上只是为了躲避一两个条件分支你把模式硬套上去反而不如一段简单函数清晰。不要为了用设计模式而用设计模式这句话相信每一个被维护祖传代码折磨过的人都懂。 没有技术团队能长期维护递归下降解析器。一套递归下降解析器需要对应的单元测试、边界case和文档积累。如果团队频繁换人又不写测试手写解释器会成为后人的噩梦。 这时可以选一些成熟的表达式引擎库或者干脆用现有的配置项组合。下面这个表格可以帮你快速判断信号倾向用解释器模式倾向别用表达式组合能力需要任意嵌套组合只有固定几种组合语法规则变化频率频繁新增运算/操作半年都不变执行重复度一个规则要跑大量数据每个规则只跑一次团队维护能力有完整测试和代码评审流浪代码没人维护工具链现状无合适现成引擎已有成熟规则引擎可用4.3 与策略模式、模板方法、状态模式的区别解释器模式经常被拿来做对比的还有策略模式、模板方法模式、状态模式。策略模式解决的是“算法可以互换”适合同一类行为不同实现比如多种排序策略。它缺少“文法组合”能力。如果只需要在两到三种算法里挑一种用策略模式就好根本没有必要解释一个表达式。 模板方法模式解决的是“算法的骨架固定部分步骤延迟到子类实现”关注的是流程的复用而非语言的解释。如果你说“我要套一组固定流程但每步可以微调”那用模板方法。 状态模式解决的是“对象状态不同行为不同”它强调的是状态驱动流转适合状态机类业务。如果条件解析是基于当前状态的转移状态模式会比解释器更顺手。真正的解释器模式和策略模式的结合倒是常见的。例如一个复杂的组合规则里某些叶子节点对应的行为是一个策略。这样的话解释器负责把条件组合串起来策略负责叶子节点的执行结果双方各司其职。4.4 我遇到的一个“不该用却用了”的教训以前某公司有个需求后端给前端传一段简单的过滤条件前端根据条件渲染隐藏某些UI组件。条件只有三种用户是内部员工、用户等级大于某个值、用户所在城市白名单。那时我刚学完解释器模式热血上头写了一个完整的递归下降解析器支持各种组合条件。结果呢需求经过大半年真的只用了三种固定条件从来没有任何组合出现。维护者每次改需求看着解析器里的一堆类都头疼。后来重构时把它换成了三行if-else。这个经历给我的教训很真诚模式是工具不是装饰。一个if-else都写不明白就不要硬上解释器。只有在语言成为产品功能的一部分时才值得动用这套体系。5. 工程化落地与性能优化5.1 解释器模式的性能瓶颈在哪里性能问题要从解析和解释两段分别看。解析阶段的成本主要在词法分析和语法分析的字符遍历以及为每个节点创建对象。表达式越长Token越多对象越多解析时间越长还伴随一定的内存占用。解释阶段的成本主要是频繁的递归调用和节点对象的方法调用。相比直接写成一行if-else这样间接跳转带来的性能开销肯定大。对于业务系统来说单次表达式的解释往往是微秒级别不是问题。但假如你用一套规则去过滤上百万条数据每条数据都来一次语法树解释性能就会被放大。5.2 三个立竿见影的优化手段一是缓存AST。同一段表达式文本只解析一次后续任何上下文都复用这棵树。这是所有优化里最见效的一条。我曾经把一个报表系统做优化原来每次查询都重新解析表达式改成AST缓存后总体响应时间下降约40%并且后续复用越多次收益越大。二是解释器内部缓存上下文结果。比如同一棵语法树在处理大量数据时某部分子表达式的结果可能在整个批次内稳定不变就可以在第一次算完后缓存到Context里后续命中直接返回。这个优化点要结合业务情况看如果数据变化频繁这个缓冲反而会成为脏数据根源慎用。三是共享节点。对于重复出现的相同结构比如多个规则里都有amount 100这段子树可以缓存这个子树对象复用减少内存占用。对于终结符表达式甚至可以通过享元模式共享同一个变量表达式节点。5.3 工程案例把报表过滤条件做成可配置DSL我们之前做一个数据分析平台每个报表页支持用户自定义过滤条件条件是字符串表达式例如channel app (amount 1000 || pay_count 3)下单时间戳直接存字符串表达式每次查询都现解析性能惨不忍睹。后来按解释器模式重构流程是后台配置页面编辑表达式 → 保存前调解析器校验并生成AST → AST序列化存入数据库 → 查询时加载AST直接解释执行。这样最耗时的解析过程被前置到了配置保存阶段查询阶段只做解释。用户体验从“改完配置要等一秒”变成“秒开”效果非常明显。另一个收益是校验前置有效地拦截了大量非法表达式运营再也不会因为配错规则导致数据全空。这个案例可以给大家一个重要思路解释器模式不一定非要在运行时解析字符串。只要能序列化语法树就能把解析成本从热路径里踢出去。这在工程上价值极大。5.4 自己写解释器还是引第三方规则引擎每逢聊到解释器模式肯定有人问“现在都有很多成熟规则引擎了为什么还要手写”答案是看需求边界。如果需要支持的语言很小、很定制比如就三四种运算符引一个重量级规则引擎引入大量依赖和复杂配置纯属杀鸡用牛刀。而如果你需要的表达能力接近SQL还是趁早引一个成熟的解析库手写一个完整SQL解释器不是闹着玩的。我通常按这个原则做决策场景复杂度建议方案简单比较逻辑3~6个运算符自己写解释器核心代码200行内中等复杂度多类型操作语法20种以上结合成熟的表达式解析库接近SQL、脚本语言用专用语言引擎不要自研大多数业务判断其实都是配置化的布尔表达式手写解释器就非常够用。太复杂的需求团队的时间和精力应该花在业务本身上而不是再造一个语言。6. 常见问题与避坑经验6.1 递归过深导致栈溢出解释器模式天然依赖递归。表达式嵌套层数过深时比如五百层括号的构造Python或者Java的递归调用栈有可能会爆。实际业务里不太会出现五百层括号但输入来自前端时尤其要注意有人故意提交恶意表达式把服务打挂。解决办法有三层。词法分析或解析阶段就限制最大深度超过阈值直接抛异常。这属于风控手段非常重要。解析时限制Token总数表达式长度限流。必要时可以把递归解析改成显式栈的迭代解析这个做法复杂度较高一般建议前两种先上。6.2 上下文线程安全解释器执行时如果上下文是多个线程共享的并且有中间缓存就要加锁或保证Context不可变。反过来如果每次线程用独立Context语法树本身只读不修改就可以放心并发执行。这是解释器模式在并发环境里最需要注意的一点。区分关键是什么。语法树可以共享上下文不能共享。让上下文携带每次执行的独立状态把全局共享信息全部放进不可变常量里。6.3 异常信息定位难语法解析时报错通常只会告诉人“第几行第几列语法错误”对非开发者不友好。手写解释器不给足错误上下文就成了灾难。建议所有节点对象都在构造时带上原始Token的行列号或偏移量。解释执行报错时不仅能提示“运行时错误”还能直接指出是表达式里哪一段出错。对于运营系统来说这种可读的报错能力比优化性能更提升体验。调试的时候把AST结构用缩进树打印出来是非常科学的办法。每个节点打印自己的类型、关键字和子节点。我经常靠这个一眼找到优先级或括号匹配问题。完整的系统里我会给AST加一个to_tree_string方法用字符串缩进展示层次结构测试失败时也方便对比。6.4 运算符优先级最容易翻车优先级处理是手写解析器的高频事故区。核心原则是每降低一个优先级层级就多一层函数嵌套。!比比较优先级高所以parse_not在parse_atom之前处理比||优先级高所以parse_and在parse_or里被调用。调试优先级问题最好的办法就是为每个层级写独立的单元测试。例如true || false false应该结果为true因为先算。!true false应该结果为false因为!优先级高。如果结果和你心智模型对不上打印AST树看层级关系一目了然。关于等号与比较运算用比较两个浮点数值时浮点误差可能造成意外结果业务上建议在词法阶段把常量归一化。如果规则里大量使用精确数值判断用Decimal替代float。6.5 综合检查清单给你一份我实践中沉淀的检查清单写解释器之前过一遍表达式文本是否需要进行长度与复杂度限制限制措施在词法层还是网络层完成语法树是否可以序列化能够从存储加载而非运行时解析上下文设计是否保证语法树无状态能不能并发解释每个节点有没有完整错误信息偏移量、节点类型、原始输入片段涉及浮点数值比较时是否已经做了精度归一化是否准备好递归深度限制或迭代版本防止恶意表达式拖垮进程是否区分了解析阶段和解释阶段让AST可复用新增运算符时解析器和节点类都能独立扩展完全不需要修改已有节点吗按这个清单走一遍基本不会在工程项目里把解释器模式写残。最后分享一个我调整过很多次的理解方式解释器模式其实是在为问题空间“发明一门很小的语言”然后再把这门语言翻译成对象行为。真正的复杂度从来不在于那几十个表达式类而在于你是否搞清楚了你自己定义的语言边界。规则越明确边界越清晰这个模式的威力越大。如果你能从自己的业务中察觉出“有人在用字符串表达某种规则”那么恭喜你你已经知道什么时候该掏出解释器模式了。
阅读完成 · 觉得有帮助?
咨询建站