这是我参与「第五届青训营 」笔记创作活动的第3天
大家都有做过需要繁杂判断的大讨论题,随着规则的增加代码会变得越来越复杂,同时也意味着可维护性、可靠性直线下降。于是,将规则判断这部分逻辑抽离出业务代码,自然而然地成为了一个新的需求。
规则引擎
定义
将业务决策从程序代码中分离出来,并使用预定义的语义模块编写业务决策。
场景
- 风控对抗
- 活动策略运营
- 数据分析和清洗
组成部分
- 数据输入
- 规则理解
- 规则执行
编译原理
编译原理本就是一门分析词法、语法的课程,与规则引擎的需求十分类似。所以我们可以使用编译原理的相关知识去构建一个规则引擎。此前我认为编译原理只是一个八股学科,没想到在业务中还有这种使用方式。
词法分析 Lexical Analysis
词法分析就是把源代码字符串转换为词法单元(Token)的过程。
如何识别Token?有限自动机(Finite-State Automaton)
有限自动机就是一个状态机,在任何一个状态,基于输入的字符,都能做一个确定的状态转换。
这里的状态是什么?通过一系列中间状态的转移后,最终得到一个token的状态,以此实现词法分析。
语法分析 Syntax Analysis
在词法分析基础上,识别表达式的语法结构
比如一个操作符需要左操作数和右操作数
抽象语法树 Abstract Syntax Tree :表达式的语法结构用树表示,每个节点(子树)是一个语法单元,这个单元的构成规则就叫做“语法”。每个节点可以有下级节点。
上下文无关语法 Context-Free Grammar:无需考虑上下文,就可以判断正确性。可以使用巴科斯范式(BNF)来表达。巴科斯使用递归下降算法(Recursive Descent Parsing),自顶向下构造语法树,不断的对Token进行语法展开(下降),过程中可能遇到递归。
通过递归下降算法,实际上根据递归深度也实现了优先级。优先级高的会先被判断,如果不满足就向下递归,直到找到符合的语法。
类型检查:
- 类型综合:根据子表达式的类型构造出父表达式的类型。
- 编译时检查&运行时检查:可以在构造语法树的阶段,也可以在执行时的阶段。
- 编译时:提前声明参数的类型,在构建语法树过程中检查
- 运行时检查:根据执行时的参数输入的值类型,在执行过程中检查。
设计规则引擎
设计目标
- 词法分析
- 词法(合法Token)
- 运算符
- 数据类型
- 优先级
- 设计词法分析的状态机
- 语法分析
- 优先级的表达
- 语法树结构:一元运算符,二元运算符,括号。。。(可以使用二叉树实现)
- 语法树执行与类型检查
- 语法树执行:后序遍历语法树,即先执行左子树,再执行右子树,最后根据根节点的操作符得到根节点的值
- 类型检查
- 检查时机:执行时检查/编译时检查
- 检查方法:对一个节点的左右子节点执行完后,分别校验左右子节点的类型是否符合对应操作符的类型检查预设规则,比如:'>'要求左右节点值都存在且为int或float,'!'要求左节点为空且右节点为bool。
实现规则引擎
词法分析
先对词素进行常量的定义,然后定义一个结构去保存token,然后就可以进行词法分析。
对字符串进行扫描,预期结果就是输出所有的token。而在扫描的过程中,首先把空格全部跳过,然后就实现状态机的流程。
想想就能感觉到,实现状态机的流程比较复杂。所以需要进行良好的封装,把基本的处理字符串的方法封装起来,关注于实现状态上。
实现完状态机后,需要进行一些检查,比如检查表达式是否平衡。
语法分析
构造语法树,然后使用递归下降算法,后序遍历语法树。当然会有意外的情况,需要对树进行特殊的调整。比如将负数变成减号,来保证树的结构。
得到根节点的值之后,就需要做类型检查,看结果是否符合该节点的预期。
至于为什么要做类型检查,可以考虑一下平时写代码的时候有没有遇到过类型检查不通过的情况。也就是说,即使满足了语法,也有可能出现类型上的错误。