一个基于事件的规则引擎






4.88/5 (39投票s)
2006年2月26日
5分钟阅读

103867

1654
一个事件驱动规则引擎的设计。
引言
我曾多次遇到需要评估一系列规则并根据结果执行特定操作的情况。我从未对为这些规则编写的代码感到自豪;要么是巨大的 switch 语句,要么是盘根错节的 if-else 命令,这些命令很快变得复杂且难以维护。由于迷宫中的路径数量变得不可知,测试变得不可能。
我需要的是一个简单的规则引擎,它允许我编写简单或复杂的规则,根据需要重新连接它们,并能够独立测试单个规则,而不必测试整个规则集。我不想要一些复杂的框架,强迫我思考它的工作原理。我喜欢简单的事物。
我想象的理想规则引擎应提供以下功能
- 有一个 Rule 需要被调用。
- Rule 要么成功(Pass),要么失败(Fail)。
- 如果 Rule 不是链中的最后一个 Rule,它将调用链中的另一个 Rule。
- 如果 Rule 是链中的最后一个 Rule,它将不调用任何其他 Rule。
实现这一目标的一种方法是使用具有 Invoke
方法的对象,该方法遵循事件签名。这允许规则调用其他规则,而无需将规则相互耦合。Rule 将有两个事件,RulePassed
和 RuleFailed
,用于调用下一个 Rule。通过将 Rule 的事件连接到其他 Rule 的 Invoke()
方法,创建了一个规则链,当最顶层的 Rule 被调用时,将导致整个规则场景被评估。
Rule 事件
Rule 使用了四个事件
public event RuleDelegate RulePassed;
public event RuleDelegate RuleFailed;
public event RuleDelegate BeginRuleEvaluation;
public event RuleDelegate EndRuleEvaluation;
BeginRuleEvaluation
和 EndRuleEvaluation
用于允许开发人员在 Rule 执行之前和之后执行任何需要的操作。
RulePassed
和 RuleFailed
事件由 Rule 用于调用其他 Rule。BaseRule
中包含一个标准的 RegisterRules()
方法。
public void RegisterRules(IRule rulePassed, IRule ruleFailed)
{
this.RulePassed += new RuleDelegate(rulePassed.Invoke);
this.RuleFailed += new RuleDelegate(ruleFailed.Invoke);
}
通过在类的 Invoke()
方法中调用 OnRulePassed()
或 OnRuleFailed()
方法来触发事件。
public override void Invoke(object sender, RuleEventArgs e)
{
OnBeginRuleEvaluation(this);
if (pass)
OnRulePassed(this);
else
OnRuleFailed(this);
OnEndRuleEvaluation(this)
}
调用 Rule
重载方法 Invoke()
用于执行 Rule。第一种形式是一个没有参数的简单方法,可用于评估独立的 Rule。这通常只在规则链的第一个 Rule 上调用。
第二种形式使用事件签名。这允许将方法注册到另一个 Rule 的 RulePassed
或 RuleFailed
事件。
public virtual void Invoke()
{
// Do Stuff
}
public virtual void Invoke(object sender, RuleEventArgs e)
{
// Do Stuff
}
BaseRule
BaseRule
实现 IRule
接口,并包含任何 Rule 将使用的绝大多数样板代码。主要提供的内容是 Name
属性、事件委托的定义以及引发事件的方法。实现的一个关键方法是 RegisterRules(IRule rulePassed, IRule ruleFailed)
。
Invoke()
方法被实现为虚函数。
BaseRule
的目标是减少实现实际规则所需的工作量,并提供一个可以通过继承进行扩展以实现更高级规则的对象。
using System;
namespace Rulez.Engine
{
public class BaseRule : IRule
{
public BaseRule(string Name)
{
_name = Name;
}
private readonly string _name;
public string Name
{
get { return _name; }
}
public virtual void Invoke()
{
throw new NotImplementedException();
}
public virtual void Invoke(object sender, RuleEventArgs e)
{
throw new NotImplementedException();
}
public event RuleDelegate RulePassed;
public void OnRulePassed(object sender)
{
Events.FireEvent(RulePassed, sender);
}
public event RuleDelegate RuleFailed;
public void OnRuleFailed(object sender)
{
Events.FireEvent(RuleFailed, sender);
}
public event RuleDelegate BeginRuleEvaluation;
public void OnBeginRuleEvaluation(object sender)
{
Events.FireEvent(BeginRuleEvaluation, sender);
}
public event RuleDelegate EndRuleEvaluation;
public void OnEndRuleEvaluation(object sender)
{
Events.FireEvent(EndRuleEvaluation, sender);
}
public void RegisterRules(IRule rulePassed, IRule ruleFailed)
{
if (rulePassed == null)
throw new ArgumentNullException("rulePassed");
if (ruleFailed == null)
throw new ArgumentNullException("ruleFailed");
this.RulePassed += new RuleDelegate(rulePassed.Invoke);
this.RuleFailed += new RuleDelegate(ruleFailed.Invoke);
}
}
}
基本上就是这样。简单、易于理解且易于实现。
随机规则示例
包含的示例之一构建了一个最大深度为 MaxDepth
的规则树。每个规则都相同,它随机通过或失败。随机规则树是使用一个静态方法构建的,该方法递归地调用自身直到树满。
public static RandomRule BuildRuleTree(int CurrentDepth,
int MaxDepth, SimpleLogger logger, string Name)
{
RandomRule rr = new RandomRule(logger, Name);
CurrentDepth += 1;
if (CurrentDepth <= MaxDepth)
rr.RegisterRules(
BuildRuleTree(CurrentDepth, MaxDepth,
logger, Name + "Passed"),
BuildRuleTree(CurrentDepth, MaxDepth,
logger, Name + "Failed"));
return rr;
}
当调用树的根 Rule 时,它只是调用事件签名 Invoke()
。
public override void Invoke()
{
Invoke(this, EventArgs.Empty as RuleEventArgs);
}
Rule 本身将通过或失败,方法是随机选择一个 0 到 999 之间的数字,并确定它是奇数还是偶数。
public override void Invoke(object sender, RuleEventArgs e)
{
bool pass = (GenerateRandomNumbers.GetRandomNumber(999))%2 == 0;
if (pass)
OnRulePassed(this);
else
OnRuleFailed(this);
}
包含的测试夹具演示了随机规则生成器的执行方式。应该注意的是,深度越深,构建树所需的时间越长。
static void Main(string[] args)
{
SimpleLogger log = new SimpleLogger("RandomRuleBuilder");
log.StartTimer("Building Rules");
RandomRule baseRule = RandomRule.BuildRuleTree(0, 10, log, "BaseRule");
log.StopTimer("Rules Built");
Console.WriteLine(log.ToString());
log.StartTimer("Begin Processing Rules");
baseRule.Invoke();
log.StopTimer("All Rules Processed");
Console.WriteLine(log.ToString());
Console.Read();
}
员工加班示例
假设您想根据以下规则计算员工的加班时间
- 如果员工工作 40 小时或更少
1.1 OTM = 0
- 如果员工工作超过 40 小时但少于 60 小时
2.1. 如果员工按小时计薪,OTH = 1.5 * (总小时数 - 40)
2.2. 如果员工按固定工资计薪,
2.2.1 如果员工工作 45 小时或更少,OTH = 0
2.2.2 如果员工工作超过 45 小时,OTH = 1.5 * (总小时数 - 45)
- 如果员工工作超过 60 小时
3.1. 如果员工按小时计薪,OTH = 1.5 * 20 + 2 * (总小时数 - 60)
3.2. 如果员工按固定工资计薪,OTH = 1.5 * 15 + 2 * (总小时数 - 60)
第一步是创建一个 Employee
对象和一个 BaseRule
对象。
public class BaseBizRule : BaseRule
{
protected readonly Employee employee;
public BaseBizRule(string Name, Employee employee) : base(Name)
{
this.employee = employee;
}
public override void Invoke()
{
Invoke(this, EventArgs.Empty as RuleEventArgs);
}
}
public enum EmployeeType
{
Hourly,
Salary
}
public class Employee
{
public Employee(string Name, EmployeeType employeeType,
double HoursWorked)
{
_hoursWorked = HoursWorked;
_name = Name;
_employeeType = employeeType;
_overTimeHours = 0;
}
private readonly EmployeeType _employeeType;
public EmployeeType EmployeeType
{
get { return _employeeType;}
}
private readonly string _name;
public string Name
{
get { return _name; }
}
private readonly double _hoursWorked;
public double HoursWorked
{
get { return _hoursWorked; }
}
private double _overTimeHours;
public double OverTimeHours
{
get { return _overTimeHours; }
set { _overTimeHours = value; }
}
}
一个示例 Rule 会是
public class Rule1 : BaseBizRule
{
public Rule1(Employee employee) : base("Rule1", employee)
{}
public override void Invoke(object sender, RuleEventArgs e)
{
OnBeginRuleEvaluation(this);
if (employee.HoursWorked <= 40)
{
employee.OverTimeHours = 0;
OnRulePassed(this);
}
else
{
OnRuleFailed(this);
}
OnEndRuleEvaluation(this);
}
}
请注意,Rule 2.1 是链中的最后一个 Rule,因此当它被调用时,它将设置员工的加班时间,但不需要调用任何其他 Rule。
public class Rule2_1 : BaseBizRule
{
public Rule2_1(Employee employee) : base("Rule2_1", employee)
{}
public override void Invoke(object sender, RuleEventArgs e)
{
OnBeginRuleEvaluation(this);
employee.OverTimeHours = 1.5*(employee.HoursWorked - 40.0);
OnEndRuleEvaluation(this);
}
}
注册 Rule 很简单,而且确实很繁琐。在这种情况下,我创建了一个 Rule,TheRuleList
,它是链的顶层 Rule,并在 Rule 类的构造函数中注册 Rule。
public TheRuleList(Employee employee) : base("TheRuleList", employee)
{
r1 = new Rule1(employee);
r2 = new Rule2(employee);
r3 = new Rule3(employee);
r2_1 = new Rule2_1(employee);
r2_2 = new Rule2_2(employee);
r2_2_1 = new Rule2_2_1(employee);
r2_2_2 = new Rule2_2_2(employee);
r3_1 = new Rule3_1(employee);
r3_2 = new Rule3_2(employee);
r_isSalary1 = new RuleIsSalary(employee);
r_isSalary2 = new RuleIsSalary(employee);
rt = new RuleTerminator(employee);
r1.RegisterRules(rt, r2);
r2.RegisterRules(r_isSalary1, r3);
r3.RegisterRules(r_isSalary2, rt);
r_isSalary1.RegisterRules(r2_2, r2_1);
r_isSalary2.RegisterRules(r3_2, r3_1);
r2_2.RegisterRules(r2_2_1, r2_2_2);
r2_1.RegisterRules(rt, rt);
r2_2_1.RegisterRules(rt, rt);
r2_2_2.RegisterRules(rt, rt);
r3_1.RegisterRules(rt, rt);
r3_2.RegisterRules(rt, rt);
}
要使用 Rule 列表,您将创建一个员工对象,然后 Rule 列表将调用 Rule。
Employee emp = new Employee("Darren", EmployeeType.Hourly, 55);
TheRuleList theRuleList = new TheRuleList(emp);
theRuleList.Invoke();
return emp.OverTimeHours;
结论
在自然界中,一些最复杂的系统拥有最简单的构建块。DNA 基于仅四种化学构建块,它们只能以四种可能的方式组合。IRule
提供了两个选项:通过或失败。使用这个简单的接口,可以开发出复杂的规则链。
这种简单性带来的一个警告是 Rule 的注册。尽管单个 Rule 可能易于构建和维护,但将它们连接在一起的过程可能会迅速变得复杂,正如上面员工示例所示。由于整个系统的目标是易于维护,因此必须解决这一点。
目前,我还没有找到一种好的方法来注册 Rule 事件。我想要探索的一个选项是使用 IOC 容器或 XML 文件。我还没有尝试过这两种方式,但将来我会探索这些选项。
历史
- 2006-02-24:初始发布。