规则引擎Golang指南 – Mohit Khare

22-09-15 banq

如果您一直在开发产品,那么经常出现的场景就是不断变化的业务需求。开发人员根据一组条件构建解决方案。随着时间的推移,这些逻辑条件可能会由于不断变化的业务需求或其他外部市场因素而发生变化。

在本文中,您将了解规则引擎以及如何利用该系统以可扩展和可维护的方式解决复杂的业务问题。


什么是规则引擎?
您可以将规则引擎视为一种业务逻辑和条件的方式,有助于随着时间的推移发展您的业务。用非常通俗的话来说,这些可能是一堆与业务属性密切相关的 if-else 条件,这些条件会随着时间的推移而变化和增长。因此,这些是检查条件并根据结果执行操作的规则集。

每个规则都遵循一个基本结构

When
   <Condition is true>
Then
   <Take desired Action>


让我们举一个例子来更好地理解这一点。假设您正在处理一个问题,您希望为您的企业提供的食品订购服务向用户提供相关优惠。(例如 Zomato、Swiggy、Uber Eats)

条件:当用户满足以下所有条件时:
  • 用户至少下了 10 个订单
  • 平均订单价值大于卢比。150
  • 用户年龄在20-30之间

行动:为用户提供 20% 的折扣
这个逻辑可以很容易地修改,也可以进一步增强到属于用户的其他属性。

规则引擎在解决面向业务的逻辑时很有用,这些逻辑会导致使用大量业务属性做出某种决策。您可能会争辩说,我们不能将这个逻辑嵌入到我们的代码本身中。是的,我们可以这样做,但规则引擎提供了修改条件和添加更多逻辑的灵活性。由于这些条件来自产品/业务,因此它们具有更多可访问性,并且不必每次都与开发人员联系。
您还可以灵活地定义规则。它可以在 JSON、文本文件或 Web 界面中,任何人都可以轻松地执行 CRUD 操作。另一个添加是支持不同用户集的多个版本的规则。

在下一节中,让我们了解规则引擎的实际工作原理。

规则引擎工作原理
正如您必须理解的那样,规则引擎通常像多个if-else 条件一样工作。因此,系统通过定义的规则集运行输入(也称为事实),根据条件的结果决定是否运行相应的操作。为了更正式地定义它,一次执行有 3 个阶段。

规则引擎的 3 个阶段:
1、匹配
这是模式匹配的阶段,系统将事实和数据与一组定义的条件(规则)进行匹配。一些常用的模式匹配算法,如Rete(在 Drools 中使用)、Treat、Leaps 等。当今现代业务规则管理解决方案 (BRMS) 中使用了各种版本的 Rete。深入 Rete 超出了本博客的范围(可能是其他时间)。

2、解决
匹配阶段可能存在冲突场景,引擎处理冲突规则的顺序。将此视为优先级,允许引擎对某些条件给予更多的权重而不是其他条件。很少有用于解决冲突的算法是基于 Recency、优先级、重构等的。

3、执行
在这个阶段,引擎执行所选规则对应的动作并返回最终结果。

规则引擎的一个重要属性是链接——其中一个规则的操作部分会改变系统的状态,从而改变其他规则的条件部分的值。

实现规则引擎
让我们尝试实现一个规则引擎来获得动手体验。我们将使用Grule库并在 Golang 中实现一个相当简单的规则引擎。Grule 有自己的领域特定语言,灵感来自流行的 Drools 库。

我们将实现上一节中定义的报价示例。让我们从设置一个 go 项目开始。

mkdir test_rule_engine
cd test_rule_engine
go mod init test_rule_engine
touch main.go

main.go在编辑器中打开并添加以下代码。

package main

import (
    "fmt"
)

func main() {
  fmt.Println("TODO: implementing rule engine")
}

现在项目已经准备好了,让我们创建一个规则引擎服务。

mkdir rule_engine
touch rule_engine/service.go
touch rule_engine/offer.go
go get -u github.com/hyperjumptech/grule-rule-engine
让我们定义我们的核心规则引擎服务。将以下代码粘贴到service.go

// rule_engine/service.go
package rule_engine

import (
    "github.com/hyperjumptech/grule-rule-engine/ast"
    "github.com/hyperjumptech/grule-rule-engine/builder"
    "github.com/hyperjumptech/grule-rule-engine/engine"
    "github.com/hyperjumptech/grule-rule-engine/pkg"
)

var knowledgeLibrary = *ast.NewKnowledgeLibrary()

// Rule input object
type RuleInput interface {
    DataKey() string
}

// Rule output object
type RuleOutput interface {
    DataKey() string
}

// configs associated with each rule
type RuleConfig interface {
    RuleName() string
    RuleInput() RuleInput
    RuleOutput() RuleOutput
}

type RuleEngineSvc struct {
}

func NewRuleEngineSvc() *RuleEngineSvc {
    // you could add your cloud provider here instead of keeping rule file in your code.
    buildRuleEngine()
    return &RuleEngineSvc{}
}

func buildRuleEngine() {
    ruleBuilder := builder.NewRuleBuilder(&knowledgeLibrary)

    // Read rule from file and build rules
    ruleFile := pkg.NewFileResource("rules.grl")
    err := ruleBuilder.BuildRuleFromResource("Rules", "0.0.1", ruleFile)
    if err != nil {
        panic(err)
    }

}

func (svc *RuleEngineSvc) Execute(ruleConf RuleConfig) error {
    // get KnowledgeBase instance to execute particular rule
    knowledgeBase := knowledgeLibrary.NewKnowledgeBaseInstance("Rules", "0.0.1")

    dataCtx := ast.NewDataContext()
    // add input data context
    err := dataCtx.Add(ruleConf.RuleInput().DataKey(), ruleConf.RuleInput())
    if err != nil {
        return err
    }

    // add output data context
    err = dataCtx.Add(ruleConf.RuleOutput().DataKey(), ruleConf.RuleOutput())
    if err != nil {
        return err
    }

    // create rule engine and execute on provided data and knowledge base
    ruleEngine := engine.NewGruleEngine()
    err = ruleEngine.Execute(dataCtx, knowledgeBase)
    if err != nil {
        return err
    }
    return nil
}

我尝试以有助于您理解流程的方式记录代码。这里我们定义了一个规则引擎服务。如上所解释的规则引擎执行在理论上分三个部分工作。
  • 定义知识库(加载规则)
  • 定义规则将评估的数据属性
  • 执行规则引擎并获取结果。

现在让我们创建我们的报价规则,它使用我们在核心规则引擎服务中定义的接口。

// rule_engine/offer.go
package rule_engine

type UserOfferContext struct {
    UserOfferInput  *UserOfferInput
    UserOfferOutput *UserOfferOutput
}

func (uoc *UserOfferContext) RuleName() string {
    return "user_offers"
}

func (uoc *UserOfferContext) RuleInput() RuleInput {
    return uoc.UserOfferInput
}

func (uoc *UserOfferContext) RuleOutput() RuleOutput {
    return uoc.UserOfferOutput
}

// User data attributes
type UserOfferInput struct {
    Name              string  `json:"name"`
    Username          string  `json:"username"`
    Email             string  `json:"email"`
    Age               int     `json:"age"`
    Gender            string  `json:"gender"`
    TotalOrders       int     `json:"total_orders"`
    AverageOrderValue float64 `json:"average_order_value"`
}

func (u *UserOfferInput) DataKey() string {
    return "InputData"
}

// Offer output object
type UserOfferOutput struct {
    IsOfferApplicable bool `json:"is_offer_applicable"`
}

func (u *UserOfferOutput) DataKey() string {
    return "OutputData"
}

func NewUserOfferContext() *UserOfferContext {
    return &UserOfferContext{
        UserOfferInput:  &UserOfferInput{},
        UserOfferOutput: &UserOfferOutput{},
    }
}


很酷,所以我们已经定义了我们的报价规则结构。但是我们缺少一步,我们还没有添加任何规则。记住rules.grl我们从中读取规则的文件。让我们补充一下。

# go to root level in project
touch rules.grl

将以下代码粘贴到rules.grl

rule CheckOffer "Check if offer can be applied for user" salience 10 {
    when
        InputData.TotalOrders >= 10 && InputData.AverageOrderValue > 150 && InputData.Age >= 20 && InputData.Age <= 30
    then
        OutputData.IsOfferApplicable = true;
        Retract("CheckOffer");
}


这里有几件事可以进一步重构。但我会把它留作练习。现在我们已经准备好使用我们的报价规则引擎,让我们看看它的实际效果。

转到main.go并使用以下代码对其进行更新。

// main.go
package main

import (
    "fmt"
    "testgo/rule_engine"

    "github.com/hyperjumptech/grule-rule-engine/logger"
)

// can be part of user serice and a separate directory
type User struct {
    Name              string  `json:"name"`
    Username          string  `json:"username"`
    Email             string  `json:"email"`
    Age               int     `json:"age"`
    Gender            string  `json:"gender"`
    TotalOrders       int     `json:"total_orders"`
    AverageOrderValue float64 `json:"average_order_value"`
}

// can be moved to offer directory
type OfferService interface {
    CheckOfferForUser(user User) bool
}

type OfferServiceClient struct {
    ruleEngineSvc *rule_engine.RuleEngineSvc
}

func NewOfferService(ruleEngineSvc *rule_engine.RuleEngineSvc) OfferService {
    return &OfferServiceClient{
        ruleEngineSvc: ruleEngineSvc,
    }
}

func (svc OfferServiceClient) CheckOfferForUser(user User) bool {
    offerCard := rule_engine.NewUserOfferContext()
    offerCard.UserOfferInput = &rule_engine.UserOfferInput{
        Name:              user.Name,
        Username:          user.Username,
        Email:             user.Email,
        Gender:            user.Gender,
        Age:               user.Age,
        TotalOrders:       user.TotalOrders,
        AverageOrderValue: user.AverageOrderValue,
    }

    err := svc.ruleEngineSvc.Execute(offerCard)
    if err != nil {
        logger.Log.Error("get user offer rule engine failed", err)
    }

    return offerCard.UserOfferOutput.IsOfferApplicable
}

func main() {
    ruleEngineSvc := rule_engine.NewRuleEngineSvc()
    offerSvc := NewOfferService(ruleEngineSvc)

    userA := User{
        Name:              "Mohit Khare",
        Username:          "mkfeuhrer",
        Email:             "me@mohitkhare.com",
        Gender:            "Male",
        Age:               25,
        TotalOrders:       50,
        AverageOrderValue: 225,
    }

    fmt.Println("offer validity for user A: ", offerSvc.CheckOfferForUser(userA))

    userB := User{
        Name:              "Pranjal Sharma",
        Username:          "pj",
        Email:             "pj@abc.com",
        Gender:            "Male",
        Age:               25,
        TotalOrders:       10,
        AverageOrderValue: 80,
    }

    fmt.Println("offer validity for user B: ", offerSvc.CheckOfferForUser(userB))
}

只需运行主文件,您应该会看到输出。

go run main.go

offer validity for user A:  true
offer validity for user B:  false


恭喜,你刚刚实现了你的第一个规则引擎。通过添加更多规则来测试您的知识。您需要添加一个offer.go与您自己的数据属性类似的新文件。不要忘记更新rules.grl文件。

规则引擎的好处

  • 由于规则是在正常的逻辑语句中定义的,因此对于非技术人员来说很容易理解。
  • 逻辑独立于代码。这允许在多个地方使用相同的逻辑。(可重用性FTW)
  • 如果业务需要更改相同的属性,则无需更改代码。频繁更改是可行的,您不需要每次都进行部署。
  • 所有业务规则的中心位置,而不是分布在多个代码库中。


何时避免使用规则引擎
规则引擎不是您的开发人员的替代品。在大多数情况下,使用 AI 模型构建服务,适当的系统设计是实现可扩展和高效解决方案的方式。规则引擎是可以帮助您解决业务问题的额外用品。
 

1