.Net Core 環(huán)境下構(gòu)建強(qiáng)大且易用的規(guī)則引擎

本文源碼: https://github.com/jonechenug/ZHS.Nrules.Sample

1. 引言

1.1 為什么需要規(guī)則引擎

在業(yè)務(wù)的早期時(shí)代尤溜,也許使用硬編碼或者邏輯判斷就可以滿足要求。但隨著業(yè)務(wù)的發(fā)展碉碉,越來(lái)越多的問(wèn)題會(huì)暴露出來(lái):

  • 邏輯復(fù)雜度帶來(lái)的編碼挑戰(zhàn)会通,需求變更時(shí)改變邏輯可能會(huì)引起災(zāi)難
  • 重復(fù)性的需求必須可重用,否則必須重復(fù)性編碼
  • 運(yùn)行期間無(wú)法即時(shí)修改規(guī)則搜骡,但重新部署可能會(huì)帶來(lái)其他問(wèn)題
  • 上線前的測(cè)試變得繁瑣且不可控稿壁,必須花大量的人力和時(shí)間去測(cè)試

這些困境在『 小明歷險(xiǎn)記:規(guī)則引擎 drools 教程一 』 一文中可以體會(huì)一番,一開(kāi)始只是簡(jiǎn)單的根據(jù)購(gòu)物金額來(lái)發(fā)放積分贸宏,運(yùn)行期間又要更改為更多的規(guī)則層次造寝,如果不及時(shí)引入對(duì)應(yīng)的規(guī)范化處理機(jī)制,開(kāi)發(fā)人員將慢慢墜入無(wú)止盡的業(yè)務(wù)深淵吭练。對(duì)此诫龙,聰明的做法是在系統(tǒng)中引入規(guī)則引擎,對(duì)業(yè)務(wù)操作員要提供盡量簡(jiǎn)單的操作頁(yè)面來(lái)配置規(guī)則鲫咽,規(guī)則引擎和配置盡量不要耦合到一塊签赃。

1.2 .Net Core 環(huán)境下的選擇 -- Nrules

目前最流行的規(guī)則引擎應(yīng)該是 Drools , 用 Java 語(yǔ)言編寫的開(kāi)放源碼規(guī)則引擎,使用 Rete 算法 對(duì)所編寫的規(guī)則求值分尸,其操作流程如下:

Drools 操作流程

對(duì)于 .Net 應(yīng)用來(lái)說(shuō)锦聊,可以通過(guò) Kie 組件提供的 Rest 接口調(diào)用規(guī)則引擎運(yùn)算。然而其過(guò)于龐大箩绍,僅僅只是需要規(guī)則引擎計(jì)算核心的部分孔庭。對(duì)此,查找了 .Net 中開(kāi)源的規(guī)則引擎材蛛,發(fā)現(xiàn)只有同樣實(shí)現(xiàn) Rete 算法的 Nrules 滿足要求(支持 .Net Core圆到,運(yùn)行時(shí)加載規(guī)則引擎)。

注:本文參考借鑒了美團(tuán)技術(shù)團(tuán)隊(duì) 從 0 到 1:構(gòu)建強(qiáng)大且易用的規(guī)則引擎 一文的設(shè)計(jì)思路卑吭,對(duì) Drools 從入門到放棄芽淡。

2. Nrules 實(shí)戰(zhàn) -- 電商促銷活動(dòng)規(guī)則引擎設(shè)計(jì)

2.1 了解 Nrules

NRules 是基于 Rete 匹配算法的.NET 生產(chǎn)規(guī)則引擎,基于.NET Standard 豆赏,支持 4.5+ 的應(yīng)用挣菲,提供 流式聲明規(guī)則富稻、運(yùn)行時(shí)構(gòu)建規(guī)則專門的規(guī)則語(yǔ)言(開(kāi)發(fā)中白胀,不推薦使用到生產(chǎn)椭赋,基于.Net 4.5 而不是 .NETStandard )。
其計(jì)算機(jī)制也與其他規(guī)則引擎大同小異:

計(jì)算機(jī)制

2.2 設(shè)計(jì)規(guī)則配置

前文提到 對(duì)業(yè)務(wù)操作員要提供盡量簡(jiǎn)單的操作頁(yè)面來(lái)配置規(guī)則 或杠,所以我們定義促銷活動(dòng)的規(guī)則配置就要盡量簡(jiǎn)單纹份。

業(yè)務(wù)操作員眼中的規(guī)則

在設(shè)計(jì)模型時(shí),我們必須先參考現(xiàn)實(shí)生活中遇到的電商促銷活動(dòng)廷痘,大致可以想到有這么幾種 活動(dòng)類型 :滿減促銷、單品促銷件已、套裝促銷笋额、贈(zèng)品促銷、滿贈(zèng)促銷篷扩、多買優(yōu)惠促銷兄猩、定金促銷等。
在這里鉴未,我選擇對(duì)多買優(yōu)惠促銷做分析枢冤,多買促銷優(yōu)惠即所謂的階梯打折,如買一件9折铜秆,買兩件8折淹真,其模型大致如下:

    public class LadderDiscountPromotion
    {
        public List<LadderDiscountRuleItem> Rules { get; set; }
        public string Name { get; set; }
        public DateTime StarTime { get; set; }
        public DateTime EndTime { get; set; }
        public PromotionState State { get; set; }
        public List<string> ProductIdRanges { get; set; }
        public bool IsSingle { get; set; }
        public string Id { get; set; }
    }

    public class LadderDiscountRuleItem
    {
        /// <summary>
        /// 數(shù)量
        /// </summary>
        public Int32 Quantity { get; set; }

        /// <summary>
        /// 打折的百分比
        /// </summary>
        public Decimal DiscountOff { get; set; }
    }

這里為了簡(jiǎn)化設(shè)計(jì),設(shè)計(jì)的模型并不會(huì)去約束平臺(tái)连茧、活動(dòng)范圍核蘸、會(huì)員等級(jí)等,僅僅約束了使用的產(chǎn)品 id 范圍啸驯。為了匹配現(xiàn)實(shí)中可能出現(xiàn)的組合優(yōu)惠(類似滿減活動(dòng)后還可以使用優(yōu)惠券等)現(xiàn)象和相反的獨(dú)斥現(xiàn)象(如該商品參與xx活動(dòng)后不支持X券)客扎,設(shè)置了一個(gè)字段來(lái)判斷是否可以組合優(yōu)惠,也可以理解為所有活動(dòng)都為組合優(yōu)惠罚斗,只是有些組合優(yōu)惠只有一個(gè)促銷活動(dòng)徙鱼。

注:想了解更多關(guān)于電商促銷系統(tǒng)設(shè)計(jì)可參考 腦圖

2.3 規(guī)則配置轉(zhuǎn)換

為了實(shí)現(xiàn) 規(guī)則引擎和配置盡量不要耦合到一塊信卡,必須有中間層對(duì)規(guī)則配置進(jìn)行轉(zhuǎn)換為 Nrules 能夠接受的規(guī)則描述圾叼。聯(lián)系前文的計(jì)算機(jī)制,我們可以得到這樣一個(gè)描述模型:

    public class RuleDefinition
    {
        /// <summary>
        /// 規(guī)則的名稱
        /// </summary>
        public String Name { get; set; }
        /// <summary>
        /// 約束條件
        /// </summary>
        public List<LambdaExpression> Conditions { get; set; }
        /// <summary>
        ///  執(zhí)行行動(dòng)
        /// </summary>
        public  List<LambdaExpression> Actions { get; set; }
    }

由于 Nrules 支持流式聲明彼妻,所以約束條件和產(chǎn)生的結(jié)果都可以用 LambdaExpression 表達(dá)式實(shí)現(xiàn)〈昊希現(xiàn)在我們需要把階梯打折的配置轉(zhuǎn)換成規(guī)則描述杆故,那我們需要先分析一下。假設(shè)滿一件9折溉愁,滿兩件8折处铛,滿三件7折饲趋,那我們可以將其分解為:

  • 大于等于三件打 7 折
  • 大于等于兩件且小于三件打 8 折
  • 大于等于一件且小于兩件 9 折

基于此分析,我們可以看出撤蟆,只有第一個(gè)最多的數(shù)量規(guī)則是不一樣的奕塑,其他規(guī)則都是比前一個(gè)規(guī)則的數(shù)量小且大于等于當(dāng)前規(guī)則的數(shù)量,那么我們可以這樣轉(zhuǎn)換我們的規(guī)則配置:

List<RuleDefinition> BuildLadderDiscountDefinition(LadderDiscountPromotion promotion)
        {
            var ruleDefinitions = new List<RuleDefinition>();
            //按影響的數(shù)量倒敘
            var ruleLimits = promotion.Rules.OrderByDescending(r => r.Quantity).ToList();
            var currentIndex = 0;
            var previousLimit = ruleLimits.FirstOrDefault();
            foreach (var current in ruleLimits)
            {
                //約束表達(dá)式
                var conditions = new List<LambdaExpression>();
                var actions = new List<LambdaExpression>();
                if (currentIndex == 0)
                {
                    Expression<Func<Order, bool>> conditionPart =
                        o => o.GetRangesTotalCount(promotion.ProductIdRanges) >= current.Quantity;
                    conditions.Add(conditionPart);
                }
                else
                {
                    var limit = previousLimit;
                    Expression<Func<Order, bool>> conditionPart = o =>
                        o.GetRangesTotalCount(promotion.ProductIdRanges) >= current.Quantity
                        && o.GetRangesTotalCount(promotion.ProductIdRanges) < limit.Quantity;
                    conditions.Add(conditionPart);
                }
                currentIndex = currentIndex + 1;

                //觸發(fā)的行為表達(dá)式
                Expression<Action<Order>> actionPart =
                    o => o.DiscountOrderItems(promotion.ProductIdRanges, current.DiscountOff, promotion.Name, promotion.Id);
                actions.Add(actionPart);

                // 增加描述
                ruleDefinitions.Add(new RuleDefinition
                {
                    Actions = actions,
                    Conditions = conditions,
                    Name = promotion.Name
                });
                previousLimit = current;
            }
            return ruleDefinitions;
        }

2.4 生成規(guī)則集合

在 Nrules 的 wiki 中家肯,為了實(shí)現(xiàn)運(yùn)行時(shí)加載規(guī)則引擎龄砰,我們需要引入實(shí)現(xiàn) IRuleRepository ,所以我們需要將描述模型轉(zhuǎn)換成 Nrules 中的 RuleSet

    public class ExecuterRepository : IRuleRepository, IExecuterRepository
    {
        private readonly IRuleSet _ruleSet;
        public ExecuterRepository()
        {
            _ruleSet = new RuleSet("default");
        }

        public IEnumerable<IRuleSet> GetRuleSets()
        {
            //合并
            var sets = new List<IRuleSet>();
            sets.Add(_ruleSet);
            return sets;
        }

        public void AddRule(RuleDefinition definition)
        {
            var builder = new RuleBuilder();
            builder.Name(definition.Name);
            foreach (var condition in definition.Conditions)
            {
                ParsePattern(builder, condition);
            }
            foreach (var action in definition.Actions)
            {
                var param = action.Parameters.FirstOrDefault();
                var obj = GetObject(param.Type);
                builder.RightHandSide().Action(ParseAction(obj, action, param.Name));
            }
            _ruleSet.Add(new[] { builder.Build() });
        }

        PatternBuilder ParsePattern(RuleBuilder builder, LambdaExpression condition)
        {
            var parameter = condition.Parameters.FirstOrDefault();
            var type = parameter.Type;
            var customerPattern = builder.LeftHandSide().Pattern(type, parameter.Name);
            customerPattern.Condition(condition);
            return customerPattern;
        }

        LambdaExpression ParseAction<TEntity>(TEntity entity, LambdaExpression action, String param) where TEntity : class, new()
        {
            return NRulesHelper.AddContext(action as Expression<Action<TEntity>>);
        }

    }

2.5 執(zhí)行規(guī)則引擎

做了轉(zhuǎn)換處理僅僅是第一步讨衣,我們還必須創(chuàng)建一個(gè)規(guī)則引擎的處理會(huì)話换棚,并把相關(guān)的事實(shí)對(duì)象(fact)傳遞到會(huì)話,執(zhí)行觸發(fā)的代碼反镇,相關(guān)對(duì)象發(fā)生了變化固蚤,其簡(jiǎn)單代碼如下:

var repository = new ExecuterRepository();
//加載規(guī)則
repository.AddRule(new RuleDefinition());
repository.LoadRules();
// 生成規(guī)則
ISessionFactory factory = repository.Compile();
// 創(chuàng)建會(huì)話
ISession session = factory.CreateSession();
// 加載事實(shí)對(duì)象
session.Insert(new Order());
// 執(zhí)行
session.Fire();

2.6 應(yīng)用場(chǎng)景示例

我們假設(shè)有這么一個(gè)應(yīng)用入口:傳入一個(gè)購(gòu)物車(這里等價(jià)于訂單)id,獲取其可以參加的促銷活動(dòng)歹茶,返回對(duì)應(yīng)活動(dòng)優(yōu)惠后的結(jié)果夕玩,并按總價(jià)的最低依次升序,那么可以這么寫:

       public IEnumerable<AllPromotionForOrderOutput> AllPromotionForOrder([FromQuery]String id)
        {
            var result = new List<AllPromotionForOrderOutput>();
            var order = _orderService.Get(id) ?? throw new ArgumentNullException("_orderService.Get(id)");
            var promotionGroup = _promotionService.GetActiveGroup();
            var orderjson = JsonConvert.SerializeObject(order);
            foreach (var promotions in promotionGroup)
            {
                var tempOrder = JsonConvert.DeserializeObject<Order>(orderjson);
                var ruleEngineService = HttpContext.RequestServices.GetService(typeof(RuleEngineService)) as RuleEngineService;
                ruleEngineService.AddAssembly(typeof(OrderRemarkRule).Assembly);
                ruleEngineService.ExecutePromotion(promotions, new List<object>
                {
                    tempOrder
                });
                result.Add(new AllPromotionForOrderOutput(tempOrder));
            }
            return result.OrderBy(i => i.Order.GetTotalPrice());
        }

假設(shè)這么一個(gè)購(gòu)物車id惊豺,買一件時(shí)最優(yōu)惠是參加 A 活動(dòng)燎孟,買兩件時(shí)最優(yōu)惠是參加 B 和 C 活動(dòng),那么其效果圖可能如下:

不同的條件對(duì)規(guī)則的影響

3. 結(jié)語(yǔ)

本文只是對(duì)規(guī)則引擎及 Nrules 的簡(jiǎn)單介紹及應(yīng)用尸昧,過(guò)程中隱藏了很多細(xì)節(jié)揩页。在體會(huì)到規(guī)則引擎的強(qiáng)大的同時(shí),還必須指出其局限性彻磁,規(guī)則引擎同樣不是銀彈碍沐,必須結(jié)合實(shí)際出發(fā)。

擴(kuò)展閱讀:Martin Fowler:應(yīng)該使用規(guī)則引擎嗎?


本文采用 知識(shí)共享署名-非商業(yè)性使用-相同方式共享 3.0 中國(guó)大陸許可協(xié)議
轉(zhuǎn)載請(qǐng)注明來(lái)源:張蘅水

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末衷蜓,一起剝皮案震驚了整個(gè)濱河市累提,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌磁浇,老刑警劉巖斋陪,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異置吓,居然都是意外死亡无虚,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門衍锚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)友题,“玉大人,你說(shuō)我怎么就攤上這事戴质《然拢” “怎么了踢匣?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)戈抄。 經(jīng)常有香客問(wèn)我离唬,道長(zhǎng),這世上最難降的妖魔是什么划鸽? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任输莺,我火速辦了婚禮,結(jié)果婚禮上裸诽,老公的妹妹穿的比我還像新娘嫂用。我一直安慰自己,他們只是感情好丈冬,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布尸折。 她就那樣靜靜地躺著,像睡著了一般殷蛇。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上橄浓,一...
    開(kāi)封第一講書(shū)人閱讀 51,146評(píng)論 1 297
  • 那天粒梦,我揣著相機(jī)與錄音,去河邊找鬼荸实。 笑死匀们,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的准给。 我是一名探鬼主播泄朴,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼露氮!你這毒婦竟也來(lái)了祖灰?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤畔规,失蹤者是張志新(化名)和其女友劉穎局扶,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體叁扫,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡三妈,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了莫绣。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片畴蒲。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖对室,靈堂內(nèi)的尸體忽然破棺而出模燥,到底是詐尸還是另有隱情咖祭,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布涧窒,位于F島的核電站心肪,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏纠吴。R本人自食惡果不足惜硬鞍,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望戴已。 院中可真熱鬧固该,春花似錦、人聲如沸糖儡。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)握联。三九已至桦沉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間金闽,已是汗流浹背纯露。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留代芜,地道東北人埠褪。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像挤庇,于是被迫代替她去往敵國(guó)和親钞速。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容