前言:概念解析
DSL
: 读取yaml
文件后对应的结构体,还需要转换为DecisionFlow
才是真正可以执行的图
DecisionFlow
: 可实际执行决策流(图),FlowNode
记录了具体的一个节点
类比到DB存储,则是DB一条记录是一个DSL元信息配置,我们将其加载到内存后用结构体(DSL)保存,而后这个结构体需要再转换为另一个结构体(DecisionFlow)方可执行。可以将Dsl理解为Strategy元信息,对应DB存储,但又不完全一致,如字段类型不一致,如Strategy的某个struct属性字段,json序列化后按string存储到DB。所以又会分别定义common.Strategy(Vo)和resource.Strategy(Po),并提供相关ConvertXXToYY函数让他们可以互相转换,如果需要传输,如HTTP或RPC,则可能还会提供AdaptXXToYY函数转换为Dto,,而DecisionFlow相当于可执行的Graph。
以下是在Go
语言中对BO(Business Object,业务对象)、VO(Value Object,值对象)、PO(Persistent Object,持久化对象)、DTO(Data Transfer Object,数据传输对象)
概念的解释:
一、BO(业务对象)
1. 概念
在Go
中,BO
是对业务逻辑进行抽象和封装的对象。它包含了与业务规则相关的属性和方法。BO
主要关注业务逻辑的实现,可能会涉及多个实体或者数据结构的交互,并且会对数据进行处理以满足业务需求。
2. 示例
type OrderBO struct {
items []Item
customerID int
}
func (o *OrderBO) CalculateTotalPrice() float64 {
total := 0.0
for _, item := range o.items {
total += item.Price * float64(item.Quantity)
}
return total
}
func (o *OrderBO) IsOrderValid() bool {
// 这里可以添加更多复杂的订单有效性验证逻辑
if len(o.items) == 0 {
return false
}
return true
}
假设我们有一个在线购物系统,有一个表示订单的业务对象。这个BO
可能包含计算订单总价、验证订单状态等业务逻辑。
二、VO(值对象)
1.概念
VO
在Go
中是一种简单的值封装对象。它主要用于在不同的组件或者层之间传递数据,并且数据是不可变的(通常通过不提供修改方法来实现)。VO
的目的是为了确保数据的完整性和一致性在传递过程中不被破坏。
2. 示例
type PriceQuantityVO struct {
Price float64
Quantity int
}
比如在上述购物系统中,有一个表示商品价格和数量的组合值对象。
三、PO(持久化对象)
1.概念
在Go
中,PO
是与数据库存储相关的对象。它的结构通常与数据库中的表结构相对应,并且包含了与数据库操作相关的标签或者方法,用于数据的持久化操作,如插入、查询、更新和删除等。
2.示例
假设我们有一个数据库表“users”
,包含“id”、“name”、“email”
等字段。对应的PO
可能如下:
type UserPO struct {
ID int `gorm:"primaryKey"`
Name string `gorm:"column:name"`
Email string `gorm:"column:email"`
}
如果使用Gorm
(一个Go
语言的ORM
库),可以使用这个PO
进行数据库操作,例如:
var user UserPO
db.First(&user, 1) // 查询id为1的用户
四、DTO(数据传输对象)
1.概念
DTO
在Go
中用于在不同的系统、模块或者服务之间传输数据。它的设计目的是为了减少网络传输的数据量,只包含需要传输的必要数据,不包含业务逻辑。
2. 示例
type UserInfoDTO struct {
Name string
Email string
}
在我们的购物系统中,如果要将用户的基本信息传输给外部服务,可能会创建一个用户信息的DTO
。
一、Dsl
如之前理论部分介绍,Dsl具有如下信息
Key,Version
用于唯一标志一个DslMetadata
其余相关元信息DecisionFlow
决策流元信息(这里只包含节点的编排,即只有当前节点的名字、类型、下一个节点的名字、类型等信息,具体的节点运行时所需信息需要通过节点名字在后续字段关联上)Features
保存了该决策流所需的特征以及特征的值和类型等信息Abtests
(AB概率分流节点)、Conditionals
(条件分流节点)、Rulesets
(规则集节点)、Trees\Matrixs
(决策树与决策矩阵)、Scorecards
(评分卡),他们都实现了INode接口,只是不同的节点类型罢了
注:字段的各种类型(如FlowNode,Feature,AbtestNode等),后续博客会详解
risk_engine/core/dsl.go
type Dsl struct {
Key string `yaml:"key"`
Version string `yaml:"version"`
Metadata map[string]interface{} `yaml:"metadata"`
DecisionFlow []FlowNode `yaml:"decision_flow,flow"`
Features []Feature `yaml:"features,flow"`
Abtests []AbtestNode `yaml:"abtests,flow"`
Conditionals []ConditionalNode `yaml:"conditionals,flow"`
Rulesets []RulesetNode `yaml:"rulesets,flow"`
Matrixs []MatrixNode `yaml:"matrixs,flow"`
Trees []TreeNode `yaml:"trees,flow"`
Scorecards []ScorecardNode `yaml:"scorecards,flow"`
}
为了简洁,后续博客可能主要会介绍下Abtests
(AB概率分流节点)、Conditionals
(条件分流节点)、Rulesets
(规则集节点)的代码,Trees\Matrixs
(决策树与决策矩阵)、Scorecards
(评分卡)则可看仓库代码,原理是类似的,都是实现INode
接口的方法,只是实现不同罢了。
Dsl
的元信息从文件或者DB加载出来后,首先需要校验是否合法(是否具有Key以及决策流信息,可根据实际情况添加其他校验)
func (dsl *Dsl) CheckValid() bool {
if dsl.Key == "" {
return false
}
if len(dsl.DecisionFlow) == 0 {
return false
}
return true
}
我们还需要提供将Dsl
转为可执行图DecisionFlow
的方法
//dsl to decisionflow
func (dsl *Dsl) ConvertToDecisionFlow() (*DecisionFlow, error) {
flow := NewDecisionFlow()
flow.Key = dsl.Key
flow.Version = dsl.Version
flow.Metadata = dsl.Metadata
//map
featureMap := make(map[string]IFeature)
for _, feature := range dsl.Features {
featureMap[feature.Name] = NewFeature(feature.Name, GetFeatureType(feature.Kind)) //IFeature
}
flow.FeatureMap = featureMap
rulesetMap := make(map[string]INode)
for _, ruleset := range dsl.Rulesets {
rulesetMap[ruleset.GetName()] = ruleset
}
abtestMap := make(map[string]INode)
for _, abtest := range dsl.Abtests {
abtestMap[abtest.GetName()] = abtest
}
conditionalMap := make(map[string]INode)
for _, conditional := range dsl.Conditionals {
conditionalMap[conditional.GetName()] = conditional
}
matrixMap := make(map[string]INode)
for _, martix := range dsl.Matrixs {
matrixMap[martix.GetName()] = martix
}
treeMap := make(map[string]INode)
for _, tree := range dsl.Trees {
treeMap[tree.GetName()] = tree
}
scorecardMap := make(map[string]INode)
for _, scorecard := range dsl.Scorecards {
scorecardMap[scorecard.GetName()] = scorecard
}
//flow
for _, flowNode := range dsl.DecisionFlow {
newNode := flowNode //need set new variable
switch GetNodeType(newNode.NodeKind) {
case TypeRuleset:
newNode.SetElem(rulesetMap[newNode.NodeName])
flow.AddNode(&newNode)
case TypeAbtest:
newNode.SetElem(abtestMap[newNode.NodeName])
flow.AddNode(&newNode)
case TypeConditional:
newNode.SetElem(conditionalMap[newNode.NodeName])
flow.AddNode(&newNode)
case TypeStart:
newNode.SetElem(NewStartNode(newNode.NodeName))
flow.SetStartNode(&newNode)
flow.AddNode(&newNode)
case TypeEnd:
newNode.SetElem(NewEndNode(newNode.NodeName))
flow.AddNode(&newNode)
case TypeMatrix:
newNode.SetElem(matrixMap[newNode.NodeName])
flow.AddNode(&newNode)
case TypeTree:
newNode.SetElem(treeMap[newNode.NodeName])
flow.AddNode(&newNode)
case TypeScorecard:
newNode.SetElem(scorecardMap[newNode.NodeName])
flow.AddNode(&newNode)
default:
log.Warnf("dsl %s - %s convert warning: unkown node type %s", dsl.Key, dsl.Version, newNode.NodeKind)
}
}
return flow, nil
}
二、INode接口
不同的节点类型都可统一编排到决策流中的重要条件,就是决策流依赖的是INode接口,具体的节点实现这个接口即可
risk_engine/core/inode.go
package core
import (
"github.com/liyouming/risk_engine/configs"
)
type NodeInfo struct {
Id int64 `yaml:"id"`
Name string `yaml:"name"`
Tag string `yaml:"tag"`
Label string `yaml:"label"`
Kind string `yaml:"kind"`
Depends []string `yaml:"depends,flow"`
}
// INode 各类型节点实现该接口
type INode interface {
GetName() string
GetType() NodeType
GetInfo() NodeInfo
BeforeParse(*PipelineContext) error
Parse(*PipelineContext) (*NodeResult, error)
AfterParse(*PipelineContext, *NodeResult) error
}
// NodeResult 节点返回内容 是否阻断 下一个节点信息(ab,条件节点)
type NodeResult struct {
Id int64
Name string
Label string
Tag string
Kind NodeType
IsBlock bool
Score float64
Value interface{}
NextNodeName string //ab,条件节点有用
NextNodeType NodeType
}
// all support node
type NodeType int
const (
TypeStart NodeType = iota
TypeEnd
TypeRuleset
TypeAbtest
TypeConditional
TypeTree
TypeMatrix
TypeScorecard
)
var nodeStrMap = map[NodeType]string{
TypeStart: configs.START,
TypeEnd: configs.END,
TypeRuleset: configs.RULESET,
TypeAbtest: configs.ABTEST,
TypeConditional: configs.CONDITIONAL,
TypeTree: configs.DECISIONTREE,
TypeMatrix: configs.DECISIONMATRIX,
TypeScorecard: configs.SCORECARD,
}
func (nodeType NodeType) String() string {
return nodeStrMap[nodeType]
}
var nodeTypeMap map[string]NodeType = map[string]NodeType{
configs.START: TypeStart,
configs.END: TypeEnd,
configs.RULESET: TypeRuleset,
configs.ABTEST: TypeAbtest,
configs.CONDITIONAL: TypeConditional,
configs.DECISIONTREE: TypeTree,
configs.DECISIONMATRIX: TypeMatrix,
configs.SCORECARD: TypeScorecard,
}
func GetNodeType(name string) NodeType {
return nodeTypeMap[name]
}
三、DecisionFlow
DecisionFlow 通过Run方法开始,执行一个决策流
- 找到开始节点,不断向后执行
- flowMap 存储了本决策流执行所需的所有节点信息
- FeatureMap则存着所有需要的特征信息
risk_engine/core/flow.go
package core
import (
"errors"
"fmt"
"github.com/liyouming/risk_engine/internal/log"
)
type DecisionFlow struct {
Key string
Version string
Metadata map[string]interface{}
Md5 string // yaml文件的md5值
flowMap map[string]*FlowNode // 对应dsl中(yaml)文件中的DecisionFlow字段
startNode *FlowNode
FeatureMap map[string]IFeature // 决策流依赖的相关特征
}
func NewDecisionFlow() *DecisionFlow {
return &DecisionFlow{flowMap: make(map[string]*FlowNode)}
}
// AddNode 在一个决策流中,节点名字和节点类型唯一确定一个节点
func (flow *DecisionFlow) AddNode(node *FlowNode) {
key := flow.getNodeKey(node.NodeName, node.NodeKind)
if _, ok := flow.flowMap[key]; !ok {
flow.flowMap[key] = node
} else {
log.Warnf("repeat add node %s", key)
}
}
// NodeType string
func (flow *DecisionFlow) GetNode(name string, nodeType interface{}) (*FlowNode, bool) {
key := flow.getNodeKey(name, nodeType)
if flowNode, ok := flow.flowMap[key]; ok {
return flowNode, ok
}
return new(FlowNode), false
}
func (flow *DecisionFlow) GetAllNodes() map[string]*FlowNode {
return flow.flowMap
}
func (flow *DecisionFlow) getNodeKey(name string, nodeType interface{}) string {
return fmt.Sprintf("%s-%s", nodeType, name)
}
func (flow *DecisionFlow) SetStartNode(startNode *FlowNode) {
flow.startNode = startNode
}
func (flow *DecisionFlow) GetStartNode() (*FlowNode, bool) {
if flow.startNode == nil {
return &FlowNode{}, false
}
return flow.startNode, true
}
func (flow *DecisionFlow) Run(ctx *PipelineContext) (err error) {
//recover
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from panic: %v", r)
log.Error(err)
}
}()
//find StartNode
flowNode, ok := flow.GetStartNode()
if !ok {
err = errors.New("no start node")
return
}
gotoNext := true
for gotoNext {
//ctx.SetCurrentNode(flowNode)
flowNode, gotoNext = flow.parseNode(flowNode, ctx)
}
return
}
// parse current node and return next node
func (flow *DecisionFlow) parseNode(curNode *FlowNode, ctx *PipelineContext) (nextNode *FlowNode, gotoNext bool) {
//parse current node
ctx.AddTrack(curNode.GetElem())
res, err := curNode.Parse(ctx)
ctx.AddNodeResult(curNode.NodeName, res)
//error break
if err != nil {
log.Error(err)
return
}
//node is block
if res.IsBlock {
gotoNext = !res.IsBlock
return
}
//goto next node
switch curNode.GetNodeType() {
case TypeEnd: //END:
gotoNext = false
return
case TypeConditional:
fallthrough
case TypeAbtest: //ABTEST
nextNode, gotoNext = flow.GetNode(res.NextNodeName, res.NextNodeType)
return
default: //start,matrix,ruleset,tree,scorecard
nextNode, gotoNext = flow.GetNode(curNode.NextNodeName, curNode.NextNodeKind)
return
}
}
type FlowNode struct {
NodeName string `yaml:"node_name"`
NodeKind string `yaml:"node_kind"`
NextNodeName string `yaml:"next_node_name"`
NextNodeKind string `yaml:"next_node_kind"`
elem INode
nextNode *FlowNode
}
func (flowNode *FlowNode) GetNodeType() NodeType {
return GetNodeType(flowNode.NodeKind)
}
func (flowNode *FlowNode) GetNextNodeType() NodeType {
return GetNodeType(flowNode.NextNodeKind)
}
func (flowNode *FlowNode) SetElem(elem INode) {
flowNode.elem = elem
}
func (flowNode *FlowNode) GetElem() INode {
return flowNode.elem
}
func (flowNode *FlowNode) Parse(ctx *PipelineContext) (*NodeResult, error) {
//before hook
err := flowNode.elem.BeforeParse(ctx)
if err != nil {
return (*NodeResult)(nil), err
}
//parse
result, err := flowNode.elem.Parse(ctx)
if err != nil {
return (*NodeResult)(nil), err
}
//after hook
err = flowNode.elem.AfterParse(ctx, result)
if err != nil {
return (*NodeResult)(nil), err
}
return result, nil
}
四、Pipeline上下文
在策略执行的过程中,我们有很多的信息需要在整个决策流中流转,因此就有了上下文的定义,记录命中的规则、节点、节点的执行结果、特征的值等。
package core
import (
"sync"
)
type PipelineContext struct {
//request params
//proccess result
hMutex sync.RWMutex
hitRules map[string]*Rule // 命中了哪些规则
tMutex sync.RWMutex
tracks []*Track // 经过了哪些节点
nMutex sync.RWMutex
nodeResults map[string]*NodeResult // 经过的节点的执行结果
fMutex sync.RWMutex
features map[string]IFeature //保存所有上下文中已赋值的特征
}
type Track struct {
Id int64
Name string
Label string
Tag string
Kind NodeType
}
func NewPipelineContext() *PipelineContext {
return &PipelineContext{features: make(map[string]IFeature),
hitRules: make(map[string]*Rule),
nodeResults: make(map[string]*NodeResult),
}
}
func (ctx *PipelineContext) AddTrack(node INode) {
ctx.tMutex.Lock()
defer ctx.tMutex.Unlock()
ctx.tracks = append(ctx.tracks, &Track{Name: node.GetName(),
Id: node.GetInfo().Id,
Label: node.GetInfo().Label,
Tag: node.GetInfo().Tag,
Kind: node.GetType(),
})
}
func (ctx *PipelineContext) GetTracks() []*Track {
ctx.tMutex.RLock()
defer ctx.tMutex.RUnlock()
return ctx.tracks
}
func (ctx *PipelineContext) SetFeatures(features map[string]IFeature) {
if len(features) == 0 {
return
}
ctx.fMutex.Lock()
defer ctx.fMutex.Unlock()
for k, v := range features {
ctx.features[k] = v //override the same key feature
}
}
func (ctx *PipelineContext) SetFeature(feature IFeature) {
ctx.fMutex.Lock()
defer ctx.fMutex.Unlock()
ctx.features[feature.GetName()] = feature //override the same key feature
}
func (ctx *PipelineContext) GetFeature(name string) (result IFeature, ok bool) {
//local
ctx.fMutex.RLock()
localFeatures := ctx.features
ctx.fMutex.RUnlock()
if result, ok = localFeatures[name]; ok {
return
}
//from remote
return
}
func (ctx *PipelineContext) GetFeatures(depends []string) (result map[string]IFeature) {
if len(depends) == 0 {
return
}
//from local
ctx.fMutex.RLock()
localFeatures := ctx.features
ctx.fMutex.RUnlock()
result = make(map[string]IFeature)
remoteList := make([]string, 0)
for _, name := range depends {
if v, ok := localFeatures[name]; ok {
result[name] = v
} else {
remoteList = append(remoteList, name)
}
}
//from remote
if len(remoteList) == 0 {
//远程调用后new的时候要知道类型
return
}
//curl
return
}
func (ctx *PipelineContext) GetAllFeatures() map[string]IFeature {
ctx.fMutex.RLock()
defer ctx.fMutex.RUnlock()
return ctx.features
}
func (ctx *PipelineContext) AddHitRule(rule *Rule) {
ctx.tMutex.Lock()
defer ctx.tMutex.Unlock()
ctx.hitRules[rule.Name] = rule
}
func (ctx *PipelineContext) GetHitRules() map[string]*Rule {
ctx.tMutex.RLock()
defer ctx.tMutex.RUnlock()
return ctx.hitRules
}
func (ctx *PipelineContext) AddNodeResult(name string, nodeResult *NodeResult) {
ctx.nMutex.Lock()
defer ctx.nMutex.Unlock()
ctx.nodeResults[name] = nodeResult
}
func (ctx *PipelineContext) GetNodeResults() map[string]*NodeResult {
ctx.nMutex.RLock()
defer ctx.nMutex.RUnlock()
return ctx.nodeResults
}
type DecisionResult struct {
HitRules map[string]*Rule
Tracks []*Track
Features map[string]IFeature
NodeResults map[string]*NodeResult
}
func (ctx *PipelineContext) GetDecisionResult() *DecisionResult {
return &DecisionResult{
HitRules: ctx.GetHitRules(),
Tracks: ctx.GetTracks(),
Features: ctx.GetAllFeatures(),
NodeResults: ctx.GetNodeResults(),
}
}