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

78 阅读3分钟

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

一、本堂课重点内容:

  • 理解规则引擎的组成部分与应用场景
  • 理解规则引擎的核心原理
  • 设计并实现一个规则引擎

二、详细知识点介绍:

  • 规则引擎

    • 基本概念
      • 通过输入不同的规则以及条件 输出对应的业务决策 并且规则简单 可持续扩展
      • 将业务决策从应用程序中抽离出来 实现一个解藕
      • 解决开发人员重复编码的问题 提高服务的可维护性 缩短开发路径
    • 组成部分
      • 数据输入 使用预定义的语义编写的规则作为策略集
      • 规则理解 按照预定义的词法、语法、优先级等理解业务规则所表达的语义
      • 规则执行 根据执行输入的参数对策略集中的规则进行正确的解释和执行 同时进行一些类型检查 保证结果正确
    • 应用场景
      • 风控优化
      • 活动策略运营
      • 数据分析和清洗
  • 编译原理基本概念

    • 词法分析
      • 将源代码字符串转化为词法单元的过程
      • 有限自动机 就是一个状态机,它的状态数量是有限的,在任何一个状态时,基于输入的字符,都能做出一个状态转换
    • 语法分析
      • 在词法分析的基础上 识别表达式的语法结构的过程
    • 抽象语法树
      • 表达式的语法结构可以用树表示 每个节点都是一个语法单元 单元构成的规则就叫语法
      • 上下文无关语法
      • 递归下降算法 自顶向下递归构造语法树 巴科斯范式
    • 类型检查
      • 类型综合 根据子表达的类型构造出父表达式的类型
      • 编译时检查 & 运行时检查 如构造语法数的阶段、在做数据运算的时候
      • 编译时需要提前声明参数类型 执行时可以根据参数输入的值来进行类型检查

三、实践练习例子:

设计一个规则引擎

  • 设计目标 支持特定的语法、运算符、数据类型、优先级等等。并且支持基于以上预定义的语法的规则表达式的编译与执行

状态机 image.png

func (scanner *Scanner) Scan() (token.Token, error) {
   var tok token.Token
   var err error

   scanner.skipWhitespace()
   tok.Position = scanner.position

   switch ch := scanner.cur(); {
   case isEof(ch):
      tok.Kind = token.Eof
   case isLetter(ch):
      // if the first character is letter, this token must be an Identifier or BoolLiteral or otherwise
      literal := scanner.scanIdentifier()
      tok.Kind = token.Lookup(literal)
      tok.Value = literal
      // boolean?
      if tok.Kind == token.BoolLiteral {
         tok.Value = parseBool(literal)
      }
   case isDecimal(ch) || isDot(ch): // 123  123.4  .678   7.7.7
      // Decimal,
      literal := scanner.scanNumber()
      if strings.Contains(literal, ".") { // float
         tok.Value, err = strconv.ParseFloat(literal, 64)
         tok.Kind = token.FloatLiteral
      } else { // int
         tok.Value, err = strconv.ParseInt(literal, 10, 64)
         tok.Kind = token.IntegerLiteral
      }

      if err != nil {
         errorMsg := fmt.Sprintf("Unable to compiler numeric value '%v'", literal)
         return tok, errors.New(errorMsg)
      }
   default:
      switch ch {
      case '+', '-', '*', '/', '%', '(', ')': // 确定的单一运算符
         tok.Kind = token.LookupOperator(string(ch))
         tok.Value = scanner.read()
      case '"', ''':
         tok.Kind = token.StringLiteral
         tok.Value, err = scanner.scanString()
      case '`':
         tok.Kind = token.StringLiteral
         tok.Value, err = scanner.scanRawString()
      case '<':
         tok.Value, tok.Kind = scanner.scanSwitch2(token.LessThan, '=', token.LessEqual)
      case '>':
         tok.Value, tok.Kind = scanner.scanSwitch2(token.GreaterThan, '=', token.GreaterEqual)
      case '!':
         tok.Value, tok.Kind = scanner.scanSwitch2(token.Not, '=', token.NotEqual)
      case '=':
         tok.Value, tok.Kind = scanner.scanSwitch2(token.Illegal, '=', token.Equal)
         if tok.Kind.IsIllegal() {
            return tok, errors.New("expected to get '==', but only found '='")
         }
      case '&':
         tok.Value, tok.Kind = scanner.scanSwitch2(token.Illegal, '&', token.And)
         if tok.Kind.IsIllegal() {
            return tok, errors.New("expected to get '&&', but only found '&'")
         }
      case '|':
         tok.Value, tok.Kind = scanner.scanSwitch2(token.Illegal, '|', token.Or)
         if tok.Kind.IsIllegal() {
            return tok, errors.New("expected to get '||', but only found '|'")
         }
      default:
         tok.Kind = token.Illegal
         tok.Value = string(ch)
         errMsg := fmt.Sprintf("the scan found an illegal character '%v'", ch)
         return tok, errors.New(errMsg)
      }
   }

   return tok, err
}

语法树的结构(二叉树)

  • 一元运算符 左子树为空 右子树为操作数
  • 二元运算符 左右子树为左右操作数
  • 括号 左子树为空 右子树为括号内表达式
func (p *precedence) plan(builder *Builder) (*executor.Node, error) {
   var err error
   var leftNode, rightNode *executor.Node

   if p.nextPrecedence != nil {
      leftNode, err = p.nextPrecedence.plan(builder)
      if err != nil {
         return nil, err
      }
   } else if p.planner != nil {
      leftNode, err = p.planner(builder, p)
      if err != nil {
         return nil, err
      }
   }

   for builder.parser.hasNext() {
      tok := builder.parser.next()

      if tok.Kind.IsEof() {
         break
      }

      symbol, exist := p.validKindsToSymbols[tok.Kind]
      if !exist {
         break
      }

      rightNode, err = p.plan(builder)
      if err != nil {
         return nil, err
      }

      node := executor.NewNode(leftNode, rightNode, symbol, nil)
      return node, nil
   }
   builder.parser.rewind()
   return leftNode, nil
}

语法树的执行使用后序遍历的方式来执行

类型检查使用执行时检查