【实践课】规则引擎设计与实现 | 青训营笔记

109 阅读4分钟

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

重点内容

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

知识点介绍

认识规则引擎

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

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

EG

  • 输入:计算规则、商品价格、用户标签、商品属性... ...

  • 输出:积分

组成部分

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

应用场景

  • 风控对抗
  • 活动策略运行
  • 数据分析和清洗

编译原理基本概念

词法分析

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

李想通过了青训营选拔

李想 通过了 青训营选拔

有限自动机

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

语法分析

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

李想(主语) 通过了(谓语) 青训营选拔(宾语)

抽象语法树

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

  • 上下文无关语法 Context-Free Grammar

    r := a > b
    

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

    exp : add;
    add : add '+' mul | mul ;                 // 加法表达式  a + b +c  a + b * c
    mul : mul '*' pri | pri ;                 // 乘法表达式  a * b *c 
    pri : string | bool | number | identifer; // 基础表达式  weight | 20 | "abcde"
    
  • 递归下降算法 Recursive Descent Parsing

    • 自顶向下构造语法树
    • 不断地对Token进行语法展开(下降),展开过程中可能会遇到递归的情况

类型检查

  • 类型总综合
    • 根据子表达式的类型构造出父表达式的类型
  • 编译时检查 & 运行时检查
    • 类型检查可以发生在表达式的编译阶段,即在构造语法树的阶段;也可以发生在执行时的阶段

设计一个规则引擎

设计目标

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

语法与词法

  • 词法(合法Token)

    • 参数:由字母下划线组成 eg:_ab2、user_name
    • 布尔值:true、false
    • 字符串:“abcd”、’abcd‘、` abcd`
    • 十进制int:1234
    • 十进制float:123.5
    • 预定义运算符:+ -
  • 运算符

    • 一元运算符: + -
    • 二元运算符: + - * / % > < >= <= == !=
    • 逻辑操作符:&& || !
    • 括号:( )
  • 数据类型

    • 字符串
    • 布尔值
    • 十进制int
    • 十进制float
  • 语法分析

    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 | 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

规则引擎的实现

作业

type AddExpressionRequest struct {
   Exp string `json:"exp"`
}

type AddExpressionResponse struct {
   Id uint `json:"id"`
}

type RunExpressionRequest struct {
   ExpId  uint                   `json:"exp_id"`
   Params map[string]interface{} `json:"params"`
}

func HandleAddExpression(ctx context.Context, c *app.RequestContext) {
   var req AddExpressionRequest
   var resp AddExpressionResponse

   if err := c.Bind(&req); err != nil {
      BindResp(c, ParamErrCode, err.Error(), nil)
      return
   }

   // determines whether the expression already exists
   expression := dal.Expression{Exp: req.Exp}
   dal.DB.Where("exp = ?", expression.Exp).Find(&expression)
   if expression.ID != 0 {
      resp.Id = expression.ID
      BindResp(c, SuccessCode, SuccessMsg, resp)
      return
   }

   // try to compile the expression
   _, err := Compiler(req.Exp)
   if err != nil {
      BindResp(c, CompileErrCode, err.Error(), resp)
      return
   }

   // compilation passed
   dal.DB.Create(&expression)
   resp.Id = expression.ID
   BindResp(c, SuccessCode, SuccessMsg, resp)
}

func HandleDeleteExpression(ctx context.Context, c *app.RequestContext) {
   // get id
   idString := c.Param("id")
   id, err := strconv.ParseUint(idString, 10, 64)
   if err != nil {
      BindResp(c, ParamErrCode, err.Error(), nil)
      return
   }

   // determines whether the expression exists
   expression := dal.Expression{}
   dal.DB.First(&expression, id)
   if expression.ID == 0 {
      BindResp(c, RuleNotExistCode, fmt.Sprintf("exp id %d not exist", idString), nil)
      return
   }

   // delete
   dal.DB.Delete(&expression)
   BindResp(c, SuccessCode, SuccessMsg, expression)
}

func HandleGetAllExpression(ctx context.Context, c *app.RequestContext) {
   expressions := make([]dal.Expression, 0)
   dal.DB.Find(&expressions)
   BindResp(c, SuccessCode, SuccessMsg, expressions)
}

func HandleRunExpression(ctx context.Context, c *app.RequestContext) {
   var req RunExpressionRequest
   if err := c.Bind(&req); err != nil {
      BindResp(c, ParamErrCode, err.Error(), nil)
      return
   }

   // find expression by id
   var expression dal.Expression
   dal.DB.First(&expression, req.ExpId)

   if expression.ID == 0 {
      BindResp(c, RuleNotExistCode, fmt.Sprintf("exp id %d not exist", req.ExpId), nil)
      return
   }

   evaluatedExp, err := Compiler(expression.Exp)
   if err != nil {
      BindResp(c, CompileErrCode, err.Error(), nil)
      return
   }

   params, _ := getParams(req.Params)
   err = evaluatedExp.Eval(params)
   if err != nil {
      BindResp(c, RuleExecErrCode, err.Error(), nil)
      return
   }

   resp, _ := evaluatedExp.GetVal()

   BindResp(c, SuccessCode, SuccessMsg, resp)
}