使用 Drools 規則引擎實現業務邏輯

chuanzhongdu1發表於2011-07-25

使用宣告性程式設計方法編寫程式的業務邏輯

要求施加在當今軟體產品上的大多數複雜性是行為和功能方面的,從而導致元件實現具有複雜的業務邏輯。實現 J2EE 或 J2SE 應用程式中業務邏輯最常見的方法是編寫 Java 程式碼來實現需求文件的規則和邏輯。在大多數情況下,該程式碼的錯綜複雜性使得維護和更新應用程式的業務邏輯成為一項令人畏懼的任務,甚至對於經驗豐富的開發人員來說也是如此。任何更改,不管多麼簡單,仍然會產生重編譯和重部署成本。

規則引擎試圖解決(或者至少降低)應用程式業務邏輯的開發和維護中固有的問題和困難。可以將規則引擎看作實現複雜業務邏輯的框架。大多數規則引擎允許您使用宣告性程式設計來表達對於某些給定資訊或知識有效的結果。您可以專注於已知為真的事實及其結果,也就是應用程式的業務邏輯。

有多個規則引擎可供使用,其中包括商業和開放原始碼選擇。商業規則引擎通常允許使用專用的類似英語的語言來表達規則。其他規則引擎允許使用指令碼語言(比如 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 在其上執行多個測試。

  • 在每臺機器上執行的測試取決於每臺機器的型別和功能。目前,定義了五種測試:Test1、Test2、Test3、Test4 和 Test5。

  • 當將測試分配給一臺計算機時,也將測試到期日期 分配給該機器。分配給計算機的測試不能晚於該到期日期執行。到期日期值取決於分配給機器的測試。

  • XYZ 使用可以確定機器型別和功能的內部開發的軟體應用程式,自動化了執行測試時的大部分過程。然後,基於這些屬性,應用程式確定要執行的測試及其到期日期。

  • 目前,為計算機分配測試和測試到期日期的邏輯是該應用程式的已編譯程式碼的一部分。包含該邏輯的元件用 Java 語言編寫。

  • 分配測試和到期日期的邏輯一個月更改多次。當開發人員需要使用 Java 程式碼實現該邏輯時,必須經歷一個冗長乏味的過程。

何時使用規則引擎?

並非所有應用程式都應使用規則引擎。如果業務邏輯程式碼包括很多 if-else 語句,則應考慮使用一個規則引擎。維護複雜的 Boolean 邏輯可能是非常困難的任務,而規則引擎可以幫助您組織該邏輯。當您可以使用宣告方法而非命令程式語言表達邏輯時,變化引入錯誤的可能性會大大降低。

如果程式碼變化可能導致大量的財政損失,則也應考慮規則引擎。許多組織在將已編譯程式碼部署到託管環境中時具有嚴格的規則。例如,如果需要修改 Java 類中的邏輯,在更改進入生產環境之前,將會經歷一個冗長乏味的過程:

  1. 必須重新編譯應用程式程式碼。
  2. 在測試中轉環境中刪除程式碼。
  3. 由資料質量稽核員檢查程式碼。
  4. 由託管環境架構師批准更改。
  5. 計劃程式碼部署。

即使對一行程式碼的簡單更改也可能花費組織的幾千美元。如果需要遵循這些嚴格規則並且發現您頻繁更改業務邏輯程式碼,則非常有必要考慮使用規則引擎。

對客戶的瞭解也是該決策的一個因素。儘管您使用的是一個簡單的需求集合,只需 Java 程式碼中的簡單實現,但是您可能從上一個專案得知,您的客戶具有在開發週期期間甚至部署之後新增和更改業務邏輯需求的傾向(以及財政和政治資源)。如果從一開始就選擇使用規則引擎,您可能會過得舒服一些。

因為在對為計算機分配測試和到期日期的邏輯進行更改時,公司會發生高額成本,所以 XYZ 主管已經要求軟體工程師尋找一種靈活的方法,用最少的代價將對業務規則的更改 “推” 至生產環境。於是 Drools 走上舞臺了。工程師決定,如果它們使用規則引擎來表達確定哪些測試應該執行的規則,則可以節省更多時間和精力。他們將只需要更改規則檔案的內容,然後在生產環境中替換該檔案。對於他們來說,這比更改已編譯程式碼並在將已編譯程式碼部署到生產環境中時進行由組織強制的冗長過程要簡單省時得多(參閱側欄 何時使用規則引擎?)。

目前,在為機器分配測試和到期日期時必須遵循以下業務規則:

  • 如果計算機是 Type1,則只能在其上執行 Test1、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 工作區
  4. 然後選擇下載的存檔檔案並將其匯入工作區中。您將在工作區中發現一個名為 DroolsDemo 的新 Java 專案,如圖 2 所示: 

  5. 如果啟用了 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 例項。然後,使用這個例項配置 DroolsRuleBase 類的一個例項,後者被分配給 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,則只能在該機器上執行 Test1、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 屬性的值)受這些規則的啟動順序所影響。如果檢視 要解決的問題 中詳細的業務規則,您將注意到用於分配測試到期日期的規則具有優先順序。例如,如果已經將 Test3、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 選項卡)

  6. 現在選擇 Arguments 選項卡並輸入 -t demo.test.TestsRulesEngineTest 作為程式引數(參見圖 4)。輸入該引數後,單擊對話方塊右下角的 Apply 按鈕,儲存新的啟動配置。然後,可以單擊 Debug 按鈕,開始以 “Drools Application” 的形式除錯TestsRulesEngineTest JUnit 類。如果之前在 testRules1.drl 或 testRules2.drl 中新增了斷點,那麼當使用這個啟動配置時,偵錯程式應該會在遇到這些斷點時停下來。


    圖 4. TestsRulesEngineTest 類的 Drools Application 啟動配置(Arguments 選項卡)
  7. 結束語

    使用規則引擎可以顯著降低實現 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 的類。

    原文地址:http://www.ibm.com/developerworks/cn/java/j-drools/#download

相關文章