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

https://www.cnblogs.com/chenug/p/9160397.html

本文源码: 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 从入门到放弃。

  1. 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 BuildLadderDiscountDefinition(LadderDiscountPromotion promotion)

{

var ruleDefinitions = new List();

//按影响的数量倒叙

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

var actions = new List();

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 活动,那么其效果图可能如下:

不同的条件对规则的影响

  1. 结语

    本文只是对规则引擎及 Nrules 的简单介绍及应用,过程中隐藏了很多细节。在体会到规则引擎的强大的同时,还必须指出其局限性,规则引擎同样不是银弹,必须结合实际出发。

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

规则引擎.Net Core的更多相关文章

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

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

  2. jboss规则引擎KIE Drools 6.3.0 Final 教程(2)

    使用JAVA程序调用规则-运行KIE-DROOLS上的规则 第一步:建立一个MAVEN的Java工程 POM.XML 给出pom.xml文件 <project xmlns="http: ...

  3. Drools规则引擎

    一.简介 Drools is a Business Rules Management System (BRMS) solution. It provides a core Business Rules ...

  4. 开源工作流引擎 Workflow Core 的研究和使用教程

    目录 开源工作流引擎 Workflow Core 的研究和使用教程 一,工作流对象和使用前说明 二,IStepBuilder 节点 三,工作流节点的逻辑和操作 容器操作 普通节点 事件 条件体和循环体 ...

  5. .NET RulesEngine(规则引擎)

    一次偶然的机会,让我拿出RulesEngine去完成一个业务,对于业务来说主要是完成一个可伸缩性(不确定的类型,以及不确定的条件,条件的变动可能是持续增加修改的)的业务判断.比如说完成一个成就系统,管 ...

  6. Drools 规则引擎应用

    规则引擎-drools 1 .场景 1.1需求 商城系统消费赠送积分 100元以下, 不加分 100元-500元 加100分 500元-1000元 加500分 1000元 以上 加1000分 .... ...

  7. Drools 规则引擎应用 看这一篇就够了

    1 .场景 1.1需求 商城系统消费赠送积分 100元以下, 不加分 100元-500元 加100分 500元-1000元 加500分 1000元 以上 加1000分 ...... 1.2传统做法 1 ...

  8. C# RulesEngine 规则引擎:从入门到看懵

    说明 RulesEngine 是 C# 写的一个规则引擎类库,读者可以从这些地方了解它: 仓库地址: https://github.com/microsoft/RulesEngine 使用方法: ht ...

  9. Scala化规则引擎

    1. 引言 什么是规则引擎 一个业务规则包含一组条件和在此条件下执行的操作,它们表示业务规则应用程序的一段业务逻辑.业务规则通常应该由业务分析人员和策略管理者开发和修改,但有些复杂的业务规则也可以由技 ...

随机推荐

  1. win7 jenkins 修改主目录

    1.安装tomcat 2.下载Jenkins.war包,把Jenkins.war放在D:\01Install\tomcat\webapps目录下,启动tomcat

  2. str_1.判断两个字符串每个字符出现的次数一样

    1.两个字符串每个字符出现的次数一样 $str1 = "ab'c4*"; $str2 = "cb*'a4"; $ret = isBX($str1, $str2) ...

  3. CSDN不登陆看博文

    做前端的朋友说,手动改太Low,给了段JS代码: javascript: void((function() {var divElement = document.getElementById('art ...

  4. kettle结合MySQL生成保留最近6个月月度报告_20161009

    之前计算用户ID各月的金额(各月在列字段),用的是下面代码 ,b.金额,,b.金额,,b.金额,NULL)) AS 9月金额 FROM ( SELECT city AS 城市,DATE_FORMAT( ...

  5. tyvj1659救援队——最小生成树

    题目:http://www.joyoi.cn/problem/tyvj-1659 想清楚了是非常简单的最小生成树: 1.树中每条边都会被走两边: 2.每个点会走度数遍,起点又多走一遍: 根据以上两条处 ...

  6. openstack开发环境搭建

    1       目的 让linux下的openstack代码能在windows上面实现同步开发. 2       目标 使用samba实现window与Linux的文件共享. 3       实验环境 ...

  7. 用expressjs写RESTful API

    http://blog.csdn.net/kiwi_coder/article/details/36424671     用expressjs写RESTful API http://blog.csdn ...

  8. 干货:SEO长尾关键词优化方法和技巧

    在网站SEO优化上,优化比较成功的网站,根据SEO界前辈的经验结论,网站的总流量主要来源于长尾关键词,占网站总流量的80%.长尾关键词主要分布在网站的文章页,其次就是栏目页title.标签页.专题页等 ...

  9. 如何在VS2015中使用Git命令提示符

    本文转载自 http://qkxue.net/info/176223/Visual-Studio-Git VS2015自带了Git插件,但有时候觉得Git控制台命令更方便些.VS中本身不能把Git B ...

  10. JAVA学习笔记——(三)

    今日内容介绍 1.引用类型变量的创建及使用 2.流程控制语句之选择语句 3.流程控制语句之循环语句 4.循环高级 01创建引用类型变量公式 * A: 创建引用类型变量公式 * a: 我们要学的Scan ...