在業務的早期時代,也許使用硬編碼或者邏輯判斷就能夠知足要求。但隨着業務的發展,愈來愈多的問題會暴露出來:git
這些困境在『 小明歷險記:規則引擎 drools 教程一』 一文中能夠體會一番,一開始只是簡單的根據購物金額來發放積分,運行期間又要更改成更多的規則層次,若是不及時引入對應的規範化處理機制,開發人員將慢慢墜入無止盡的業務深淵。對此,聰明的作法是在系統中引入規則引擎,對業務操做員要提供儘可能簡單的操做頁面來配置規則,規則引擎和配置儘可能不要耦合到一塊。github
目前最流行的規則引擎應該是Drools, 用 Java 語言編寫的開放源碼規則引擎,使用 Rete 算法對所編寫的規則求值,其操做流程以下:算法
對於 .Net 應用來講,能夠經過 Kie 組件提供的 Rest 接口調用規則引擎運算。然而其過於龐大,僅僅只是須要規則引擎計算核心的部分。對此,查找了 .Net 中開源的規則引擎,發現只有一樣實現 Rete 算法的 Nrules 知足要求(支持 .Net Core,運行時加載規則引擎)。json
注:本文參考借鑑了美團技術團隊 從 0 到 1:構建強大且易用的規則引擎 一文的設計思路,對 Drools 從入門到放棄。session
NRules 是基於 Rete 匹配算法的.NET 生產規則引擎,基於.NET Standard ,支持 4.5+ 的應用,提供 流式聲明規則、運行時構建規則、專門的規則語言(開發中,不推薦使用到生產,基於.Net 4.5 而不是 .NETStandard )。
其計算機制也與其餘規則引擎大同小異:
ide
前文提到 對業務操做員要提供儘可能簡單的操做頁面來配置規則 ,因此咱們定義促銷活動的規則配置就要儘可能簡單。測試
在設計模型時,咱們必須先參考現實生活中遇到的電商促銷活動,大體能夠想到有這麼幾種活動類型:滿減促銷、單品促銷、套裝促銷、贈品促銷、滿贈促銷、多買優惠促銷、定金促銷等。
在這裏,我選擇對多買優惠促銷作分析,多買促銷優惠即所謂的階梯打折,如買一件9折,買兩件8折,其模型大體以下:ui
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> /// 數量 /// </summary> public Int32 Quantity { get; set; } /// <summary> /// 打折的百分比 /// </summary> public Decimal DiscountOff { get; set; } }
這裏爲了簡化設計,設計的模型並不會去約束平臺、活動範圍、會員等級等,僅僅約束了使用的產品 id 範圍。爲了匹配現實中可能出現的組合優惠(相似滿減活動後還可使用優惠券等)現象和相反的獨斥現象(如該商品參與xx活動後不支持X券),設置了一個字段來判斷是否能夠組合優惠,也能夠理解爲全部活動都爲組合優惠,只是有些組合優惠只有一個促銷活動。編碼
注:想了解更多關於電商促銷系統設計可參考腦圖
爲了實現 規則引擎和配置儘可能不要耦合到一塊,必須有中間層對規則配置進行轉換爲 Nrules 可以接受的規則描述。聯繫前文的計算機制,咱們能夠獲得這樣一個描述模型:
public class RuleDefinition { /// <summary> /// 規則的名稱 /// </summary> public String Name { get; set; } /// <summary> /// 約束條件 /// </summary> public List<LambdaExpression> Conditions { get; set; } /// <summary> /// 執行行動 /// </summary> public List<LambdaExpression> Actions { get; set; } }
因爲 Nrules 支持流式聲明,因此約束條件和產生的結果均可以用 LambdaExpression 表達式實現。如今咱們須要把階梯打折的配置轉換成規則描述,那咱們須要先分析一下。假設滿一件9折,滿兩件8折,滿三件7折,那咱們能夠將其分解爲:
基於此分析,咱們能夠看出,只有第一個最多的數量規則是不同的,其餘規則都是比前一個規則的數量小且大於等於當前規則的數量,那麼咱們能夠這樣轉換咱們的規則配置:
List<RuleDefinition> BuildLadderDiscountDefinition(LadderDiscountPromotion promotion) { var ruleDefinitions = new List<RuleDefinition>(); //按影響的數量倒敘 var ruleLimits = promotion.Rules.OrderByDescending(r => r.Quantity).ToList(); var currentIndex = 0; var previousLimit = ruleLimits.FirstOrDefault(); foreach (var current in ruleLimits) { //約束表達式 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; //觸發的行爲表達式 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; }
在 Nrules 的 wiki 中,爲了實現運行時加載規則引擎,咱們須要引入實現 IRuleRepository ,因此咱們須要將描述模型轉換成 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>>); } }
作了轉換處理僅僅是第一步,咱們還必須建立一個規則引擎的處理會話,並把相關的事實對象(fact)傳遞到會話,執行觸發的代碼,相關對象發生了變化,其簡單代碼以下:
var repository = new ExecuterRepository(); //加載規則 repository.AddRule(new RuleDefinition()); repository.LoadRules(); // 生成規則 ISessionFactory factory = repository.Compile(); // 建立會話 ISession session = factory.CreateSession(); // 加載事實對象 session.Insert(new Order()); // 執行 session.Fire();
咱們假設有這麼一個應用入口:傳入一個購物車(這裏等價於訂單)id,獲取其能夠參加的促銷活動,返回對應活動優惠後的結果,並按總價的最低依次升序,那麼能夠這麼寫:
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()); }
假設這麼一個購物車id,買一件時最優惠是參加 A 活動,買兩件時最優惠是參加 B 和 C 活動,那麼其效果圖可能以下:
本文只是對規則引擎及 Nrules 的簡單介紹及應用,過程當中隱藏了不少細節。在體會到規則引擎的強大的同時,還必須指出其侷限性,規則引擎一樣不是銀彈,必須結合實際出發。