65.9K
CodeProject 正在变化。 阅读更多。
Home

适用于 .NET 应用程序的业务规则引擎

starIconstarIconstarIconstarIconstarIcon

5.00/5 (13投票s)

2019 年 11 月 21 日

CPOL

3分钟阅读

viewsIcon

18087

由其他属性更改触发的复杂级联默认属性设置(在对象图上)。

引言

该项目是完全开源的,并且已在要求极高的生产环境中得到应用。

源代码位于 这里。该软件包可在 nuget.org 上获取。

dotnet add package BusinessRules.Engine --version 1.0.7

Install-Package BusinessRules.Engine -Version 1.0.7

在最新版本中,我简化了定义规则的流畅语法。不再需要 "EndRule" 和 "Or"。一个规则定义如下:

Set(x=>x.Target)

 .With(x=> ComputeValue(x))

 .If(x => SomeConditionOn(x))

  // variable number of arguments for OnChanged
 .OnChanged(x=> x.SomePropertyOnWhichTargetDepends, x=> SomeOtherProperty); 

问题

我开发此组件的动力源于一个真实的实际问题。一个交易处理系统需要非常复杂的级联规则。它已经在生产环境中使用(投资银行)。在大量的业务线系统中也可能会出现同样的需求。给定一个对象图,当对象的属性发生变化时,它会在整个图中触发级联变化(依赖于其他属性的值的默认设置)。幼稚的方法根本行不通。在 setter 中触发业务规则,或者更糟糕的是,在 setter 中编写业务规则。看起来很简单,但从长远来看是不可维护的

  • 业务规则与数据结构紧密耦合:这不允许根据上下文应用不同的规则
  • 业务规则分散在整个对象图中,这使得长期维护成为一场噩梦(我真的不得不在一个遗留系统上这样做,这并非假设)
  • 很容易多次触发同一规则:性能问题和调试噩梦
  • 在大多数情况下,要默认的值取决于多个输入。无法保证如果以不同顺序更改输入,代码的行为方式将相同。再次是调试和维护噩梦。

解决方案

该解决方案的灵感来自 ORM 框架,这些框架需要拦截属性更改以生成 SQL 更新命令;在对象周围创建一个透明的代理,它会拦截更改并存储它们。当对象被保存时,这将生成一个 SQL UPDATE 命令。

同样,在我们的例子中,将在图中的每个对象周围创建一个透明的代理。它会拦截更改并触发所需的规则。让我们看一些代码

首先,一个涉及单个对象的抽象示例

public class Abcd : IAbcd
{
    public int A { get; set; }
    public int B { get; set; }
    public int C { get; set; }
    public int D { get; set; }
}

规则在不同的类中表达。它们都使用流畅的语法在此类的构造函数中描述。

public class AbcdRules : MappingRules<IAbcd>
{
   public AbcdRules()        
   {
       Set(x => x.B)
           .With(x => x.A)
           .If(x => x.A < 100);
           .OnChanged(x => x.A);

       Set(x => x.C)
           .With(x => x.B)
           .If(x => x.C < 100)
           .OnChanged(x => x.B);

       Set(x => x.D)
           .With(x => x.C)
           .If(x => x.D < 100)
           .OnChanged(x => x.C);           

       Set(x => x.A)
           .With(x => x.D + 1)
           .If(x => x.A < 100)
           .OnChanged(x => x.D);
   } 

为了使用规则引擎,我们实例化一个 "外观"。它将对象的一个实例作为参数,以及规则的一个实例

var instance = new Abcd();

var rules = new AbcdRules()

var abcd = new InterfaceWrapper<IAbcd>(instance, rules);

我们像对业务对象一样,在外观上设置值。

设置一个值

abcd.Target.A = 1;

检查对象的状态

Assert.AreEqual(100, instance.A);

拦截属性更新

要围绕一个对象创建一个类型化的代理,所有公共属性都需要是虚拟的 (显式声明为 virtual 或从接口继承,如前面的示例所示),并且该类不应被密封

当无法满足这些条件时,提出了一个替代方案:使用动态代理。

一个来自真实交易系统的例子。这一次,我们在一个包含两个节点 trade -> product 的对象图周围创建一个外观。在对象上设置一个属性可能会改变另一个。

var trade = new CdsTrade
{
    Product = new CreditDefaultSwap()
};

var rules = new CdsRules();

dynamic p = new DynamicWrapper<CdsTrade>(trade, rules);

p.CdsProduct.RefEntity = "AXA";

p.Counterparty = "CHASEOTC";

Assert.AreEqual("ICEURO", trade.ClearingHouse);
Assert.AreEqual("MMR", trade.CdsProduct.Restructuring);
Assert.AreEqual("SNR", trade.CdsProduct.Seniority);

来自生产系统中规则的一小段摘录(完整的代码使用了大约 300 条规则)

public class CdsRules : MappingRules<CdsTrade>
{
    public CdsRules()        
    {
        Set(t => t.CounterpartyRole)
            .With(t => t.Sales != null ? "Client" : "Dealer")
            .OnChanged(t => t.Sales);

        Set(t => t.ClearingHouse)
            .With(t => GetDefaultClearingHouse(t.Counterparty, t.CdsProduct.RefEntity))
            .OnChanged(t => t.CdsProduct.RefEntity, t => t.Counterparty);

        Set(t => t.SalesCredit)
            .With(t => Calculator(t.CdsProduct.Spread, t.CdsProduct.Nominal))
            .OnChanged(t => t.CdsProduct.Spread, t => t.CdsProduct.RefEntity);

        Set(t => t.CdsProduct.TransactionType)
            .With(t => GetTransactionType(t.CdsProduct.RefEntity))
            .OnChanged(t => t.CdsProduct.RefEntity);

        Set(t => t.CdsProduct.Currency)
            .With(t => GetDefaultCurrency(t.CdsProduct.TransactionType))
            .OnChanged(t => t.CdsProduct.TransactionType);

        Set(t => t.CdsProduct.Restructuring)
            .With(t => GetDefaultRestructuring(t.CdsProduct.TransactionType))
            .OnChanged(t => t.CdsProduct.TransactionType);

        Set(t => t.CdsProduct.Seniority)
            .With(t => GetDefaultSeniority(t.CdsProduct.TransactionType))
            .OnChanged(t => t.CdsProduct.TransactionType);
    }
    // more code here    
    ...
}

这两个外观都实现了 INotifyPropertyChange,因此它们可以直接与 WPF 或 WindowsForms 视图进行数据绑定。

在内部,所有属性更新都是通过代码注入完成的(没有反射)。正如您在性能测试中看到的那样,它非常快。

实用建议

  • 仅当属性的值发生变化时才会触发规则。对于值类型,要区分具有其默认值的属性和未填充的属性,请使用可空类型。
  • 如果您想在某些条件下强制设置目标值,请也将其放入触发器中。因此,如果在触发规则后设置了另一个值,它将再次被触发并覆盖该值。
Set(v => v.DealWay)
  .With(v => "B")
  .If(v => v.BookOcCode == 9826 && v.Folder == "HEDGE_OA_HY")
  .OnChanged(v => v.BookOcCode)
  .Or(v => v.Folder)
  .Or(v=>v.DealWay)
.EndRule();

历史

  • 2019 年 11 月 21 日:初始版本
© . All rights reserved.