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

一个基于事件的规则引擎

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (39投票s)

2006年2月26日

5分钟阅读

viewsIcon

103867

downloadIcon

1654

一个事件驱动规则引擎的设计。

Sample Image - irule.jpg

引言

我曾多次遇到需要评估一系列规则并根据结果执行特定操作的情况。我从未对为这些规则编写的代码感到自豪;要么是巨大的 switch 语句,要么是盘根错节的 if-else 命令,这些命令很快变得复杂且难以维护。由于迷宫中的路径数量变得不可知,测试变得不可能。

我需要的是一个简单的规则引擎,它允许我编写简单或复杂的规则,根据需要重新连接它们,并能够独立测试单个规则,而不必测试整个规则集。我不想要一些复杂的框架,强迫我思考它的工作原理。我喜欢简单的事物。

我想象的理想规则引擎应提供以下功能

  1. 有一个 Rule 需要被调用。
  2. Rule 要么成功(Pass),要么失败(Fail)。
  3. 如果 Rule 不是链中的最后一个 Rule,它将调用链中的另一个 Rule。
  4. 如果 Rule 是链中的最后一个 Rule,它将不调用任何其他 Rule。

实现这一目标的一种方法是使用具有 Invoke 方法的对象,该方法遵循事件签名。这允许规则调用其他规则,而无需将规则相互耦合。Rule 将有两个事件,RulePassedRuleFailed,用于调用下一个 Rule。通过将 Rule 的事件连接到其他 Rule 的 Invoke() 方法,创建了一个规则链,当最顶层的 Rule 被调用时,将导致整个规则场景被评估。

Rule 事件

Rule 使用了四个事件

public event RuleDelegate RulePassed;
public event RuleDelegate RuleFailed;
public event RuleDelegate BeginRuleEvaluation;
public event RuleDelegate EndRuleEvaluation;

BeginRuleEvaluationEndRuleEvaluation 用于允许开发人员在 Rule 执行之前和之后执行任何需要的操作。

RulePassedRuleFailed 事件由 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 的 RulePassedRuleFailed 事件。

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

员工加班示例

假设您想根据以下规则计算员工的加班时间

  1. 如果员工工作 40 小时或更少

      1.1 OTM = 0

  2. 如果员工工作超过 40 小时但少于 60 小时

      2.1. 如果员工按小时计薪,OTH = 1.5 * (总小时数 - 40)

      2.2. 如果员工按固定工资计薪,

        2.2.1 如果员工工作 45 小时或更少,OTH = 0

        2.2.2 如果员工工作超过 45 小时,OTH = 1.5 * (总小时数 - 45)

  3. 如果员工工作超过 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:初始发布。
© . All rights reserved.