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

简介: 本文源码: https://github.com/jonechenug/ZHS.Nrules.Sample1. 引言1.1 为什么需要规则引擎在业务的早期时代,也许使用硬编码或者逻辑判断就可以满足要求。

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

1. 引言

1.1 为什么需要规则引擎

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

  • 逻辑复杂度带来的编码挑战,需求变更时改变逻辑可能会引起灾难
  • 重复性的需求必须可重用,否则必须重复性编码
  • 运行期间无法即时修改规则,但重新部署可能会带来其他问题
  • 上线前的测试变得繁琐且不可控,必须花大量的人力和时间去测试

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

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

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

Drools 操作流程

对于 .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 的简单介绍及应用,过程中隐藏了很多细节。在体会到规则引擎的强大的同时,还必须指出其局限性,规则引擎同样不是银弹,必须结合实际出发。

扩展阅读:Martin Fowler:应该使用规则引擎吗?



本文采用 知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议
转载请注明来源: 张蘅水
我在开发者头条中还会每日分享不错的技术文章,搜索 356194 即可查看
目录
相关文章
|
2月前
|
存储 Shell Linux
快速上手基于 BaGet 的脚本自动化构建 .net 应用打包
本文介绍了如何使用脚本自动化构建 `.net` 应用的 `nuget` 包并推送到指定服务仓库。首先概述了 `BaGet`——一个开源、轻量级且高性能的 `NuGet` 服务器,支持多种存储后端及配置选项。接着详细描述了 `BaGet` 的安装、配置及使用方法,并提供了 `PowerShell` 和 `Bash` 脚本实例,用于自动化推送 `.nupkg` 文件。最后总结了 `BaGet` 的优势及其在实际部署中的便捷性。
110 10
|
1月前
|
存储 开发框架 JSON
ASP.NET Core OData 9 正式发布
【10月更文挑战第8天】Microsoft 在 2024 年 8 月 30 日宣布推出 ASP.NET Core OData 9,此版本与 .NET 8 的 OData 库保持一致,改进了数据编码以符合 OData 规范,并放弃了对旧版 .NET Framework 的支持,仅支持 .NET 8 及更高版本。新版本引入了更快的 JSON 编写器 `System.Text.UTF8JsonWriter`,优化了内存使用和序列化速度。
|
11天前
|
安全 算法 编译器
.NET 9 AOT的突破 - 支持老旧Win7与XP环境
【10月更文挑战第30天】在.NET 9 中,AOT(Ahead-of-Time)编译技术在支持老旧的 Windows 7 和 XP 系统方面取得了显著进展。主要突破包括:性能提升(启动速度加快、执行效率提高)、部署优化(无需安装.NET 运行时、减小应用程序体积)、兼容性保障(编译策略优化、依赖项管理改进)以及安全性增强(代码保护机制)。这些改进使得应用程序在老旧系统上运行更加流畅、高效和安全。
|
12天前
|
XML 安全 API
.NET 9 AOT的突破 - 支持老旧Win7与XP环境
.NET 9开始,AOT支持Win7和XP,不仅仅只支持SP1版本
.NET 9 AOT的突破 - 支持老旧Win7与XP环境
|
2月前
|
开发框架 监控 前端开发
在 ASP.NET Core Web API 中使用操作筛选器统一处理通用操作
【9月更文挑战第27天】操作筛选器是ASP.NET Core MVC和Web API中的一种过滤器,可在操作方法执行前后运行代码,适用于日志记录、性能监控和验证等场景。通过实现`IActionFilter`接口的`OnActionExecuting`和`OnActionExecuted`方法,可以统一处理日志、验证及异常。创建并注册自定义筛选器类,能提升代码的可维护性和复用性。
|
2月前
|
开发框架 .NET 中间件
ASP.NET Core Web 开发浅谈
本文介绍ASP.NET Core,一个轻量级、开源的跨平台框架,专为构建高性能Web应用设计。通过简单步骤,你将学会创建首个Web应用。文章还深入探讨了路由配置、依赖注入及安全性配置等常见问题,并提供了实用示例代码以助于理解与避免错误,帮助开发者更好地掌握ASP.NET Core的核心概念。
87 3
|
25天前
|
开发框架 JavaScript 前端开发
一个适用于 ASP.NET Core 的轻量级插件框架
一个适用于 ASP.NET Core 的轻量级插件框架
|
2月前
|
开发框架 NoSQL .NET
利用分布式锁在ASP.NET Core中实现防抖
【9月更文挑战第5天】在 ASP.NET Core 中,可通过分布式锁实现防抖功能,仅处理连续相同请求中的首个请求,其余请求返回 204 No Content,直至锁释放。具体步骤包括:安装分布式锁库如 `StackExchange.Redis`;创建分布式锁服务接口及其实现;构建防抖中间件;并在 `Startup.cs` 中注册相关服务和中间件。这一机制有效避免了短时间内重复操作的问题。
|
3月前
|
设计模式 存储 前端开发
揭秘.NET架构设计模式:如何构建坚不可摧的系统?掌握这些,让你的项目无懈可击!
【8月更文挑战第28天】在软件开发中,设计模式是解决常见问题的经典方案,助力构建可维护、可扩展的系统。本文探讨了.NET中三种关键架构设计模式:MVC、依赖注入与仓储模式,并提供了示例代码。MVC通过模型、视图和控制器分离关注点;依赖注入则通过外部管理组件依赖提升复用性和可测性;仓储模式则统一数据访问接口,分离数据逻辑与业务逻辑。掌握这些模式有助于开发者优化系统架构,提升软件质量。
52 5
|
3月前
|
机器学习/深度学习 人工智能 算法
【悬念揭秘】ML.NET:那片未被探索的机器学习宝藏,如何让普通开发者一夜变身AI高手?——从零开始,揭秘构建智能应用的神秘旅程!
【8月更文挑战第28天】ML.NET 是微软推出的一款开源机器学习框架,专为希望在本地应用中嵌入智能功能的 .NET 开发者设计。无需深厚的数据科学背景,即可实现预测分析、推荐系统和图像识别等功能。它支持多种数据源,提供丰富的预处理工具和多样化的机器学习算法,简化了数据处理和模型训练流程。
53 1