[z]规则引擎

https://www.ibm.com/developerworks/cn/java/j-drools/java

使用声明性编程方法编写程序的业务逻辑算法

使用规则引擎能够经过下降实现复杂业务逻辑的组件的复杂性,下降应用程序的维护和可扩展性成本。这篇更新的文章展现如何使用开源的 Drools 规则引擎让 Java™ 应用程序更适应变化。Drools 项目引入了一个新的本地规则表达式语言和一个 Eclipse 插件,使 Drools 比之前更容易使用。sql

Ricardo Olivieri (roliv@us.ibm.com), 软件工程师, EMC编程

2008 年 4 月 15 日 (最初于 2006 年 6 月 19 日)架构

  • expand内容

要求施加在当今软件产品上的大多数复杂性是行为和功能方面的,从而致使组件实现具备复杂的业务逻辑。实现 J2EE 或 J2SE 应用程序中业务逻辑最多见的方法是编写 Java 代码来实现需求文档的规则和逻辑。在大多数状况下,该代码的错综复杂性使得维护和更新应用程序的业务逻辑成为一项使人畏惧的任务,甚至对于经验丰富的开发人员来讲也是如此。任何更改,无论多么简单,仍然会产生重编译和重部署成本。编程语言

规则引擎试图解决(或者至少下降)应用程序业务逻辑的开发和维护中固有的问题和困难。能够将规则引擎看做实现复杂业务逻辑的框架。大多数规则引擎容许您使用声明性编程来表达对于某些给定信息或知识有效的结果。您能够专一于已知为真的事实及其结果,也就是应用程序的业务逻辑。ide

有多个规则引擎可供使用,其中包括商业和开放源码选择。商业规则引擎一般容许使用专用的相似英语的语言来表达规则。其余规则引擎容许使用脚本语言(好比 Groovy 或 Python)编写规则。这篇更新的文章为您介绍 Drools 引擎,并使用示例程序帮助您理解如何使用 Drools 做为 Java 应用程序中业务逻辑层的一部分。

更多事情在变化……

俗话说得好,“唯一不变的是变化。”软件应用程序的业务逻辑正是如此。出于如下缘由,实现应用程序业务逻辑的组件可能必须更改:

  • 在开发期间或部署后修复代码缺陷
  • 应付特殊情况,即客户一开始没有提到要将业务逻辑考虑在内
  • 处理客户已更改的业务目标
  • 符合组织对敏捷或迭代开发过程的使用

若是存在这些可能性,则迫切须要一个无需太多复杂性就能处理业务逻辑更改的应用程序,尤为是当更改复杂 if-else 逻辑的开发人员并非之前编写代码的开发人员时。

Drools 是用 Java 语言编写的开放源码规则引擎,使用 Rete 算法(参阅 参考资料)对所编写的规则求值。Drools 容许使用声明方式表达业务逻辑。可使用非 XML 的本地语言编写规则,从而便于学习和理解。而且,还能够将 Java 代码直接嵌入到规则文件中,这令 Drools 的学习更加吸引人。Drools 还具备其余优势:

  • 很是活跃的社区支持
  • 易用
  • 快速的执行速度
  • 在 Java 开发人员中流行
  • 与 Java Rule Engine API(JSR 94)兼容(参阅 参考资料
  • 免费

当前 Drools 版本

在编写本文之际,Drools 规则引擎的最新版本是 4.0.4。这是一个重要更新。虽然如今还存在一些向后兼容性问题,但这个版本的特性让 Drools 比之前更有吸引力。例如,用于表达规则的新的本地语言比旧版本使用的 XML 格式更简单,更优雅。这种新语言所需的代码更少,而且格式易于阅读。

另外一个值得注意的进步是,新版本提供了用于 Eclipse IDE(Versions 3.2 和 3.3)的一个 Drools 插件。我强烈建议您经过这个插件来使用 Drools。它能够简化使用 Drools 的项目开发,而且能够提升生产率。例如,该插件会检查规则文件是否有语法错误,并提供代码完成功能。它还使您能够调试规则文件,将调试时间从数小时减小到几分钟。您能够在规则文件中添加断点,以便在规则执行期间的特定时刻检查对象的状态。这使您能够得到关于规则引擎在特定时刻所处理的知识(knowledge)(在本文的后面您将熟悉这个术语)的信息。

要解决的问题

本文展现如何使用 Drools 做为示例 Java 应用程序中业务逻辑层的一部分。为了理解本文,您应该熟悉使用 Eclipse IDE 开发和调试 Java 代码。而且,您还应该熟悉 JUnit 测试框架,并知道如何在 Eclipse 中使用它。

下列假设为应用程序解决的虚构问题设置了场景:

  • 名为 XYZ 的公司构建两种类型的计算机机器:Type1 和 Type2。机器类型按其架构定义。
  • XYZ 计算机能够提供多种功能。当前定义了四种功能:DDNS Server、DNS Server、Gateway 和 Router。
  • 在发运每台机器以前,XYZ 在其上执行多个测试。
  • 在每台机器上执行的测试取决于每台机器的类型和功能。目前,定义了五种测试:Test一、Test二、Test三、Test4 和 Test5。
  • 当将测试分配给一台计算机时,也将测试到期日期 分配给该机器。分配给计算机的测试不能晚于该到期日期执行。到期日期值取决于分配给机器的测试。
  • XYZ 使用能够肯定机器类型和功能的内部开发的软件应用程序,自动化了执行测试时的大部分过程。而后,基于这些属性,应用程序肯定要执行的测试及其到期日期。
  • 目前,为计算机分配测试和测试到期日期的逻辑是该应用程序的已编译代码的一部分。包含该逻辑的组件用 Java 语言编写。
  • 分配测试和到期日期的逻辑一个月更改屡次。当开发人员须要使用 Java 代码实现该逻辑时,必须经历一个冗长乏味的过程。

什么时候使用规则引擎?

并不是全部应用程序都应使用规则引擎。若是业务逻辑代码包括不少 if-else 语句,则应考虑使用一个规则引擎。维护复杂的 Boolean 逻辑多是很是困难的任务,而规则引擎能够帮助您组织该逻辑。当您可使用声明方法而非命令编程语言表达逻辑时,变化引入错误的可能性会大大下降。

若是代码变化可能致使大量的财政损失,则也应考虑规则引擎。许多组织在将已编译代码部署到托管环境中时具备严格的规则。例如,若是须要修改 Java 类中的逻辑,在更改进入生产环境以前,将会经历一个冗长乏味的过程:

  1. 必须从新编译应用程序代码。
  2. 在测试中转环境中删除代码。
  3. 由数据质量审核员检查代码。
  4. 由托管环境架构师批准更改。
  5. 计划代码部署。

即便对一行代码的简单更改也可能花费组织的几千美圆。若是须要遵循这些严格规则而且发现您频繁更改业务逻辑代码,则很是有必要考虑使用规则引擎。

对客户的了解也是该决策的一个因素。尽管您使用的是一个简单的需求集合,只需 Java 代码中的简单实现,可是您可能从上一个项目得知,您的客户具备在开发周期期间甚至部署以后添加和更改业务逻辑需求的倾向(以及财政和政治资源)。若是从一开始就选择使用规则引擎,您可能会过得舒服一些。

由于在对为计算机分配测试和到期日期的逻辑进行更改时,公司会发生高额成本,因此 XYZ 主管已经要求软件工程师寻找一种灵活的方法,用最少的代价将对业务规则的更改 “推” 至生产环境。因而 Drools 走上舞台了。工程师决定,若是它们使用规则引擎来表达肯定哪些测试应该执行的规则,则能够节省更多时间和精力。他们将只须要更改规则文件的内容,而后在生产环境中替换该文件。对于他们来讲,这比更改已编译代码并在将已编译代码部署到生产环境中时进行由组织强制的冗长过程要简单省时得多(参阅侧栏 什么时候使用规则引擎?)。

目前,在为机器分配测试和到期日期时必须遵循如下业务规则:

  • 若是计算机是 Type1,则只能在其上执行 Test一、Test2 和 Test5。
  • 若是计算机是 Type2 且其中一个功能为 DNS Server,则应执行 Test4 和 Test5。
  • 若是计算机是 Type2 且其中一个功能为 DDNS Server,则应执行 Test2 和 Test3。
  • 若是计算机是 Type2 且其中一个功能为 Gateway,则应执行 Test3 和 Test4。
  • 若是计算机是 Type2 且其中一个功能为 Router,则应执行 Test1 和 Test3。
  • 若是 Test1 是要在计算机上执行的测试之一,则测试到期日期距离机器的建立日期 3 天。该规则优先于测试到期日期的全部下列规则。
  • 若是 Test2 是要在计算机上执行的测试之一,则测试到期日期距离机器的建立日期 7 天。该规则优先于测试到期日期的全部下列规则。
  • 若是 Test3 是要在计算机上执行的测试之一,则测试到期日期距离机器的建立日期 10 天。该规则优先于测试到期日期的全部下列规则。
  • 若是 Test4 是要在计算机上执行的测试之一,则测试到期日期距离机器的建立日期 12 天。该规则优先于测试到期日期的全部下列规则。
  • 若是 Test5 是要在计算机上执行的测试之一,则测试到期日期距离机器的建立日期 14 天。

捕获为机器分配测试和测试到期日期的上述业务规则的当前 Java 代码如清单 1 所示:

清单 1. 使用 if-else 语句实现业务规则逻辑
Machine machine = ...
// Assign tests
Collections.sort(machine.getFunctions());
int index;

if (machine.getType().equals("Type1")) {
   Test test1 = ...
   Test test2 = ...
   Test test5 = ...
   machine.getTests().add(test1);
   machine.getTests().add(test2);
   machine.getTests().add(test5);
} else if (machine.getType().equals("Type2")) {
   index = Collections.binarySearch(machine.getFunctions(), "Router");
   if (index >= 0) {
      Test test1 = ...
      Test test3 = ...
      machine.getTests().add(test1);
      machine.getTests().add(test3);
   }
   index = Collections.binarySearch(machine.getFunctions(), "Gateway");
   if (index >= 0) {
      Test test4 = ...
      Test test3 = ...
      machine.getTests().add(test4);
      machine.getTests().add(test3);
   }
...
}

// Assign tests due date
Collections.sort(machine.getTests(), new TestComparator());
...
Test test1 = ...
index = Collections.binarySearch(machine.getTests(), test1);
if (index >= 0) {
   // Set due date to 3 days after Machine was created
   Timestamp creationTs = machine.getCreationTs();
   machine.setTestsDueTime(...);
   return;
}

index = Collections.binarySearch(machine.getTests(), test2);
if (index >= 0) {
   // Set due date to 7 days after Machine was created
   Timestamp creationTs = machine.getCreationTs();
   machine.setTestsDueTime(...);
   return;
}
...

清单 1 中的代码不是太复杂,但也并不简单。若是要对其进行更改,须要十分当心。一堆互相缠绕的 if-else 语句正试图捕获已经为应用程序标识的业务逻辑。若是您对业务规则不甚了解,就没法一眼看出代码的意图。

 

导入示例程序

使用 Drools 规则的示例程序附带在本文的 ZIP 存档中。程序使用 Drools 规则文件以声明方法表示上一节定义的业务规则。它包含一个 Eclipse 3.2 Java 项目,该项目是使用 Drools 插件和 4.0.4 版的 Drools 规则引擎开发的。请遵循如下步骤设置示例程序:

  1. 下载 ZIP 存档(参见 下载)。
  2. 下载并安装 Drools Eclipse 插件(参见 参考资料)。
  3. 在 Eclipse 中,选择该选项以导入 Existing Projects into Workspace,如图 1 所示:
    图 1. 将示例程序导入到 Eclipse 工做区
    将示例程序导入到 Eclipse 工做区
  4. 而后选择下载的存档文件并将其导入工做区中。您将在工做区中发现一个名为 DroolsDemo 的新 Java 项目,如图 2 所示:
    图 2. 导入到工做区中的示例程序
    导入到工做区中的示例程序

若是启用了 Build automatically 选项,则代码应该已编译并可供使用。若是未启用该选项,则如今构建 DroolsDemo 项目。

 

检查代码

如今来看一下示例程序中的代码。该程序的 Java 类的核心集合位于 demo 包中。在该包中能够找到 Machine 和 Test 域对象类。Machine 类的实例表示要分配测试和测试到期日期的计算机机器。下面来看 Machine 类,如清单 2 所示:

清单 2. Machine 类的实例变量
public class Machine {

   private String type;
   private List functions = new ArrayList();
   private String serialNumber;
   private Collection tests = new HashSet();
   private Timestamp creationTs;
   private Timestamp testsDueTime;

   public Machine() {
     super();
     this.creationTs = new Timestamp(System.currentTimeMillis());
   }
   ...

在清单 2 中能够看到 Machine 类的属性有:

  • type(表示为 string 属性)—— 保存机器的类型值。
  • functions (表示为 list)—— 保存机器的功能。
  • testsDueTime (表示为 timestamp 变量)—— 保存分配的测试到期日期值。
  • tests (Collection 对象)—— 保存分配的测试集合。

注意,能够为机器分配多个测试,并且一个机器能够具备一个或多个功能。

出于简洁目的,机器的建立日期值设置为建立 Machine 类的实例时的当前时间。若是这是真实的应用程序,建立时间将设置为机器最终构建完成并准备测试的实际时间。

Test 类的实例表示能够分配给机器的测试。Test实例由其 id 和 name 唯一描述,如清单 3 所示:

清单 3. Test 类的实例变量
public class Test {

   public static Integer TEST1 = new Integer(1);
   public static Integer TEST2 = new Integer(2);
   public static Integer TEST3 = new Integer(3);
   public static Integer TEST4 = new Integer(4);
   public static Integer TEST5 = new Integer(5);

   private Integer id;
   private String name;
   private String description;
   public Test() {
      super();
   }
   ...

示例程序使用 Drools 规则引擎对 Machine 类的实例求值。基于 Machine 实例的 type 和 functions 属性的值,规则引擎肯定应分配给tests 和 testsDueTime 属性的值。

在 demo 包中,还会发现 Test 对象的数据访问对象 (TestDAOImpl) 的实现,它容许您按照 ID 查找 Test 实例。该数据访问对象极其简单;它不链接任何外部资源(好比关系数据库)以得到 Test 实例。相反,在其定义中硬编码了预约义的 Test 实例集合。在现实世界中,您可能会具备链接外部资源以检索 Test 对象的数据访问对象。

RulesEngine 类

demo 中比较重要(若是不是最重要的)的一个类是 RulesEngine 类。该类的实例用做封装逻辑以访问 Drools 类的包装器对象。能够在您本身的 Java 项目中容易地重用该类,由于它所包含的逻辑不是特定于示例程序的。清单 4 展现了该类的属性和构造函数:

清单 4. RulesEngine 类的实例变量和构造函数
public class RulesEngine {

   private RuleBase rules;
   private boolean debug = false;

   public RulesEngine(String rulesFile) throws RulesEngineException {
      super();
      try {
         // Read in the rules source file
         Reader source = new InputStreamReader(RulesEngine.class
            .getResourceAsStream("/" + rulesFile));
         // Use package builder to build up a rule package
         PackageBuilder builder = new PackageBuilder();
         // This parses and compiles in one step
         builder.addPackageFromDrl(source);
         // Get the compiled package
         Package pkg = builder.getPackage();
         // Add the package to a rulebase (deploy the rule package).
         rules = RuleBaseFactory.newRuleBase();
         rules.addPackage(pkg);
      } catch (Exception e) {
         throw new RulesEngineException(
            "Could not load/compile rules file: " + rulesFile, e);
      }
   }
   ...

在清单 4 中能够看到,RulesEngine 类的构造函数接受字符串值形式的参数,该值表示包含业务规则集合的文件的名称。该构造函数使用PackageBuilder 类的实例解析和编译源文件中包含的规则。(注意: 该代码假设规则文件位于程序类路径中名为 rules 的文件夹中。)而后,使用 PackageBuilder 实例将全部编译好的规则合并为一个二进制 Package 实例。而后,使用这个实例配置 Drools RuleBase 类的一个实例,后者被分配给 RulesEngine 类的 rules 属性。能够将这个类的实例看做规则文件中所包含规则的内存中表示。

清单 5 展现了 RulesEngine 类的 executeRules() 方法:

清单 5. RulesEngine 类的 executeRules() 方法
public void executeRules(WorkingEnvironmentCallback callback) {
   WorkingMemory workingMemory = rules.newStatefulSession();
   if (debug) {
      workingMemory
         .addEventListener(new DebugWorkingMemoryEventListener());
   }
   callback.initEnvironment(workingMemory);
   workingMemory.fireAllRules();
}

executeRules() 方法几乎包含了 Java 代码中的全部魔力。调用该方法执行先前加载到类构造函数中的规则。Drools WorkingMemory 类的实例用于断言或声明知识,规则引擎应使用它来肯定应执行的结果。(若是知足规则的全部条件,则执行该规则的结果。)将知识看成规则引擎用于肯定是否应启动规则的数据或信息。例如,规则引擎的知识能够包含一个或多个对象及其属性的当前状态。

规则结果在调用 WorkingMemory 对象的 fireAllRules() 方法时执行。您可能奇怪(我但愿您如此)知识是如何插入到 WorkingMemory实例中的。若是仔细看一下该方法的签名,将会注意到所传递的参数是 WorkingEnvironmentCallback 接口的实例。executeRules() 方法的调用者须要建立实现该接口的对象。该接口只须要开发人员实现一个方法(参见清单 6 ):

清单 6. WorkingEnvironmentCallback 接口
public interface WorkingEnvironmentCallback {
   void initEnvironment(WorkingMemory workingMemory) throws FactException;
}

因此,应该是 executeRules() 方法的调用者将知识插入到 WorkingMemory 实例中的。稍后将展现这是如何实现的。

TestsRulesEngine 类

清单 7 展现了 TestsRulesEngine 类,它也位于 demo 包中:

清单 7. TestsRulesEngine 类
public class TestsRulesEngine {

   private RulesEngine rulesEngine;
   private TestDAO testDAO;

   public TestsRulesEngine(TestDAO testDAO) throws RulesEngineException {
      super();
      rulesEngine = new RulesEngine("testRules1.drl");
      this.testDAO = testDAO;
   }

   public void assignTests(final Machine machine) {
      rulesEngine.executeRules(new WorkingEnvironmentCallback() {
         public void initEnvironment(WorkingMemory workingMemory) {
            // Set globals first before asserting/inserting any knowledge!
            workingMemory.setGlobal("testDAO", testDAO);
            workingMemory.insert(machine);
         };
      });
   }
}

TestsRulesEngine 类只有两个实例变量。rulesEngine 属性是 RulesEngine 类的实例。 testDAO 属性保存对 TestDAO 接口的具体实现的引用。 rulesEngine 对象是使用 "testRules1.drl" 字符串做为其构造函数的参数实例化的。testRules1.drl 文件以声明方式捕获 要解决的问题 中的业务规则。 TestsRulesEngine 类的 assignTests() 方法调用 RulesEngine 类的 executeRules() 方法。在这个方法中,建立了 WorkingEnvironmentCallback 接口的一个匿名实例,而后将该实例做为参数传递给 executeRules() 方法。

若是查看 assignTests() 方法的实现,能够看到知识是如何插入到 WorkingMemory 实例中的。 WorkingMemory 类的 insert() 方法被调用以声明在对规则求值时规则引擎应使用的知识。在这种状况下,知识由 Machine 类的一个实例组成。被插入的对象用于对规则的条件求值。

若是在对条件求值时,须要让规则引擎引用 用做知识的对象,则应使用 WorkingMemory 类的 setGlobal() 方法。在示例程序中,setGlobal() 方法将对 TestDAO 实例的引用传递给规则引擎。而后规则引擎使用 TestDAO 实例查找它可能须要的任何 Test 实例。

TestsRulesEngine 类是示例程序中唯一的 Java 代码,它包含专门致力于为机器分配测试和测试到期日期的实现的逻辑。该类中的逻辑永远不须要更改,即便业务规则须要更新时也是如此。

 

Drools 规则文件

如前所述,testRules.xml 文件包含规则引擎为机器分配测试和测试到期日期所遵循的规则。它使用 Drools 本地语言表达所包含的规则。

Drools 规则文件有一个或多个 rule 声明。每一个 rule 声明由一个或多个 conditional 元素以及要执行的一个或多个 consequences 或 actions 组成。一个规则文件还能够有多个(即 0 个或多个)import 声明、多个 global 声明以及多个 function 声明。

理解 Drools 规则文件组成最好的方法是查看一个真正的规则文件。下面来看 testRules1.drl 文件的第一部分,如清单 8 所示:

清单 8. testRules1.drl 文件的第一部分
package demo;

import demo.Machine;
import demo.Test;
import demo.TestDAO;
import java.util.Calendar;
import java.sql.Timestamp;
global TestDAO testDAO;

在清单 8 中,能够看到 import 声明如何让规则执行引擎知道在哪里查找将在规则中使用的对象的类定义。global 声明让规则引擎知道,某个对象应该能够从规则中访问,但该对象不该是用于对规则条件求值的知识的一部分。能够将 global 声明看做规则中的全局变量。对于global 声明,须要指定它的类型(即类名)和想要用于引用它的标识符(即变量名)。global 声明中的这个标识符名称应该与调用WorkingMemory 类的 setGlobal() 方法时使用的标识符值匹配,在此即为 testDAO (参见 清单 7)。

function 关键词用于定义一个 Java 函数(参见 清单 9)。若是看到 consequence(稍后将讨论)中重复的代码,则应该提取该代码并将其编写为一个 Java 函数。可是,这样作时要当心,避免在 Drools 规则文件中编写复杂的 Java 代码。规则文件中定义的 Java 函数应该简短易懂。这不是 Drools 的技术限制。若是想要在规则文件中编写复杂的 Java 代码,也能够。但这样作可能会让您的代码更加难以测试、调试和维护。复杂的 Java 代码应该是 Java 类的一部分。若是须要 Drools 规则执行引擎调用复杂的 Java 代码,则能够将对包含复杂代码的 Java 类的引用做为全局数据传递给规则引擎。

清单 9. testRules1.drl 文件中定义的 Java 函数
function void setTestsDueTime(Machine machine, int numberOfDays) {
   setDueTime(machine, Calendar.DATE, numberOfDays);
}

function void setDueTime(Machine machine, int field, int amount) {
   Calendar calendar = Calendar.getInstance();
   calendar.setTime(machine.getCreationTs());
   calendar.add(field, amount);
   machine.setTestsDueTime(new Timestamp(calendar.getTimeInMillis()));
}
 ...

清单 10 展现了 testRules1.drl 文件中定义的第一个规则:

清单 10. testRules1.drl 中定义的第一个规则
rule "Tests for type1 machine"
salience 100
when
   machine : Machine( type == "Type1" )
then
   Test test1 = testDAO.findByKey(Test.TEST1);
   Test test2 = testDAO.findByKey(Test.TEST2);
   Test test5 = testDAO.findByKey(Test.TEST5);
   machine.getTests().add(test1);
   machine.getTests().add(test2);
   machine.getTests().add(test5);
   insert( test1 );
   insert( test2 );
   insert( test5 );
end

如清单 10 所示,rule 声明有一个唯一标识它的 name。还能够看到,when 关键词定义规则中的条件块,then 关键词定义结果块。清单 10 中显示的规则有一个引用 Machine 对象的条件元素。若是回到 清单 7 能够看到, Machine 对象被插入到 WorkingMemory 对象中。这正是这个规则中使用的对象。条件元素对 Machine 实例(知识的一部分)求值,以肯定是否应执行规则的结果。若是条件元素等于 true,则启动或执行结果。从清单 10 中还能够看出,结果只不过是一个 Java 语言语句。经过快速浏览该规则,能够很容易地识别出这是下列业务规则的实现:

  • 若是计算机是 Type1,则只能在该机器上执行 Test一、Test2 和 Test5。

所以,该规则的条件元素检查( Machine 对象的) type 属性是否为 Type1。 (在条件元素中,只要对象听从 Java bean 模式,就能够直接访问对象的属性,而没必要调用 getter 方法。)若是该属性的值为 true,那么将 Machine 实例的一个引用分配给 machine 标识符。而后,在规则的结果块使用该引用,将测试分配给 Machine 对象。

在该规则中,唯一看上去有些奇怪的语句是最后三条结果语句。回忆 “要解决的问题” 小节中的业务规则,应该分配为测试到期日期的值取决于分配给机器的测试。所以,分配给机器的测试须要成为规则执行引擎在对规则求值时所使用的知识的一部分。这正是这三条语句的做用。这些语句使用一个名为 insert 的方法更新规则引擎中的知识。

肯定规则执行顺序

规则的另外一个重要的方面是可选的 salience 属性。使用它可让规则执行引擎知道应该启动规则的结果语句的顺序。具备最高显著值的规则的结果语句首先执行;具备第二高显著值的规则的结果语句第二执行,依此类推。当您须要让规则按预约义顺序启动时,这一点很是重要,很快您将会看到。

testRules1.drl 文件中接下来的四个规则实现与机器测试分配有关的其余业务规则(参见清单 11)。这些规则与刚讨论的第一个规则很是类似。注意,salience 属性值对于前五个规则是相同的;无论这五个规则的启动顺序如何,其执行结果将相同。若是结果受规则的启动顺序影响,则须要为规则指定不一样的显著值。

清单 11. testRules1.drl 文件中与测试分配有关的其余规则
rule "Tests for type2, DNS server machine"
salience 100
when
   machine : Machine( type == "Type2", functions contains "DNS Server")
then
   Test test5 = testDAO.findByKey(Test.TEST5);
   Test test4 = testDAO.findByKey(Test.TEST4);
   machine.getTests().add(test5);
   machine.getTests().add(test4);
   insert( test4 );
   insert( test5 );
end

rule "Tests for type2, DDNS server machine"
salience 100
when
   machine : Machine( type == "Type2", functions contains "DDNS Server")
then
   Test test2 = testDAO.findByKey(Test.TEST2);
   Test test3 = testDAO.findByKey(Test.TEST3);
   machine.getTests().add(test2);
   machine.getTests().add(test3);
   insert( test2 );
   insert( test3 );
end

rule "Tests for type2, Gateway machine"
salience 100
when
   machine : Machine( type == "Type2", functions contains "Gateway")
then
   Test test3 = testDAO.findByKey(Test.TEST3);
   Test test4 = testDAO.findByKey(Test.TEST4);
   machine.getTests().add(test3);
   machine.getTests().add(test4);
   insert( test3 );
   insert( test4 );
end

rule "Tests for type2, Router machine"
salience 100
when
   machine : Machine( type == "Type2", functions contains "Router")
then
   Test test3 = testDAO.findByKey(Test.TEST3);
   Test test1 = testDAO.findByKey(Test.TEST1);
   machine.getTests().add(test3);
   machine.getTests().add(test1);
   insert( test1 );
   insert( test3 );
end
...

清单 12 展现了 Drools 规则文件中的其余规则。您可能已经猜到,这些规则与测试到期日期的分配有关:

清单 12. testRules1.drl 文件中与测试到期日期分配有关的规则
rule "Due date for Test 5"
salience 50
when
   machine : Machine()
   Test( id == Test.TEST5 )
then
   setTestsDueTime(machine, 14);
end

rule "Due date for Test 4"
salience 40
when
   machine : Machine()
   Test( id == Test.TEST4 )
then
   setTestsDueTime(machine, 12);
end

rule "Due date for Test 3"
salience 30
when
   machine : Machine()
   Test( id == Test.TEST3 )
then
   setTestsDueTime(machine, 10);
end

rule "Due date for Test 2"
salience 20
when
   machine : Machine()
   Test( id == Test.TEST2 )
then
   setTestsDueTime(machine, 7);
end

rule "Due date for Test 1"
salience 10
when
   machine : Machine()
   Test( id == Test.TEST1 )
then
   setTestsDueTime(machine, 3);
end

这些规则的实现比用于分配测试的规则的实现要略微简单一些,但我发现它们更有趣一些,缘由有四。

第一,注意这些规则的执行顺序很重要。结果(即,分配给 Machine 实例的 testsDueTime 属性的值)受这些规则的启动顺序所影响。若是查看 要解决的问题 中详细的业务规则,您将注意到用于分配测试到期日期的规则具备优先顺序。例如,若是已经将 Test三、Test4 和 Test5 分配给机器,则测试到期日期应距离机器的建立日期 10 天。缘由在于 Test3 的到期日期规则优先于 Test4 和 Test5 的测试到期日期规则。如何在 Drools 规则文件中表达这一点呢?答案是 salience 属性。为 testsDueTime 属性设置值的规则的 salience 属性值不一样。Test1 的测试到期日期规则优先于全部其余测试到期日期规则,因此这应是要启动的最后一个规则。换句话说,若是 Test1 是分配给机器的测试之一,则由该规则分配的值应该是优先使用的值。因此,该规则的 salience 属性值最低:10。

第二,每一个规则有两个条件元素。第一个元素只检查工做内存中是否存在一个 Machine 实例。(注意,这里不会对 Machine 对象的属性进行比较。)当这个元素等于 true 时,它将一个引用分配给 Machine 对象,然后者将在规则的结果块被用到。若是不分配这个引用,那么就没法将测试到期日期分配给 Machine 对象。第二个条件元素检查 Test 对象的 id 属性。当且仅当这两个条件元素都等于 true 时,才执行规则的结果元素。

第三,在 Test 类的一个实例成为知识的一部分(即,包含在工做内存中)以前,Drools 规则执行引擎不会(也不能)对这些规则的条件块求值。这很符合逻辑,由于若是工做内存中尚未 Test 类的一个实例,那么规则执行引擎就没法执行这些规则的条件中所包含的比较。若是您想知道 Test 实例什么时候成为知识的一部分,那么能够回忆,在与分配测试相关规则的结果的执行期间,一个或多个 Test 实例被插入到工做内存中。(参见 清单 10 和 清单 11)。

第四,注意这些规则的结果块至关简短。缘由在于在全部结果块中调用了规则文件中以前使用 function 关键词定义的 setTestsDueTime()Java 方法。该方法为 testsDueTime 属性实际分配值。

 

测试代码

既然已经仔细检查了实现业务规则逻辑的代码,如今应该检查它是否能工做。要执行示例程序,运行 demo.test 中的TestsRulesEngineTest JUnit 测试。

在该测试中,建立了 5 个 Machine 对象,每一个对象具备不一样的属性集合(序号、类型和功能)。为这五个 Machine 对象的每个都调用TestsRulesEngine 类的 assignTests() 方法。一旦 assignTests() 方法完成其执行,就执行断言以验证 testRules1.drl 中指定的业务规则逻辑是否正确(参见清单 13)。能够修改 TestsRulesEngineTest JUnit 类以多添加几个具备不一样属性的 Machine 实例,而后使用断言验证结果是否跟预期同样。

清单 13. testTestsRulesEngine() 方法中用于验证业务逻辑实现是否正确的断言
public void testTestsRulesEngine() throws Exception {
   while (machineResultSet.next()) {
      Machine machine = machineResultSet.getMachine();
      testsRulesEngine.assignTests(machine);
      Timestamp creationTs = machine.getCreationTs();
      Calendar calendar = Calendar.getInstance();
      calendar.setTime(creationTs);
      Timestamp testsDueTime = machine.getTestsDueTime();

      if (machine.getSerialNumber().equals("1234A")) {
         assertEquals(3, machine.getTests().size());
         assertTrue(machine.getTests().contains(testDAO.findByKey(Test.TEST1)));
         assertTrue(machine.getTests().contains(testDAO.findByKey(Test.TEST2)));
         assertTrue(machine.getTests().contains(testDAO.findByKey(Test.TEST5)));
         calendar.add(Calendar.DATE, 3);
         assertEquals(calendar.getTime(), testsDueTime);

      } else if (machine.getSerialNumber().equals("1234B")) {
         assertEquals(4, machine.getTests().size());
         assertTrue(machine.getTests().contains(testDAO.findByKey(Test.TEST5)));
         assertTrue(machine.getTests().contains(testDAO.findByKey(Test.TEST4)));
         assertTrue(machine.getTests().contains(testDAO.findByKey(Test.TEST3)));
         assertTrue(machine.getTests().contains(testDAO.findByKey(Test.TEST2)));
         calendar.add(Calendar.DATE, 7);
         assertEquals(calendar.getTime(), testsDueTime);
...
 

关于知识的其余备注

值得一提的是,除了将对象插入至工做内存以外,还能够在工做内存中修改对象或从中撤回对象。能够在规则的结果块中进行这些操做。若是在结果语句中修改做为当前知识一部分的对象,而且所修改的属性被用在 condition 元素中以肯定是否应启动规则,则应在结果块中调用update() 方法。调用 update() 方法时,您让 Drools 规则引擎知道对象已更新且引用该对象的任何规则的任何条件元素(例如,检查一个或多个对象属性的值)应再次求值,以肯定条件的结果如今是 true 仍是 false。这意味着甚至当前活动规则(在其结果块中修改对象的规则)的条件均可以再次求值,这可能致使规则再次启动,并可能致使无限循环。若是不但愿这种状况发生,则应该包括 rule 的可选 no-loop 属性并将其赋值为 true

清单 14 用两个规则的定义的伪代码演示了这种状况。Rule 1 修改 objectA 的 property1。而后它调用 update() 方法,以容许规则执行引擎知道该更新,从而触发对引用 objectA 的规则的条件元素的从新求值。所以,启动 Rule 1 的条件应再次求值。由于该条件应再次等于trueproperty2 的值仍相同,由于它在结果块中未更改),Rule 1 应再次启动,从而致使无限循环的执行。为了不这种状况,添加no-loop 属性并将其赋值为 true,从而避免当前活动规则再次执行。

清单 14. 修改工做内存中的对象并使用规则元素的 no-loop 属性
...
rule "Rule 1"
salience 100
no-loop true
when
   objectA : ClassA (property2().equals(...))
then
   Object value = ...
   objectA.setProperty1(value);
   update( objectA );
end

rule "Rule 2"
salience 100
when
   objectB : ClassB()
   objectA : ClassA ( property1().equals(objectB) )
   ...
then
   ...
end
...

若是对象再也不是知识的一部分,则应将该对象从工做内存中撤回(参见清单 15)。经过在结果块中调用 retract() 方法实现这一点。当从工做内存中移除对象以后,引用该对象的(属于任何规则的)任何条件元素将不被求值。由于对象再也不做为知识的一部分存在,因此规则没有启动的机会。

清单 15. 从工做内存中撤回对象
...
rule "Rule 1"
salience 100
when
   objectB : ...
   objectA : ...
then
   Object value = ...
   objectA.setProperty1(value);
   retract(objectB);
end

rule "Rule 2"
salience 90
when
   objectB : ClassB ( property().equals(...) )
then
  ...
end
...

清单 15 包含两个规则的定义的伪代码。假设启动两个规则的条件等于 true。则应该首先启动 Rule 1,由于 Rule 1 的显著值比 Rule 2 的高。如今,注意在 Rule 1 的结果块中,objectB 从工做内存中撤回(也就是说,objectB 再也不是知识的一部分)。该动做更改了规则引擎的 “执行日程”,由于如今将不启动 Rule 2。缘由在于曾经为真值的用于启动 Rule 2 的条件再也不为真,由于它引用了一个再也不是知识的一部分的对象(objectB)。若是清单 15 中还有其余规则引用了 objectB,且这些规则还没有启动,则它们将不会再启动了。

做为关于如何修改工做内存中当前知识的具体例子,我将从新编写前面讨论的规则源文件。业务规则仍然与 “要解决的问题” 小节中列出的同样。可是,我将使用这些规则的不一样实现取得相同的结果。按照这种方法,任什么时候候工做内存中唯一可用的知识是 Machine 实例。换句话说,规则的条件元素将只针对 Machine 对象的属性执行比较。这与以前的方法有所不一样,以前的方法还要对 Test 对象的属性进行比较(参见 清单 12)。 这些规则的新实现被捕获在示例应用程序的 testRules2.drl 文件中。清单 16 展现了 testRules2.drl 中与分配测试相关的规则:

清单 16. testRules2.drl 中与分配测试相关的规则
rule "Tests for type1 machine"
lock-on-active true
salience 100

when
   machine : Machine( type == "Type1" )
then
   Test test1 = testDAO.findByKey(Test.TEST1);
   Test test2 = testDAO.findByKey(Test.TEST2);
   Test test5 = testDAO.findByKey(Test.TEST5);
   machine.getTests().add(test1);
   machine.getTests().add(test2);
   machine.getTests().add(test5);
   update( machine );
end

rule "Tests for type2, DNS server machine"
lock-on-active true
salience 100

when
   machine : Machine( type == "Type2", functions contains "DNS Server")
then
   Test test5 = testDAO.findByKey(Test.TEST5);
   Test test4 = testDAO.findByKey(Test.TEST4);
   machine.getTests().add(test5);
   machine.getTests().add(test4);
   update( machine );
end

rule "Tests for type2, DDNS server machine"
lock-on-active true
salience 100

when
   machine : Machine( type == "Type2", functions contains "DDNS Server")
then
   Test test2 = testDAO.findByKey(Test.TEST2);
   Test test3 = testDAO.findByKey(Test.TEST3);
   machine.getTests().add(test2);
   machine.getTests().add(test3);
   update( machine );
end

rule "Tests for type2, Gateway machine"
lock-on-active true
salience 100

when
   machine : Machine( type == "Type2", functions contains "Gateway")
then
   Test test3 = testDAO.findByKey(Test.TEST3);
   Test test4 = testDAO.findByKey(Test.TEST4);
   machine.getTests().add(test3);
   machine.getTests().add(test4);
   update( machine );
end

rule "Tests for type2, Router machine"
lock-on-active true
salience 100

when
   machine : Machine( type == "Type2", functions contains "Router")
then
   Test test3 = testDAO.findByKey(Test.TEST3);
   Test test1 = testDAO.findByKey(Test.TEST1);
   machine.getTests().add(test3);
   machine.getTests().add(test1);
   update( machine );
end
...

若是将清单 16 中第一个规则的定义与 清单 10 中的定义相比较,能够看到,新方法没有将分配给 Machine 对象的 Test 实例插入到工做内存中,而是由规则的结果块调用 update() 方法,让规则引擎知道 Machine 对象已被修改。(Test 实例被添加/指定给它。) 若是看看清单 16 中其余的规则,应该能够看到,每当将测试分配给一个 Machine 对象时,都采用这种方法:一个或多个 Test 实例被分配给一个 Machine 实例,而后,修改工做知识,并通知规则引擎。

还应注意清单 16 中使用的 active-lock 属性。该属性的值被设为 true;若是不是这样,在执行这些规则时将陷入无限循环。将它设为true 能够确保当一个规则更新工做内存中的知识时,最终不会致使对规则从新求值并从新执行规则,也就不会致使无限循环。能够将active-lock 属性 看做 no-loop 属性的增强版。 no-loop 属性确保当修改知识的规则更新后不会再被调用,而 active-lock 属性则确保在修改知识之后,文件中的任何规则(其 active-lock 属性被设为 true)不会从新执行。

清单 17 展现了其余规则有何更改:

清单 17. testRules2.drl 中与分配测试到期日期有关的规则
rule "Due date for Test 5"
salience 50
when
   machine : Machine(tests contains (testDAO.findByKey(Test.TEST5)))
then
   setTestsDueTime(machine, 14);
end

rule "Due date for Test 4"
salience 40
when
   machine : Machine(tests contains (testDAO.findByKey(Test.TEST4)))
then
   setTestsDueTime(machine, 12);
end

rule "Due date for Test 3"
salience 30
when
   machine : Machine(tests contains (testDAO.findByKey(Test.TEST3)))
then
   setTestsDueTime(machine, 10);
end

rule "Due date for Test 2"
salience 20
when
   machine : Machine(tests contains (testDAO.findByKey(Test.TEST2)))
then
   setTestsDueTime(machine, 7);
end

rule "Due date for Test 1"
salience 10
when
   machine : Machine(tests contains (testDAO.findByKey(Test.TEST1)))
then
   setTestsDueTime(machine, 3);
end

这些规则的条件元素如今检查一个 Machine 对象的 tests 集合,以肯定它是否包含特定的 Test 实例。所以,如前所述,按照这种方法,规则引擎只处理工做内存中的一个对象(一个 Machine 实例),而不是多个对象(Machine 和 Test 实例)。

要测试 testRules2.drl 文件,只需编辑示例应用程序提供的 TestsRulesEngine 类(参见 清单 7):将 "testRules1.drl" 字符串改成"testRules2.drl",而后运行 TestsRulesEngineTest JUnit 测试。全部测试都应该成功,就像将 testRules1.drl 做为规则源同样。

 

关于断点的注意事项

如前所述,用于 Eclipse 的 Drools 插件容许在规则文件中设置断点。要清楚,只有在调试做为 “Drools Application” 的程序时,才会启用这些断点。不然,调试器会忽略它们。

例如,假设您想调试做为 “Drools Application” 的 TestsRulesEngineTest JUnit 测试类。在 Eclipse 中打开常见的 Debug 对话框。在这个对话框中,应该能够看到一个 “Drools Application” 类别。在这个类别下,建立一个新的启动配置。在这个新配置的 Main 选项卡中,应该能够看到一个 Project 字段和一个 Main class 字段。对于 Project 字段,选择 Drools4Demo 项目。对于 Main class 字段,输入junit.textui.TestRunner(参见图 3)。

图 3. TestsRulesEngineTest 类的 Drools application 启动配置(Main 选项卡)
TestsRulesEngineTest 类的 Drools application 启动配置(Main 选项卡)

如今选择 Arguments 选项卡并输入 -t demo.test.TestsRulesEngineTest 做为程序参数(参见图 4)。输入该参数后,单击对话框右下角的 Apply 按钮,保存新的启动配置。而后,能够单击 Debug 按钮,开始以 “Drools Application” 的形式调试 TestsRulesEngineTestJUnit 类。若是以前在 testRules1.drl 或 testRules2.drl 中添加了断点,那么当使用这个启动配置时,调试器应该会在遇到这些断点时停下来。

图 4. TestsRulesEngineTest 类的 Drools Application 启动配置(Arguments 选项卡)
TestsRulesEngineTest 类的 Drools Application 启动配置(Arguments 选项卡)
 

结束语

使用规则引擎能够显著下降实现 Java 应用程序中业务规则逻辑的组件的复杂性。使用规则引擎以声明方法表达规则的应用程序比其余应用程序更容易维护和扩展。正如您所看到的,Drools 是一种功能强大的灵活的规则引擎实现。使用 Drools 的特性和能力,您应该可以以声明方式实现应用程序的复杂业务逻辑。Drools 使得学习和使用声明式编程对于 Java 开发人员来讲至关容易。

本文展现的 Drools 类是特定于 Drools 的。若是要在示例程序中使用另外一种规则引擎实现,代码须要做少量更改。由于 Drools 是 JSR 94 兼容的,因此可使用 Java Rule Engine API(如 JSR 94 中所指定)设计特定于 Drools 的类的接口。(Java Rule Engine API 用于 JDBC 在数据库中的规则引擎。)若是使用该 API,则能够无需更改 Java 代码而将规则引擎实现更改成另外一个不一样的实现,只要这个不一样的实现也是 JSR 94 兼容的。JSR 94 不解析包含业务规则的规则文件(在本文示例应用程序中为 testRules1.drl)的结构。文件的结构将仍取决于您选择的规则引擎实现。做为练习,能够修改示例程序以使它使用 Java Rule Engine API,而不是使用 Java 代码引用特定于 Drools 的类。

相关文章
相关标签/搜索