規則引擎Golang指南 – Mohit Khare

banq 發表於 2022-09-15
Go

如果您一直在開發產品,那麼經常出現的場景就是不斷變化的業務需求。開發人員根據一組條件構建解決方案。隨著時間的推移,這些邏輯條件可能會由於不斷變化的業務需求或其他外部市場因素而發生變化。

在本文中,您將瞭解規則引擎以及如何利用該系統以可擴充套件和可維護的方式解決複雜的業務問題。


什麼是規則引擎?
您可以將規則引擎視為一種業務邏輯和條件的方式,有助於隨著時間的推移發展您的業務。用非常通俗的話來說,這些可能是一堆與業務屬性密切相關的 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:             "[email protected]",
        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:             "[email protected]",
        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 模型構建服務,適當的系統設計是實現可擴充套件和高效解決方案的方式。規則引擎是可以幫助您解決業務問題的額外用品。