这是我参与「第五届青训营 」伴学笔记创作活动的第 6 天
一、本堂课重点内容:
- 理解规则引擎的组成部分与应用场景
- 理解规则引擎的核心原理
- 设计并实现一个规则引擎
二、详细知识点介绍:
-
规则引擎
- 基本概念
- 通过输入不同的规则以及条件 输出对应的业务决策 并且规则简单 可持续扩展
- 将业务决策从应用程序中抽离出来 实现一个解藕
- 解决开发人员重复编码的问题 提高服务的可维护性 缩短开发路径
- 组成部分
- 数据输入 使用预定义的语义编写的规则作为策略集
- 规则理解 按照预定义的词法、语法、优先级等理解业务规则所表达的语义
- 规则执行 根据执行输入的参数对策略集中的规则进行正确的解释和执行 同时进行一些类型检查 保证结果正确
- 应用场景
- 风控优化
- 活动策略运营
- 数据分析和清洗
- 基本概念
-
编译原理基本概念
- 词法分析
- 将源代码字符串转化为词法单元的过程
- 有限自动机 就是一个状态机,它的状态数量是有限的,在任何一个状态时,基于输入的字符,都能做出一个状态转换
- 语法分析
- 在词法分析的基础上 识别表达式的语法结构的过程
- 抽象语法树
- 表达式的语法结构可以用树表示 每个节点都是一个语法单元 单元构成的规则就叫语法
- 上下文无关语法
- 递归下降算法 自顶向下递归构造语法树 巴科斯范式
- 类型检查
- 类型综合 根据子表达的类型构造出父表达式的类型
- 编译时检查 & 运行时检查 如构造语法数的阶段、在做数据运算的时候
- 编译时需要提前声明参数类型 执行时可以根据参数输入的值来进行类型检查
- 词法分析
三、实践练习例子:
设计一个规则引擎
- 设计目标 支持特定的语法、运算符、数据类型、优先级等等。并且支持基于以上预定义的语法的规则表达式的编译与执行
状态机
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
}
语法树的执行使用后序遍历的方式来执行
类型检查使用执行时检查