6.规则引擎设计与实现 | 青训营笔记

213 阅读6分钟

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

一、本堂课重点内容

  1. 了解规则引擎的组成部分和应用场景。
  2. 学习并掌握规则引擎的设计与实现原理。
  3. 明确一个规则引擎的设计目标,并完成各部分的设计与实现步骤拆解。
  4. 动手实现规则引擎项目,完成预定目标。

二、详细知识点介绍

规则引擎

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

引入规则引擎能解决开发人员重复编码的问题。业务决策与服务本身解耦,提高服务的可维护性,缩短开发路径,提高效率。

词法分析

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

确定的有限自动机 DFA | Deterministic Finite Automaton

DFA

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

语法分析

词法分析是识别一个个的单词,而语法分析就是在词法分析的基础上识别出程序的语法结构。这个结构是一个树状结构。这棵树叫做抽象语法树(Abstract Syntax Tree,AST)。树的每个节点(子树)是一个语法单元,这个单元的构成规则就叫“语法”。每个节点还可以有下级节点。

Token -> AST

上下文无关语法 Context-Free Grammar

语言句子无需考虑上下文,就可以判断正确性

...
a = 0;
// 这是一个赋值语句,无论此语句的前后是什么代码,此语句所代表的操作是确定的。即给变量a赋值等于0
...

编程语言为什么不用人类的语言(自然语言),而是用上下文无关的文法呢? 因为

  1. 便于设计编译器。 客观上技术目前无法实现,如果使用了上下文相关文法,那就是真正实现了人工智能,NLP领域将会有重大突破。
  2. 便于代码开发维护。 如果开发出来的代码像高考的语文阅读理解一样,每个人都有不同的理解,那么,到底哪个才是作者真正想要表达的?如果人类都确定不了含义,那计算机同样也确定不了,最终结果就是错误执行或无法执行。
  3. 汇编语言/机器语言是上下文无关的。CPU执行指令时,读到哪条执行哪条。如果CPU需要考虑上下文,来决定一个语句到底要做什么,那么CPU执行一条语句会比现在慢千倍万倍。考虑上下文的事情,完全可以用户在编程的时候用算法实现。既然机器语言是上下文无关的,那高级语言也基本上是上下文无关的,可能有某些个别语法为了方便使用,设计成了上下文相关的,比如脚本语言的弱类型。在便于使用的同时,增加了解析器的复杂度。

上下文无关语法G:终结符集合T + 非终结符集合N + 产生式集合P + 起始符号S

G由T、N、S和P组成,由语法G推导出来的所有句子的集合称为G语言!

终结符: 组成串的基本符号。可以理解为词法分析器产生的token集合。比如 + Id ( ) 等

非终结符: 表示token的的集合的语法变量。比如 stmt varDecl 等等

start:blockStmts ;               //起始
block : '{' blockStmts '}' ;      //语句块
blockStmts : stmt* ;              //语句块中的语句
stmt = varDecl | expStmt | returnStmt | block;   //语句
varDecl : type Id varInitializer? ';' ;         //变量声明
type : Int | Long ;                              //类型
varInitializer : '=' exp ;                       //变量初始化
expStmt : exp ';' ;                              //表达式语句
returnStmt : Return exp ';' ;                    //return语句
exp : add ;                                      //表达式       
add : add '+' mul | mul;                         //加法表达式
mul : mul '*' pri | pri;                         //乘法表达式
pri : IntLiteral | Id | '(' exp ')' ;            //基础表达式 

产生式:表示形式,S : AB ,就是说S的含义可以用语法AB进行表达

S : AB
A : aA | ε
B : b | bB

展开(expand):将P(A->u )应用到符号串vAw中,得到新串vu **w

折叠(reduce):将P(A->uu )应用到符号串vuu w中,得到新串vAw

推导(derivate):符号串u 应用一系列产生式,变成符号串v ,则u =>v:S => ab | b | bb

巴科斯范式

BNF是描述上下文无关理论的一种具体方法,通过BNF可以实现上下文无关文法的具体化、公式化、科学化,是实现代码解析的必要条件。

<expr> ::= <expr> + <term>
         | <expr> - <term>
         | <term>

<term> ::= <term> * <factor>
         | <term> / <factor>
         | <factor>

<factor> ::= ( <expr> )
           | Num

BNF本质上就是树形分解,分解成一棵抽象语法树:

  • 每个产生式就是一个子树,在写编译器时,每个子树对应一个解析函数。
  • 叶子节点叫做 终结符,非叶子节点叫做 非终结符。

递归下降算法 Recursive Descent Parsing

基本思路就是按照语法规则去匹配 Token 串。比如说,变量声明语句的规则如下:

varDecl : types Id varInitializer? ';' ;        //变量声明
varInitializer : '=' exp ;                       //变量初始化
exp : add ;                                      //表达式
add : add '+' mul | mul;                         //加法表达式
mul : mul '*' pri | pri;                         //乘法表达式
pri : IntLiteral | Id | '(' exp ')' ;            //基础表达式

如果写成产生式格式,是下面这样:

varDecl -> types Id varInitializer ';'
varInitializer -> '=' exp
varInitializer -> ε
exp -> add
add -> add + mul
add -> mul
mul -> mul * pri
mul -> pri
pri -> IntLiteral
pri -> Id
pri -> ( exp )

而基于这个规则做解析的算法如下:

匹配一个数据类型(types)
匹配一个标识符(Id),作为变量名称
匹配初始化部分(varInitializer),而这会导致下降一层,使用一个新的语法规则:
   匹配一个等号
   匹配一个表达式(在这个步骤会导致多层下降:exp->add->mul->pri->IntLiteral)
   创建一个varInitializer对应的AST节点并返回
如果没有成功地匹配初始化部分,则回溯,匹配ε,也就是没有初始化部分。
匹配一个分号
创建一个varDecl对应的AST节点并返回

RDP

int a = 2

  • 对于一个非终结符,要从左到右依次匹配其产生式中的每个项,包括非终结符和终结符。

  • 在匹配产生式右边的非终结符时,要下降一层,继续匹配该非终结符的产生式。

  • 如果一个语法规则有多个可选的产生式,那么只要有一个产生式匹配成功就行。如果一个产生式匹配不成功,那就回退回来,尝试另一个产生式。这种回退过程,叫做回溯(Backtracking)。

三、实践练习例子

实现一个在线规则引擎

结合课上所讲的规则引擎的设计和实现的内容,实现一个在线版本的规则引擎。

使用Hertz框架开发一个HTTP服务,服务使用mysql,支持表达式的增删查改和编译执行,并实现以下接口:

  • 直接表达式执行
  • 新增表达式
  • 查询表达式
  • 删除表达式
  • 执行表达式