.NET Core 环境下构建强大且易用的规则引擎

2018 年 6 月 13 日 DotNet

(点击上方蓝字,可快速关注我们)


来源:弓长彳艮水

cnblogs.com/chenug/p/9160397.html


源码: https://github.com/jonechenug/ZHS.Nrules.Sample


1. 引言


1.1 为什么需要规则引擎


在业务的早期时代,也许使用硬编码或者逻辑判断就可以满足要求。但随着业务的发展,越来越多的问题会暴露出来:


  • 逻辑复杂度带来的编码挑战,需求变更时改变逻辑可能会引起灾难


  • 重复性的需求必须可重用,否则必须重复性编码


  • 运行期间无法即时修改规则,但重新部署可能会带来其他问题


  • 上线前的测试变得繁琐且不可控,必须花大量的人力和时间去测试


这些困境在《小明历险记:规则引擎 drools 教程一》(https://zhuanlan.zhihu.com/p/28528925) 

一文中可以体会一番,一开始只是简单的根据购物金额来发放积分,运行期间又要更改为更多的规则层次,如果不及时引入对应的规范化处理机制,开发人员将慢慢坠入无止尽的业务深渊。对此,聪明的做法是在系统中引入规则引擎,对业务操作员要提供尽量简单的操作页面来配置规则,规则引擎和配置尽量不要耦合到一块。


1.2 .Net Core 环境下的选择 -- Nrules


目前最流行的规则引擎应该是Drools, 用 Java 语言编写的开放源码规则引擎,使用 Rete 算法对所编写的规则求值,其操作流程如下:



对于 .Net 应用来说,可以通过 Kie 组件提供的 Rest 接口调用规则引擎运算。然而其过于庞大,仅仅只是需要规则引擎计算核心的部分。对此,查找了 .Net 中开源的规则引擎,发现只有同样实现 Rete 算法的 Nrules 满足要求(支持 .Net Core,运行时加载规则引擎)。


注:本文参考借鉴了美团技术团队 从 0 到 1:构建强大且易用的规则引擎 一文的设计思路,对 Drools 从入门到放弃。


2. Nrules 实战 -- 电商促销活动规则引擎设计


2.1 了解 Nrules


NRules 是基于 Rete 匹配算法的.NET 生产规则引擎,基于.NET Standard ,支持 4.5+ 的应用,提供 流式声明规则、运行时构建规则、专门的规则语言(开发中,不推荐使用到生产,基于.Net 4.5 而不是 .NETStandard )。


其计算机制也与其他规则引擎大同小异:



2.2 设计规则配置


前文提到 对业务操作员要提供尽量简单的操作页面来配置规则 ,所以我们定义促销活动的规则配置就要尽量简单。



在设计模型时,我们必须先参考现实生活中遇到的电商促销活动,大致可以想到有这么几种活动类型:满减促销、单品促销、套装促销、赠品促销、满赠促销、多买优惠促销、定金促销等。


在这里,我选择对多买优惠促销做分析,多买促销优惠即所谓的阶梯打折,如买一件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>

    /// 数量

    /// </summary>

    public Int32 Quantity { get; set; }


    /// <summary>

    /// 打折的百分比

    /// </summary>

    public Decimal DiscountOff { get; set; }

}


这里为了简化设计,设计的模型并不会去约束平台、活动范围、会员等级等,仅仅约束了使用的产品 id 范围。为了匹配现实中可能出现的组合优惠(类似满减活动后还可以使用优惠券等)现象和相反的独斥现象(如该商品参与xx活动后不支持X券),设置了一个字段来判断是否可以组合优惠,也可以理解为所有活动都为组合优惠,只是有些组合优惠只有一个促销活动。


注:想了解更多关于电商促销系统设计可参考脑图


2.3 规则配置转换


为了实现 规则引擎和配置尽量不要耦合到一块,必须有中间层对规则配置进行转换为 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折,那我们可以将其分解为:


  • 大于等于三件打 7 折


  • 大于等于两件且小于三件打 8 折


  • 大于等于一件且小于两件 9 折


基于此分析,我们可以看出,只有第一个最多的数量规则是不一样的,其他规则都是比前一个规则的数量小且大于等于当前规则的数量,那么我们可以这样转换我们的规则配置:


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;

}


2.4 生成规则集合


在 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>>);

    }

}


2.5 执行规则引擎


做了转换处理仅仅是第一步,我们还必须创建一个规则引擎的处理会话,并把相关的事实对象(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();


2.6 应用场景示例


我们假设有这么一个应用入口:传入一个购物车(这里等价于订单)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 活动,那么其效果图可能如下:



3. 结语


本文只是对规则引擎及 Nrules 的简单介绍及应用,过程中隐藏了很多细节。


在体会到规则引擎的强大的同时,还必须指出其局限性,规则引擎同样不是银弹,必须结合实际出发。


看完本文有收获?请转发分享给更多人

关注「DotNet」,提升.Net技能 

登录查看更多
4

相关内容

FPGA加速系统开发工具设计:综述与实践
专知会员服务
65+阅读 · 2020年6月24日
商业数据分析,39页ppt
专知会员服务
160+阅读 · 2020年6月2日
Python导论,476页pdf,现代Python计算
专知会员服务
259+阅读 · 2020年5月17日
TensorFlow Lite指南实战《TensorFlow Lite A primer》,附48页PPT
专知会员服务
69+阅读 · 2020年1月17日
使用 C# 和 Blazor 进行全栈开发
DotNet
6+阅读 · 2019年4月15日
如何分分钟构建强大又好用的深度学习环境?
机器之心
3+阅读 · 2019年3月17日
Python 杠上 Java、C/C++,赢面有几成?
CSDN
6+阅读 · 2018年4月12日
Arxiv
101+阅读 · 2020年3月4日
Accelerated Methods for Deep Reinforcement Learning
Arxiv
6+阅读 · 2019年1月10日
Deep Reinforcement Learning: An Overview
Arxiv
17+阅读 · 2018年11月26日
Relational recurrent neural networks
Arxiv
8+阅读 · 2018年6月28日
Relational Deep Reinforcement Learning
Arxiv
10+阅读 · 2018年6月28日
Arxiv
3+阅读 · 2018年3月2日
VIP会员
相关VIP内容
相关论文
Top
微信扫码咨询专知VIP会员