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





5.00/5 (13投票s)
由其他属性更改触发的复杂级联默认属性设置(在对象图上)。
引言
该项目是完全开源的,并且已在要求极高的生产环境中得到应用。
源代码位于 这里。该软件包可在 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 日:初始版本