基於 XAF Blazor 的規則引擎編輯器 - 實戰篇

haoxj發表於2024-03-15

示例專案:https://gitee.com/easyxaf/recharge-rules-engine-sample

前言

繼上一篇文章對規則引擎編輯器進行了初步介紹之後,本文將透過實際應用案例深入探討規則引擎編輯器的使用方法。編輯器的操作相對簡單,我們將重點放在RulesEngine的講解上。請注意,本文不是RulesEngine的入門教程,如果您對RulesEngine尚不熟悉,建議先行查閱其官方文件, https://microsoft.github.io/RulesEngine

RulesEngine

這裡要說一下在使用RulesEngine時的一些注意事項

RulesEngine中的Workflow類是規則資訊的核心載體。它不僅包含了一個規則列表(Rules),而且每個Rule內部同樣巢狀著一個規則列表。這樣的設計形成了一個多層次的樹狀結構。然而,值得注意的是,在這個結構中,只有葉節點的表示式會被實際執行。也就是說,如果一個Rule內部的Rules列表非空,那麼即使該Rule定義了表示式,它也不會被執行,它的執行結果由子Rule來決定。

對於巢狀的Rule(即子Rule),其執行方式可以透過NestedRuleExecutionMode進行配置。預設情況下,該模式設定為All,意味著所有規則都將被執行,而不考慮Rule中設定的運算子(Operator)。另一種模式是Performance,即效能模式,它會根據Rule中Operator的值來決定執行邏輯:當Operator為And或AndAlso時,如果任一子Rule返回false,則停止執行;當Operator為Or或OrElse時,如果任一子Rule返回true,則停止執行。這種模式是全域性性的,適用於所有子Rule。需要注意的是,Workflow中的Rules是頂級Rule,不是巢狀Rule,不受這個設定的限制。除非有特殊需求,否則通常建議保持預設的All設定。後文將進一步介紹這兩種模式的具體應用場景。

每個Rule都包含一個Actions屬性,Actions同時又包含OnSuccess和OnFailure這兩個子屬性。需要注意的是,Workflow中的所有Rule執行完畢後,才會根據結果執行相應的OnSuccess或OnFailure動作。當Rule的結果IsSuccess為true時,將執行OnSuccess;反之,則執行OnFailure。RulesEngine內部預設提供了OutputExpressionAction和EvaluateRuleAction這兩種動作。透過OutputExpressionAction,我們可以設定輸出表示式。每個Rule都儲存有自己的輸出值,因此在規則執行完畢後,我們需要自行遍歷並檢索這些輸出值,需要注意的是,輸出結果只有一個Output屬性,如果我們想區分不同的輸出值,我們需要在Contenxt中設定型別資訊,在讀取值時再透過這個型別資訊用於區分不同的值。

示例

在深入探討之前,我想向大家推薦一個專案:http://waitmoon.com/zh/guide 。這是一個基於Java語言開發的規則引擎,該專案的設計理念和功能實現在我設計規則引擎編輯器的過程中給予了我極大的啟發。接下來的示例將借鑑它文件中的案例,以助於我們更好地理解和應用規則引擎的概念。如果您對規則引擎感興趣,或者正在尋求靈感,這個專案絕對值得一看。

示例是一個充值活動,充值返現或送積分,我先從簡單開始,一步步的豐富它。

上面是一個最簡單的規則,"充100返現5元" 與 "充50送10積分" 這兩個規則在RulesEngine是頂級規則,就是它們都會被執行,如果 "充100" 那兩個優惠會被疊加。如果不想被疊加,我們需要給它們建立一個父規則,如下圖

你會看到"充值活動"的運算子是"或"(OR),同時它底下有"一個"的字樣,它還有一個選項是"全部",這是"巢狀規則輸出方式",它主要針對OR運算子,這是擴充套件出來的功能,在上面的介紹中我們知道RulesEngine預設會執行所有規則,同時輸出值會儲存在每個規則結果中,這樣我們可以取一個也可以取全部,你可以把"巢狀規則輸出方式"看作是取輸出值的標識,需要注意的是,AND運算子是沒有這個選項的,因為只要一個子規則失敗,父級規則就是失敗的,所以也不會執行OnSuccess動作了。如上面的示例,取全部就是疊加。如下圖

但這裡有一個注意事項,前面提到的NestedRuleExecutionMode設定,如果設定為Performance,則上面的"全部"選項則不起作用,它只會執行一個,所以如果想更靈活的使用RulesEngine,建議使用預設設定,除非確認沒有上面示例中的疊加場景。

下面我們再給這個規則加個日期限制,我們可以直接修改"充值活動"為"活動日期為10.1到10.7"

現在面臨一個問題,我們是否可以為"活動日期為10.1到10.7",直接設定一個表示式呢?根據我們之前對RulesEngine的瞭解,它僅執行樹狀結構中的葉節點表示式。這意味著,對於"活動日期為10.1到10.7"這一節點,其內部的表示式不會被執行,除非它是葉節點。然而,如果我們有一個具有多層次節點的複雜規則結構,那麼為每個葉節點新增父級規則的條件將變得異常繁瑣。這不僅增加了配置的複雜性,還可能導致維護上的困難。因此,我們需要尋找一種更為高效和簡潔的方法來處理這種情況,來簡化規則的設定過程。RulesEngine的預設執行方式我們改變不了,但我們可以在編譯規則之前對規則進行一次預處理。下面是預處理程式碼

public static void PreProcess(this Rule rule, Rule parentRule = null)
{
    if (!string.IsNullOrWhiteSpace(parentRule?.Expression))
    {
        if (!string.IsNullOrWhiteSpace(rule.Expression))
        {
            rule.Expression = $"({parentRule.Expression}) && ({rule.Expression})";
        }
        else
        {
            rule.Expression = parentRule.Expression;
        }
    }

    if (rule.Rules != null)
    {
        foreach (var childRule in rule.Rules.ToList())
        {
            PreProcess(childRule, rule);
        }
    }
}

透過上面的擴充套件方法,我們可以將父級的表示式與其合併,這樣葉節點就可以擁有其父級表示式了。

那如果我們再給"充50送10積分"新增一個時間限制,如"活動日期為10.5到10.7",就非常簡單了,新增"活動日期為10.5到10.7"節點併為其設定表示式就可以了,如下圖

我們又有新的需求了,如果老客戶在充值100元后,他會得到5積分,如下圖

大家想想上面的規則可以嗎?RulesEngine總是執行葉節點,這個一定要謹記。如果新客戶充100元,"老客戶送5積分"不會被執行,那"充100返5元"也不會被執行,最終是選擇下面的節點。

這裡我們有兩個處理方案

1、在不改變"充100返5元"節點的情況下,直接在其下面建立一個子規則,子規則的表示式直接返回true,這樣"老客戶送5積分"返回false,也不影響"充100返5元"的執行,如下圖

2、我們可以再最佳化一下,將"返現5元"放到子規則中,需要注意,當前運算子為"或",同時"巢狀規則輸出方式"為"全部",如下圖

關於規則建立的基本概念,我們的討論就先進行到這裡。請記住,無論規則邏輯多麼複雜,它們都可以透過這些基本元素逐步組合起來。透過巧妙地拼接簡單的規則節點,我們可以創造出功能強大、邏輯清晰的規則邏輯。

接下來,讓我們探討一下輸出。在前述示例中,涉及到了兩種輸出型別:"現金"和"積分",我們可以在Workflow節點下配置相應的輸出型別,配置完後,我們可以在輸出表示式動作(OutputExpressionAction)中選擇輸出型別。如下圖

輸出表示式動作中的表示式,是 DynamicLinq的表示式語法 https://dynamic-linq.net/expression-language ,下面我們基於該表示式建立一個新的規則需求,如上面的示例"充100返5元",我們把它改為每充100返5元,也就是充值200直接返10元。如下圖

透過上面的表示式就可以實現"每充100返5元"

當我們設定完輸出後,我們如何在執行完規則後,獲取到輸出值呢,下面是結合輸出型別獲取輸出值的程式碼,它會返回一個字典,Key是輸出型別,Value是輸出值列表(每一個成功的規則結果值),後續大家可以根據自己的業務邏輯組織這一些值,上述示例,我們是對"現金"返回最大值,對"積分"是求和。

public static Dictionary<string, List<object>> GetOutputResults(this RuleResultTree resultTree)
{
    var outputResults = new Dictionary<string, List<object>>();

    if (resultTree.IsSuccess)
    {
        if (resultTree.ActionResult?.Output != null)
        {
            var context = resultTree.Rule.Actions.OnSuccess.Context;
            var outputType = context.GetValueOrDefault("type", "default") as string;
            if (!outputResults.ContainsKey(outputType))
            {
                outputResults[outputType] = [];
            }
            outputResults[outputType].Add(resultTree.ActionResult.Output);
        }
    }

    if (resultTree.ChildResults != null)
    {
        var outputMode = resultTree.Rule.Properties?.GetValueOrDefault("nestedRuleOutputMode") as string;
        foreach (var childResult in resultTree.ChildResults)
        {
            var childOutputResults = GetOutputResults(childResult);

            foreach (var childOutputResult in childOutputResults)
            {
                if (!outputResults.ContainsKey(childOutputResult.Key))
                {
                    outputResults[childOutputResult.Key] = [];
                }
                outputResults[childOutputResult.Key].AddRange(childOutputResult.Value);
            }

            if (childOutputResults.Any() && outputMode == "one")
            {
                break;
            }
        }
    }

    return outputResults;
}

下面是對輸出值的處理

var outputResults = ruleResults.First().GetOutputResults();

Console.Write("共返");

if (outputResults.TryGetValue("現金", out List<object> moneyList))
{
    var money = moneyList.Select(m => double.Parse(m.ToString())).Max();
    Console.Write($"  {money}元現金");
}

if (outputResults.TryGetValue("積分", out List<object> scoreList))
{
    var score = scoreList.Select(m => double.Parse(m.ToString())).Sum();
    Console.Write($"  {score}積分");
}

寫在最後

RulesEngine是一款輕量的規則引擎類庫,它不僅提供了一套核心的基礎功能,而且其設計具有卓越的擴充套件性。這使得開發者得以在此基礎上構建更為強大和定製化的功能,滿足各種複雜的業務邏輯需求。然而,手動編輯RulesEngine的規則檔案無疑是一項耗時且繁瑣的任務。正是為了減輕這一工作負擔,開發規則編輯器的想法應運而生。編輯器的引入旨在簡化規則的建立和管理過程,使得規則的維護變得更加高效和直觀,從而將開發者從重複且繁雜的手工編輯工作中解放出來。

https://www.cnblogs.com/haoxj/p/18073710

相關文章