06 [实践课]规则引擎设计与实现 | 青训营笔记

90 阅读4分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第7天

学习资料 juejin.cn/post/719336…

课前去简单了解下规则引擎

项目仓库: github.com/qimengxingy…

课程目标

1 理解规则引擎的组成部分及应用场景
2 理解规则引擎的核心原理 - 编译原理的相互概念
3 设计并实现一个规则引擎 - YoungEngine
4 结合之前所学课程, 实现一个Web版规则引擎 (自行实现)

01 认识规则引擎

定义: 规则引擎是一种嵌入在应用程序中的组件, 实现了将业务决策从应用程序代码中分离出来, 并使用预定义的语义模块编写业务决策. 接受数据输入, 解释业务规则, 并根据业务规则做出业务决策

好处: 业务决策与服务本身解耦, 提高服务的可维护性缩短开发路径, 提高效率

组成部分

数据输入
支持接受使用预定义的语义编写的规则作为策略集
比如price>200, 接受业务的数据作为执行过程中的参数, 比如价格, 标签等
规则理解
能够按照预先定义的词法, 语法, 优先级, 运算符等正确理解业务规则所表达的语义
规则执行
根据执行时输入的参数对策略集中的规则进行正确的解释和执行. 同时对规则执行过程中的数据类型进行检查, 确保执行结果正确

应用场景

1 风控对抗
规则引擎作为风控系统的核心, 使产研人员能够不断的调整和优化对抗策略, 以实现最好的风控识别效果
2 活动策略运营
3 数据分析和清洗

02 编译原理基本概念

理解
词法分析: 将源代码字符串转换为词法单元(Token)的这个过程
语法分析: 在词法分析的基础上识别出表达式的语法结构
执行
抽象语法树: 表达式抽象语法结构的树状表示, 抽象语法树一定时, 表达式确定
输入输出:   类型检查: 验证执行的结果是否为合适的数据类型. 在抽象语法树中, 通常会验证某节点的子节点的数据类型是否合法
参数注入: 在规则执行过程中, 使用输入的参数值来计算语法树中的标识符节点值的过程

词法分析 (Lexical Analysis)

 将源代码字符串转换为词法单元(Token)的这个过程

有限自动机(Finite-State Automaton):

有限自动机就是一个状态机, 它的状态数量是有限的. 该状态机在任何一个状态, 基于输入的字符, 都能做一个确定的状态转换

语法分析 (Syntax Analysis)

 语法分析就是在词法分析的基础上, 识别表达式的语法结构的过程

抽象语法树(Abstract Syntax Tree):

表达式的语法结构可以用树表示, 其每个节点(子树)是一个语法单元, 这个单元的构成规则就叫"语法". 每个节点还可以有下级节点

上下文无关语法(Context-Free Grammer):

语言句子无需考虑上下文, 就可以判断正确性, 可以使用巴科斯范式(BNF)来表达

 exp: add;
 add: add '+' mul | mul;
 mul: mul '*' pri | pri;
 pri: string | bool | number | identifer;

产生式: 一个表达式可以由另外已知类型的表达式或者符号推导产生

内置符号: 字面量(string, bool, number) 标识符, 运算符

一个基础表达式可以由常量或标识符

一个乘法表达式可以由基础表达式或者乘法表达式*基础表达式组成

... ...

递归下降算法(Recursive Descent Parsing):

递归下降算法就是自顶向下构造语法树

不断的对Token进行语法展开(下降), 展开过程中可能会遇到递归的情况

 例
 exp: log
 log: log ('&&'|'||') cmp | cmp
 cmp: cmp '>' add | add
 add: add '+' mul | mul
 mul: mul '*' pri | pri
 pri: const | id | (exp)
 用上述范式, 为price > 500 && ( isNewUser || userLevel > 5 )构造语法树

类型检查

类型综合
根据子表达式的类型构造出父表达式的类型
编译时检查 & 运行时检查

03 设计一个规则引擎

设计目标

 设计一个规则引擎, 支持特定的词法, 运算符, 数据类型和优先级. 并且支持基于以上预定义语法的规则表达式的编译和执行

 词法 (合法Token)
     参数: 由字母数字下划线组成: eg: _ab2, user_name
     布尔值: true, false
     字符串: "abcd"
     十进制int: 1234
     十进制float: 123.5
     预定义运算符: + -
 运算符
     一元运算符: + -
     二元运算符: + - * / % > < >= <= == !=
     逻辑操作符: && || |
     括号: ()
 数据类型
     字符串
     布尔值
     十进制int
     十进制float
 优先级
     0 ||
     1 &&
     2 ! - +
     3 > >= < <= == !=
     4 + -
     5 * /
     6 ( )
 语法分析
 expr: logOr EOF;
 logOr: logOr '||' logAnd | logAnd;
 logAnd: logAnd '&&' logNot | logNot;
 logNot: '|' logNot | cmp;
 cmp: cmp '>' add | cmp '>=' add | cmp '<' add | cmp '<=' add | cmp '==' add | cmp '!=' add | add;
 add: add '+' mul | add '-' mul | mul;
 mul: mul '*' pri | mul '/' pri | mul '%' pri | pri;
 pri: BooleanLiteral|IntegerLiteral|FloatLiteral|StringLiteral|Identifier|'('expr')';
 优先级的表达
 type precedence struct {
     validSymbols []Symbol // 当前优先级支持的运算符类型
     nextPrecedence *precedence // 更高优先级
     planner planner // 当前优先级的处理函数
 }

语法树结构
一元运算符: 左子树为空, 右子树为操作数
二元运算符: 左子树为左操作数, 右子树为右操作数
括号: 左子树为空, 右子树为内部表达式的AST

语法树执行与类型检查
语法树执行
预先定义好每种操作数的执行逻辑
对抽象语法树进行后序遍历, 即:
先执行左子树, 得到左节点的值
再执行右子树, 得到右节点的值
最后根据根节点的操作符执行得到根节点的值
类型检查
检查时机: 执行时检查
检查方法: 在一个节点的左右子节点执行完成后, 分别校验左右子节点的类型是否符合对应操作符的类型检查预设类型
如'>'符号要求左右子节点的值都存在, 且为int或float
'!'符号要求左节点为空, 且右节点的值为bool

04 规则引擎的实现

项目目录

image.png