級(jí)別: 中級(jí)
Ricardo Olivieri (roliv@us.ibm.com), 軟件工程師, IBM
2006 年 6 月 19 日
更新 2008 年 4 月 15 日
使用規(guī)則引擎可以通過降低實(shí)現(xiàn)復(fù)雜業(yè)務(wù)邏輯的組件的復(fù)雜性,降低應(yīng)用程序的維護(hù)和可擴(kuò)展性成本。這篇更新的文章展示如何使用開源的 Drools 規(guī)則引擎讓 Java™ 應(yīng)用程序更適應(yīng)變化。Drools 項(xiàng)目引入了一個(gè)新的本地規(guī)則表達(dá)式語言和一個(gè) Eclipse 插件,使 Drools 比以前更容易使用。
要求施加在當(dāng)今軟件產(chǎn)品上的大多數(shù)復(fù)雜性是行為和功能方面的,從而導(dǎo)致組件實(shí)現(xiàn)具有復(fù)雜的業(yè)務(wù)邏輯。實(shí)現(xiàn) J2EE 或 J2SE 應(yīng)用程序中業(yè)務(wù)邏輯最常見的方法是編寫 Java 代碼來實(shí)現(xiàn)需求文檔的規(guī)則和邏輯。在大多數(shù)情況下,該代碼的錯(cuò)綜復(fù)雜性使得維護(hù)和更新應(yīng)用程序的業(yè)務(wù)邏輯成為一項(xiàng)令人畏懼的任務(wù),甚至對(duì)于經(jīng)驗(yàn)豐富的開發(fā)人員來說也是如此。任何更改,不管多么簡(jiǎn)單,仍然會(huì)產(chǎn)生重編譯和重部署成本。
規(guī)則引擎試圖解決(或者至少降低)應(yīng)用程序業(yè)務(wù)邏輯的開發(fā)和維護(hù)中固有的問題和困難。可以將規(guī)則引擎看作實(shí)現(xiàn)復(fù)雜業(yè)務(wù)邏輯的框架。大多數(shù)規(guī)則引擎允許您使用聲明性編程來表達(dá)對(duì)于某些給定信息或知識(shí)有效的結(jié)果。您可以專注于已知為真的事實(shí)及其結(jié)果,也就是應(yīng)用程序的業(yè)務(wù)邏輯。
有多個(gè)規(guī)則引擎可供使用,其中包括商業(yè)和開放源碼選擇。商業(yè)規(guī)則引擎通常允許使用專用的類似英語的語言來表達(dá)規(guī)則。其他規(guī)則引擎允許使用腳本語言(比如 Groovy 或 Python)編寫規(guī)則。這篇更新的文章為您介紹 Drools 引擎,并使用示例程序幫助您理解如何使用 Drools 作為 Java 應(yīng)用程序中業(yè)務(wù)邏輯層的一部分。
 |
更多事情在變化……
俗話說得好,“惟一不變的是變化。”軟件應(yīng)用程序的業(yè)務(wù)邏輯正是如此。出于以下原因,實(shí)現(xiàn)應(yīng)用程序業(yè)務(wù)邏輯的組件可能必須更改:
- 在開發(fā)期間或部署后修復(fù)代碼缺陷
- 應(yīng)付特殊狀況,即客戶一開始沒有提到要將業(yè)務(wù)邏輯考慮在內(nèi)
- 處理客戶已更改的業(yè)務(wù)目標(biāo)
- 符合組織對(duì)敏捷或迭代開發(fā)過程的使用
如果存在這些可能性,則迫切需要一個(gè)無需太多復(fù)雜性就能處理業(yè)務(wù)邏輯更改的應(yīng)用程序,尤其是當(dāng)更改復(fù)雜 if-else 邏輯的開發(fā)人員并不是以前編寫代碼的開發(fā)人員時(shí)。
|
|
Drools 是用 Java 語言編寫的開放源碼規(guī)則引擎,使用 Rete 算法(參閱 參考資料)對(duì)所編寫的規(guī)則求值。Drools 允許使用聲明方式表達(dá)業(yè)務(wù)邏輯。可以使用非 XML 的本地語言編寫規(guī)則,從而便于學(xué)習(xí)和理解。并且,還可以將 Java 代碼直接嵌入到規(guī)則文件中,這令 Drools 的學(xué)習(xí)更加吸引人。Drools 還具有其他優(yōu)點(diǎn):
- 非常活躍的社區(qū)支持
- 易用
- 快速的執(zhí)行速度
- 在 Java 開發(fā)人員中流行
- 與 Java Rule Engine API(JSR 94)兼容(參閱 參考資料)
- 免費(fèi)
當(dāng)前 Drools 版本
在編寫本文之際,Drools 規(guī)則引擎的最新版本是 4.0.4。這是一個(gè)重要更新。雖然現(xiàn)在還存在一些向后兼容性問題,但這個(gè)版本的特性讓 Drools 比以前更有吸引力。例如,用于表達(dá)規(guī)則的新的本地語言比舊版本使用的 XML 格式更簡(jiǎn)單,更優(yōu)雅。這種新語言所需的代碼更少,并且格式易于閱讀。
另一個(gè)值得注意的進(jìn)步是,新版本提供了用于 Eclipse IDE(Versions 3.2 和 3.3)的一個(gè) Drools 插件。我強(qiáng)烈建議您通過這個(gè)插件來使用 Drools。它可以簡(jiǎn)化使用 Drools 的項(xiàng)目開發(fā),并且可以提高生產(chǎn)率。例如,該插件會(huì)檢查規(guī)則文件是否有語法錯(cuò)誤,并提供代碼完成功能。它還使您可以調(diào)試規(guī)則文件,將調(diào)試時(shí)間從數(shù)小時(shí)減少到幾分鐘。您可以在規(guī)則文件中添加斷點(diǎn),以便在規(guī)則執(zhí)行期間的特定時(shí)刻檢查對(duì)象的狀態(tài)。這使您可以獲得關(guān)于規(guī)則引擎在特定時(shí)刻所處理的知識(shí)(knowledge)(在本文的后面您將熟悉這個(gè)術(shù)語)的信息。
要解決的問題
本文展示如何使用 Drools 作為示例 Java 應(yīng)用程序中業(yè)務(wù)邏輯層的一部分。為了理解本文,您應(yīng)該熟悉使用 Eclipse IDE 開發(fā)和調(diào)試 Java 代碼。并且,您還應(yīng)該熟悉 JUnit 測(cè)試框架,并知道如何在 Eclipse 中使用它。
下列假設(shè)為應(yīng)用程序解決的虛構(gòu)問題設(shè)置了場(chǎng)景:
- 名為 XYZ 的公司構(gòu)建兩種類型的計(jì)算機(jī)機(jī)器:Type1 和 Type2。機(jī)器類型按其架構(gòu)定義。
- XYZ 計(jì)算機(jī)可以提供多種功能。當(dāng)前定義了四種功能:DDNS Server、DNS Server、Gateway 和 Router。
- 在發(fā)運(yùn)每臺(tái)機(jī)器之前,XYZ 在其上執(zhí)行多個(gè)測(cè)試。
- 在每臺(tái)機(jī)器上執(zhí)行的測(cè)試取決于每臺(tái)機(jī)器的類型和功能。目前,定義了五種測(cè)試:Test1、Test2、Test3、Test4 和 Test5。
- 當(dāng)將測(cè)試分配給一臺(tái)計(jì)算機(jī)時(shí),也將測(cè)試到期日期 分配給該機(jī)器。分配給計(jì)算機(jī)的測(cè)試不能晚于該到期日期執(zhí)行。到期日期值取決于分配給機(jī)器的測(cè)試。
- XYZ 使用可以確定機(jī)器類型和功能的內(nèi)部開發(fā)的軟件應(yīng)用程序,自動(dòng)化了執(zhí)行測(cè)試時(shí)的大部分過程。然后,基于這些屬性,應(yīng)用程序確定要執(zhí)行的測(cè)試及其到期日期。
- 目前,為計(jì)算機(jī)分配測(cè)試和測(cè)試到期日期的邏輯是該應(yīng)用程序的已編譯代碼的一部分。包含該邏輯的組件用 Java 語言編寫。
- 分配測(cè)試和到期日期的邏輯一個(gè)月更改多次。當(dāng)開發(fā)人員需要使用 Java 代碼實(shí)現(xiàn)該邏輯時(shí),必須經(jīng)歷一個(gè)冗長(zhǎng)乏味的過程。
 |
何時(shí)使用規(guī)則引擎?
并非所有應(yīng)用程序都應(yīng)使用規(guī)則引擎。如果業(yè)務(wù)邏輯代碼包括很多 if-else 語句,則應(yīng)考慮使用一個(gè)規(guī)則引擎。維護(hù)復(fù)雜的 Boolean 邏輯可能是非常困難的任務(wù),而規(guī)則引擎可以幫助您組織該邏輯。當(dāng)您可以使用聲明方法而非命令編程語言表達(dá)邏輯時(shí),變化引入錯(cuò)誤的可能性會(huì)大大降低。
如果代碼變化可能導(dǎo)致大量的財(cái)政損失,則也應(yīng)考慮規(guī)則引擎。許多組織在將已編譯代碼部署到托管環(huán)境中時(shí)具有嚴(yán)格的規(guī)則。例如,如果需要修改 Java 類中的邏輯,在更改進(jìn)入生產(chǎn)環(huán)境之前,將會(huì)經(jīng)歷一個(gè)冗長(zhǎng)乏味的過程:
- 必須重新編譯應(yīng)用程序代碼。
- 在測(cè)試中轉(zhuǎn)環(huán)境中刪除代碼。
- 由數(shù)據(jù)質(zhì)量審核員檢查代碼。
- 由托管環(huán)境架構(gòu)師批準(zhǔn)更改。
- 計(jì)劃代碼部署。
即使對(duì)一行代碼的簡(jiǎn)單更改也可能花費(fèi)組織的幾千美元。如果需要遵循這些嚴(yán)格規(guī)則并且發(fā)現(xiàn)您頻繁更改業(yè)務(wù)邏輯代碼,則非常有必要考慮使用規(guī)則引擎。
對(duì)客戶的了解也是該決策的一個(gè)因素。盡管您使用的是一個(gè)簡(jiǎn)單的需求集合,只需 Java 代碼中的簡(jiǎn)單實(shí)現(xiàn),但是您可能從上一個(gè)項(xiàng)目得知,您的客戶具有在開發(fā)周期期間甚至部署之后添加和更改業(yè)務(wù)邏輯需求的傾向(以及財(cái)政和政治資源)。如果從一開始就選擇使用規(guī)則引擎,您可能會(huì)過得舒服一些。
|
|
因?yàn)樵趯?duì)為計(jì)算機(jī)分配測(cè)試和到期日期的邏輯進(jìn)行更改時(shí),公司會(huì)發(fā)生高額成本,所以 XYZ 主管已經(jīng)要求軟件工程師尋找一種靈活的方法,用最少的代價(jià)將對(duì)業(yè)務(wù)規(guī)則的更改 “推” 至生產(chǎn)環(huán)境。于是 Drools 走上舞臺(tái)了。工程師決定,如果它們使用規(guī)則引擎來表達(dá)確定哪些測(cè)試應(yīng)該執(zhí)行的規(guī)則,則可以節(jié)省更多時(shí)間和精力。他們將只需要更改規(guī)則文件的內(nèi)容,然后在生產(chǎn)環(huán)境中替換該文件。對(duì)于他們來說,這比更改已編譯代碼并在將已編譯代碼部署到生產(chǎn)環(huán)境中時(shí)進(jìn)行由組織強(qiáng)制的冗長(zhǎng)過程要簡(jiǎn)單省時(shí)得多(參閱側(cè)欄 何時(shí)使用規(guī)則引擎?)。
目前,在為機(jī)器分配測(cè)試和到期日期時(shí)必須遵循以下業(yè)務(wù)規(guī)則:
- 如果計(jì)算機(jī)是 Type1,則只能在其上執(zhí)行 Test1、Test2 和 Test5。
- 如果計(jì)算機(jī)是 Type2 且其中一個(gè)功能為 DNS Server,則應(yīng)執(zhí)行 Test4 和 Test5。
- 如果計(jì)算機(jī)是 Type2 且其中一個(gè)功能為 DDNS Server,則應(yīng)執(zhí)行 Test2 和 Test3。
- 如果計(jì)算機(jī)是 Type2 且其中一個(gè)功能為 Gateway,則應(yīng)執(zhí)行 Test3 和 Test4。
- 如果計(jì)算機(jī)是 Type2 且其中一個(gè)功能為 Router,則應(yīng)執(zhí)行 Test1 和 Test3。
- 如果 Test1 是要在計(jì)算機(jī)上執(zhí)行的測(cè)試之一,則測(cè)試到期日期距離機(jī)器的創(chuàng)建日期 3 天。該規(guī)則優(yōu)先于測(cè)試到期日期的所有下列規(guī)則。
- 如果 Test2 是要在計(jì)算機(jī)上執(zhí)行的測(cè)試之一,則測(cè)試到期日期距離機(jī)器的創(chuàng)建日期 7 天。該規(guī)則優(yōu)先于測(cè)試到期日期的所有下列規(guī)則。
- 如果 Test3 是要在計(jì)算機(jī)上執(zhí)行的測(cè)試之一,則測(cè)試到期日期距離機(jī)器的創(chuàng)建日期 10 天。該規(guī)則優(yōu)先于測(cè)試到期日期的所有下列規(guī)則。
- 如果 Test4 是要在計(jì)算機(jī)上執(zhí)行的測(cè)試之一,則測(cè)試到期日期距離機(jī)器的創(chuàng)建日期 12 天。該規(guī)則優(yōu)先于測(cè)試到期日期的所有下列規(guī)則。
- 如果 Test5 是要在計(jì)算機(jī)上執(zhí)行的測(cè)試之一,則測(cè)試到期日期距離機(jī)器的創(chuàng)建日期 14 天。
捕獲為機(jī)器分配測(cè)試和測(cè)試到期日期的上述業(yè)務(wù)規(guī)則的當(dāng)前 Java 代碼如清單 1 所示:
清單 1. 使用 if-else 語句實(shí)現(xiàn)業(yè)務(wù)規(guī)則邏輯
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 中的代碼不是太復(fù)雜,但也并不簡(jiǎn)單。如果要對(duì)其進(jìn)行更改,需要十分小心。一堆互相纏繞的 if-else 語句正試圖捕獲已經(jīng)為應(yīng)用程序標(biāo)識(shí)的業(yè)務(wù)邏輯。如果您對(duì)業(yè)務(wù)規(guī)則不甚了解,就無法一眼看出代碼的意圖。
導(dǎo)入示例程序
使用 Drools 規(guī)則的示例程序附帶在本文的 ZIP 存檔中。程序使用 Drools 規(guī)則文件以聲明方法表示上一節(jié)定義的業(yè)務(wù)規(guī)則。它包含一個(gè) Eclipse 3.2 Java 項(xiàng)目,該項(xiàng)目是使用 Drools 插件和 4.0.4 版的 Drools 規(guī)則引擎開發(fā)的。請(qǐng)遵循以下步驟設(shè)置示例程序:
- 下載 ZIP 存檔(參見 下載)。
- 下載并安裝 Drools Eclipse 插件(參見 參考資料)。
- 在 Eclipse 中,選擇該選項(xiàng)以導(dǎo)入 Existing Projects into Workspace,如圖 1 所示:
圖 1. 將示例程序?qū)氲?Eclipse 工作區(qū)
- 然后選擇下載的存檔文件并將其導(dǎo)入工作區(qū)中。您將在工作區(qū)中發(fā)現(xiàn)一個(gè)名為
DroolsDemo 的新 Java 項(xiàng)目,如圖 2 所示:
圖 2. 導(dǎo)入到工作區(qū)中的示例程序
如果啟用了 Build automatically 選項(xiàng),則代碼應(yīng)該已編譯并可供使用。如果未啟用該選項(xiàng),則現(xiàn)在構(gòu)建 DroolsDemo 項(xiàng)目。
檢查代碼
現(xiàn)在來看一下示例程序中的代碼。該程序的 Java 類的核心集合位于 demo 包中。在該包中可以找到 Machine 和 Test 域?qū)ο箢悺?code>Machine 類的實(shí)例表示要分配測(cè)試和測(cè)試到期日期的計(jì)算機(jī)機(jī)器。下面來看 Machine 類,如清單 2 所示:
清單 2. Machine 類的實(shí)例變量
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 屬性)—— 保存機(jī)器的類型值。
functions (表示為 list )—— 保存機(jī)器的功能。
testsDueTime (表示為 timestamp 變量)—— 保存分配的測(cè)試到期日期值。
tests (Collection 對(duì)象)—— 保存分配的測(cè)試集合。
注意,可以為機(jī)器分配多個(gè)測(cè)試,而且一個(gè)機(jī)器可以具有一個(gè)或多個(gè)功能。
出于簡(jiǎn)潔目的,機(jī)器的創(chuàng)建日期值設(shè)置為創(chuàng)建 Machine 類的實(shí)例時(shí)的當(dāng)前時(shí)間。如果這是真實(shí)的應(yīng)用程序,創(chuàng)建時(shí)間將設(shè)置為機(jī)器最終構(gòu)建完成并準(zhǔn)備測(cè)試的實(shí)際時(shí)間。
Test 類的實(shí)例表示可以分配給機(jī)器的測(cè)試。Test 實(shí)例由其 id 和 name 惟一描述,如清單 3 所示:
清單 3. Test 類的實(shí)例變量
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 規(guī)則引擎對(duì) Machine 類的實(shí)例求值。基于 Machine 實(shí)例的 type 和 functions 屬性的值,規(guī)則引擎確定應(yīng)分配給 tests 和 testsDueTime 屬性的值。
在 demo 包中,還會(huì)發(fā)現(xiàn) Test 對(duì)象的數(shù)據(jù)訪問對(duì)象 (TestDAOImpl ) 的實(shí)現(xiàn),它允許您按照 ID 查找 Test 實(shí)例。該數(shù)據(jù)訪問對(duì)象極其簡(jiǎn)單;它不連接任何外部資源(比如關(guān)系數(shù)據(jù)庫(kù))以獲得 Test 實(shí)例。相反,在其定義中硬編碼了預(yù)定義的 Test 實(shí)例集合。在現(xiàn)實(shí)世界中,您可能會(huì)具有連接外部資源以檢索 Test 對(duì)象的數(shù)據(jù)訪問對(duì)象。
RulesEngine 類
demo 中比較重要(如果不是最重要的)的一個(gè)類是 RulesEngine 類。該類的實(shí)例用作封裝邏輯以訪問 Drools 類的包裝器對(duì)象。可以在您自己的 Java 項(xiàng)目中容易地重用該類,因?yàn)樗倪壿嫴皇翘囟ㄓ谑纠绦虻摹G鍐?4 展示了該類的屬性和構(gòu)造函數(shù):
清單 4. RulesEngine 類的實(shí)例變量和構(gòu)造函數(shù)
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 類的構(gòu)造函數(shù)接受字符串值形式的參數(shù),該值表示包含業(yè)務(wù)規(guī)則集合的文件的名稱。該構(gòu)造函數(shù)使用 PackageBuilder 類的實(shí)例解析和編譯源文件中包含的規(guī)則。(注意: 該代碼假設(shè)規(guī)則文件位于程序類路徑中名為 rules 的文件夾中。)然后,使用 PackageBuilder 實(shí)例將所有編譯好的規(guī)則合并為一個(gè)二進(jìn)制 Package 實(shí)例。然后,使用這個(gè)實(shí)例配置 Drools RuleBase 類的一個(gè)實(shí)例,后者被分配給 RulesEngine 類的 rules 屬性。可以將這個(gè)類的實(shí)例看作規(guī)則文件中所包含規(guī)則的內(nèi)存中表示。
清單 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 代碼中的所有魔力。調(diào)用該方法執(zhí)行先前加載到類構(gòu)造函數(shù)中的規(guī)則。Drools WorkingMemory 類的實(shí)例用于斷言或聲明知識(shí),規(guī)則引擎應(yīng)使用它來確定應(yīng)執(zhí)行的結(jié)果。(如果滿足規(guī)則的所有條件,則執(zhí)行該規(guī)則的結(jié)果。)將知識(shí)當(dāng)作規(guī)則引擎用于確定是否應(yīng)啟動(dòng)規(guī)則的數(shù)據(jù)或信息。例如,規(guī)則引擎的知識(shí)可以包含一個(gè)或多個(gè)對(duì)象及其屬性的當(dāng)前狀態(tài)。
規(guī)則結(jié)果在調(diào)用 WorkingMemory 對(duì)象的 fireAllRules() 方法時(shí)執(zhí)行。您可能奇怪(我希望您如此)知識(shí)是如何插入到 WorkingMemory 實(shí)例中的。如果仔細(xì)看一下該方法的簽名,將會(huì)注意到所傳遞的參數(shù)是 WorkingEnvironmentCallback 接口的實(shí)例。executeRules() 方法的調(diào)用者需要?jiǎng)?chuàng)建實(shí)現(xiàn)該接口的對(duì)象。該接口只需要開發(fā)人員實(shí)現(xiàn)一個(gè)方法(參見清單 6 ):
清單 6. WorkingEnvironmentCallback 接口
public interface WorkingEnvironmentCallback {
void initEnvironment(WorkingMemory workingMemory) throws FactException;
}
|
所以,應(yīng)該是 executeRules() 方法的調(diào)用者將知識(shí)插入到 WorkingMemory 實(shí)例中的。稍后將展示這是如何實(shí)現(xiàn)的。
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 類只有兩個(gè)實(shí)例變量。rulesEngine 屬性是 RulesEngine 類的實(shí)例。 testDAO 屬性保存對(duì) TestDAO 接口的具體實(shí)現(xiàn)的引用。 rulesEngine 對(duì)象是使用 "testRules1.drl" 字符串作為其構(gòu)造函數(shù)的參數(shù)實(shí)例化的。testRules1.drl 文件以聲明方式捕獲 要解決的問題 中的業(yè)務(wù)規(guī)則。 TestsRulesEngine 類的 assignTests() 方法調(diào)用 RulesEngine 類的 executeRules() 方法。在這個(gè)方法中,創(chuàng)建了 WorkingEnvironmentCallback 接口的一個(gè)匿名實(shí)例,然后將該實(shí)例作為參數(shù)傳遞給 executeRules() 方法。
如果查看 assignTests() 方法的實(shí)現(xiàn),可以看到知識(shí)是如何插入到 WorkingMemory 實(shí)例中的。 WorkingMemory 類的 insert() 方法被調(diào)用以聲明在對(duì)規(guī)則求值時(shí)規(guī)則引擎應(yīng)使用的知識(shí)。在這種情況下,知識(shí)由 Machine 類的一個(gè)實(shí)例組成。被插入的對(duì)象用于對(duì)規(guī)則的條件求值。
如果在對(duì)條件求值時(shí),需要讓規(guī)則引擎引用未 用作知識(shí)的對(duì)象,則應(yīng)使用 WorkingMemory 類的 setGlobal() 方法。在示例程序中,setGlobal() 方法將對(duì) TestDAO 實(shí)例的引用傳遞給規(guī)則引擎。然后規(guī)則引擎使用 TestDAO 實(shí)例查找它可能需要的任何 Test 實(shí)例。
TestsRulesEngine 類是示例程序中惟一的 Java 代碼,它包含專門致力于為機(jī)器分配測(cè)試和測(cè)試到期日期的實(shí)現(xiàn)的邏輯。該類中的邏輯永遠(yuǎn)不需要更改,即使業(yè)務(wù)規(guī)則需要更新時(shí)也是如此。
Drools 規(guī)則文件
如前所述,testRules.xml 文件包含規(guī)則引擎為機(jī)器分配測(cè)試和測(cè)試到期日期所遵循的規(guī)則。它使用 Drools 本地語言表達(dá)所包含的規(guī)則。
Drools 規(guī)則文件有一個(gè)或多個(gè) rule 聲明。每個(gè) rule 聲明由一個(gè)或多個(gè) conditional 元素以及要執(zhí)行的一個(gè)或多個(gè) consequences 或 actions 組成。一個(gè)規(guī)則文件還可以有多個(gè)(即 0 個(gè)或多個(gè))import 聲明、多個(gè) global 聲明以及多個(gè) function 聲明。
理解 Drools 規(guī)則文件組成最好的方法是查看一個(gè)真正的規(guī)則文件。下面來看 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 聲明如何讓規(guī)則執(zhí)行引擎知道在哪里查找將在規(guī)則中使用的對(duì)象的類定義。global 聲明讓規(guī)則引擎知道,某個(gè)對(duì)象應(yīng)該可以從規(guī)則中訪問,但該對(duì)象不應(yīng)是用于對(duì)規(guī)則條件求值的知識(shí)的一部分。可以將 global 聲明看作規(guī)則中的全局變量。對(duì)于 global 聲明,需要指定它的類型(即類名)和想要用于引用它的標(biāo)識(shí)符(即變量名)。global 聲明中的這個(gè)標(biāo)識(shí)符名稱應(yīng)該與調(diào)用 WorkingMemory 類的 setGlobal() 方法時(shí)使用的標(biāo)識(shí)符值匹配,在此即為 testDAO (參見 清單 7)。
function 關(guān)鍵詞用于定義一個(gè) Java 函數(shù)(參見 清單 9)。如果看到 consequence(稍后將討論)中重復(fù)的代碼,則應(yīng)該提取該代碼并將其編寫為一個(gè) Java 函數(shù)。但是,這樣做時(shí)要小心,避免在 Drools 規(guī)則文件中編寫復(fù)雜的 Java 代碼。規(guī)則文件中定義的 Java 函數(shù)應(yīng)該簡(jiǎn)短易懂。這不是 Drools 的技術(shù)限制。如果想要在規(guī)則文件中編寫復(fù)雜的 Java 代碼,也可以。但這樣做可能會(huì)讓您的代碼更加難以測(cè)試、調(diào)試和維護(hù)。復(fù)雜的 Java 代碼應(yīng)該是 Java 類的一部分。如果需要 Drools 規(guī)則執(zhí)行引擎調(diào)用復(fù)雜的 Java 代碼,則可以將對(duì)包含復(fù)雜代碼的 Java 類的引用作為全局?jǐn)?shù)據(jù)傳遞給規(guī)則引擎。
清單 9. testRules1.drl 文件中定義的 Java 函數(shù)
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 文件中定義的第一個(gè)規(guī)則:
清單 10. testRules1.drl 中定義的第一個(gè)規(guī)則
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 聲明有一個(gè)惟一標(biāo)識(shí)它的 name 。還可以看到,when 關(guān)鍵詞定義規(guī)則中的條件塊,then 關(guān)鍵詞定義結(jié)果塊。清單 10 中顯示的規(guī)則有一個(gè)引用 Machine 對(duì)象的條件元素。如果回到 清單 7 可以看到, Machine 對(duì)象被插入到 WorkingMemory 對(duì)象中。這正是這個(gè)規(guī)則中使用的對(duì)象。條件元素對(duì) Machine 實(shí)例(知識(shí)的一部分)求值,以確定是否應(yīng)執(zhí)行規(guī)則的結(jié)果。如果條件元素等于 true ,則啟動(dòng)或執(zhí)行結(jié)果。從清單 10 中還可以看出,結(jié)果只不過是一個(gè) Java 語言語句。通過快速瀏覽該規(guī)則,可以很容易地識(shí)別出這是下列業(yè)務(wù)規(guī)則的實(shí)現(xiàn):
- 如果計(jì)算機(jī)是 Type1,則只能在該機(jī)器上執(zhí)行 Test1、Test2 和 Test5。
因此,該規(guī)則的條件元素檢查( Machine 對(duì)象的) type 屬性是否為 Type1 。 (在條件元素中,只要對(duì)象遵從 Java bean 模式,就可以直接訪問對(duì)象的屬性,而不必調(diào)用 getter 方法。)如果該屬性的值為 true ,那么將 Machine 實(shí)例的一個(gè)引用分配給 machine 標(biāo)識(shí)符。然后,在規(guī)則的結(jié)果塊使用該引用,將測(cè)試分配給 Machine 對(duì)象。
在該規(guī)則中,惟一看上去有些奇怪的語句是最后三條結(jié)果語句。回憶 “要解決的問題” 小節(jié)中的業(yè)務(wù)規(guī)則,應(yīng)該分配為測(cè)試到期日期的值取決于分配給機(jī)器的測(cè)試。因此,分配給機(jī)器的測(cè)試需要成為規(guī)則執(zhí)行引擎在對(duì)規(guī)則求值時(shí)所使用的知識(shí)的一部分。這正是這三條語句的作用。這些語句使用一個(gè)名為 insert 的方法更新規(guī)則引擎中的知識(shí)。
確定規(guī)則執(zhí)行順序
規(guī)則的另一個(gè)重要的方面是可選的 salience 屬性。使用它可以讓規(guī)則執(zhí)行引擎知道應(yīng)該啟動(dòng)規(guī)則的結(jié)果語句的順序。具有最高顯著值的規(guī)則的結(jié)果語句首先執(zhí)行;具有第二高顯著值的規(guī)則的結(jié)果語句第二執(zhí)行,依此類推。當(dāng)您需要讓規(guī)則按預(yù)定義順序啟動(dòng)時(shí),這一點(diǎn)非常重要,很快您將會(huì)看到。
testRules1.drl 文件中接下來的四個(gè)規(guī)則實(shí)現(xiàn)與機(jī)器測(cè)試分配有關(guān)的其他業(yè)務(wù)規(guī)則(參見清單 11)。這些規(guī)則與剛討論的第一個(gè)規(guī)則非常相似。注意,salience 屬性值對(duì)于前五個(gè)規(guī)則是相同的;不管這五個(gè)規(guī)則的啟動(dòng)順序如何,其執(zhí)行結(jié)果將相同。如果結(jié)果受規(guī)則的啟動(dòng)順序影響,則需要為規(guī)則指定不同的顯著值。
清單 11. testRules1.drl 文件中與測(cè)試分配有關(guān)的其他規(guī)則
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 規(guī)則文件中的其他規(guī)則。您可能已經(jīng)猜到,這些規(guī)則與測(cè)試到期日期的分配有關(guān):
清單 12. testRules1.drl 文件中與測(cè)試到期日期分配有關(guān)的規(guī)則
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
|
這些規(guī)則的實(shí)現(xiàn)比用于分配測(cè)試的規(guī)則的實(shí)現(xiàn)要略微簡(jiǎn)單一些,但我發(fā)現(xiàn)它們更有趣一些,原因有四。
第一,注意這些規(guī)則的執(zhí)行順序很重要。結(jié)果(即,分配給 Machine 實(shí)例的 testsDueTime 屬性的值)受這些規(guī)則的啟動(dòng)順序所影響。如果查看 要解決的問題 中詳細(xì)的業(yè)務(wù)規(guī)則,您將注意到用于分配測(cè)試到期日期的規(guī)則具有優(yōu)先順序。例如,如果已經(jīng)將 Test3、Test4 和 Test5 分配給機(jī)器,則測(cè)試到期日期應(yīng)距離機(jī)器的創(chuàng)建日期 10 天。原因在于 Test3 的到期日期規(guī)則優(yōu)先于 Test4 和 Test5 的測(cè)試到期日期規(guī)則。如何在 Drools 規(guī)則文件中表達(dá)這一點(diǎn)呢?答案是 salience 屬性。為 testsDueTime 屬性設(shè)置值的規(guī)則的 salience 屬性值不同。Test1 的測(cè)試到期日期規(guī)則優(yōu)先于所有其他測(cè)試到期日期規(guī)則,所以這應(yīng)是要啟動(dòng)的最后一個(gè)規(guī)則。換句話說,如果 Test1 是分配給機(jī)器的測(cè)試之一,則由該規(guī)則分配的值應(yīng)該是優(yōu)先使用的值。所以,該規(guī)則的 salience 屬性值最低:10。
第二,每個(gè)規(guī)則有兩個(gè)條件元素。第一個(gè)元素只檢查工作內(nèi)存中是否存在一個(gè) Machine 實(shí)例。(注意,這里不會(huì)對(duì) Machine 對(duì)象的屬性進(jìn)行比較。)當(dāng)這個(gè)元素等于 true 時(shí),它將一個(gè)引用分配給 Machine 對(duì)象,而后者將在規(guī)則的結(jié)果塊被用到。如果不分配這個(gè)引用,那么就無法將測(cè)試到期日期分配給 Machine 對(duì)象。第二個(gè)條件元素檢查 Test 對(duì)象的 id 屬性。當(dāng)且僅當(dāng)這兩個(gè)條件元素都等于 true 時(shí),才執(zhí)行規(guī)則的結(jié)果元素。
第三,在 Test 類的一個(gè)實(shí)例成為知識(shí)的一部分(即,包含在工作內(nèi)存中)之前,Drools 規(guī)則執(zhí)行引擎不會(huì)(也不能)對(duì)這些規(guī)則的條件塊求值。這很符合邏輯,因?yàn)槿绻ぷ鲀?nèi)存中還沒有 Test 類的一個(gè)實(shí)例,那么規(guī)則執(zhí)行引擎就無法執(zhí)行這些規(guī)則的條件中所包含的比較。如果您想知道 Test 實(shí)例何時(shí)成為知識(shí)的一部分,那么可以回憶,在與分配測(cè)試相關(guān)規(guī)則的結(jié)果的執(zhí)行期間,一個(gè)或多個(gè) Test 實(shí)例被插入到工作內(nèi)存中。(參見 清單 10 和 清單 11)。
第四,注意這些規(guī)則的結(jié)果塊相當(dāng)簡(jiǎn)短。原因在于在所有結(jié)果塊中調(diào)用了規(guī)則文件中之前使用 function 關(guān)鍵詞定義的 setTestsDueTime() Java 方法。該方法為 testsDueTime 屬性實(shí)際分配值。
測(cè)試代碼
既然已經(jīng)仔細(xì)檢查了實(shí)現(xiàn)業(yè)務(wù)規(guī)則邏輯的代碼,現(xiàn)在應(yīng)該檢查它是否能工作。要執(zhí)行示例程序,運(yùn)行 demo.test 中的 TestsRulesEngineTest JUnit 測(cè)試。
在該測(cè)試中,創(chuàng)建了 5 個(gè) Machine 對(duì)象,每個(gè)對(duì)象具有不同的屬性集合(序號(hào)、類型和功能)。為這五個(gè) Machine 對(duì)象的每一個(gè)都調(diào)用 TestsRulesEngine 類的 assignTests() 方法。一旦 assignTests() 方法完成其執(zhí)行,就執(zhí)行斷言以驗(yàn)證 testRules1.drl 中指定的業(yè)務(wù)規(guī)則邏輯是否正確(參見清單 13)。可以修改 TestsRulesEngineTest JUnit 類以多添加幾個(gè)具有不同屬性的 Machine 實(shí)例,然后使用斷言驗(yàn)證結(jié)果是否跟預(yù)期一樣。
清單 13. testTestsRulesEngine() 方法中用于驗(yàn)證業(yè)務(wù)邏輯實(shí)現(xiàn)是否正確的斷言
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);
...
|

 |

|
關(guān)于知識(shí)的其他備注
值得一提的是,除了將對(duì)象插入至工作內(nèi)存之外,還可以在工作內(nèi)存中修改對(duì)象或從中撤回對(duì)象。可以在規(guī)則的結(jié)果塊中進(jìn)行這些操作。如果在結(jié)果語句中修改作為當(dāng)前知識(shí)一部分的對(duì)象,并且所修改的屬性被用在 condition 元素中以確定是否應(yīng)啟動(dòng)規(guī)則,則應(yīng)在結(jié)果塊中調(diào)用 update() 方法。調(diào)用 update() 方法時(shí),您讓 Drools 規(guī)則引擎知道對(duì)象已更新且引用該對(duì)象的任何規(guī)則的任何條件元素(例如,檢查一個(gè)或多個(gè)對(duì)象屬性的值)應(yīng)再次求值,以確定條件的結(jié)果現(xiàn)在是 true 還是 false 。這意味著甚至當(dāng)前活動(dòng)規(guī)則(在其結(jié)果塊中修改對(duì)象的規(guī)則)的條件都可以再次求值,這可能導(dǎo)致規(guī)則再次啟動(dòng),并可能導(dǎo)致無限循環(huán)。如果不希望這種情況發(fā)生,則應(yīng)該包括 rule 的可選 no-loop 屬性并將其賦值為 true 。
清單 14 用兩個(gè)規(guī)則的定義的偽代碼演示了這種情況。Rule 1 修改 objectA 的 property1 。然后它調(diào)用 update() 方法,以允許規(guī)則執(zhí)行引擎知道該更新,從而觸發(fā)對(duì)引用 objectA 的規(guī)則的條件元素的重新求值。因此,啟動(dòng) Rule 1 的條件應(yīng)再次求值。因?yàn)樵摋l件應(yīng)再次等于 true (property2 的值仍相同,因?yàn)樗诮Y(jié)果塊中未更改),Rule 1 應(yīng)再次啟動(dòng),從而導(dǎo)致無限循環(huán)的執(zhí)行。為了避免這種情況,添加 no-loop 屬性并將其賦值為 true ,從而避免當(dāng)前活動(dòng)規(guī)則再次執(zhí)行。
清單 14. 修改工作內(nèi)存中的對(duì)象并使用規(guī)則元素的 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
...
|
如果對(duì)象不再是知識(shí)的一部分,則應(yīng)將該對(duì)象從工作內(nèi)存中撤回(參見清單 15)。通過在結(jié)果塊中調(diào)用 retract() 方法實(shí)現(xiàn)這一點(diǎn)。當(dāng)從工作內(nèi)存中移除對(duì)象之后,引用該對(duì)象的(屬于任何規(guī)則的)任何條件元素將不被求值。因?yàn)閷?duì)象不再作為知識(shí)的一部分存在,所以規(guī)則沒有啟動(dòng)的機(jī)會(huì)。
清單 15. 從工作內(nèi)存中撤回對(duì)象
...
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 包含兩個(gè)規(guī)則的定義的偽代碼。假設(shè)啟動(dòng)兩個(gè)規(guī)則的條件等于 true 。則應(yīng)該首先啟動(dòng) Rule 1 ,因?yàn)?Rule 1 的顯著值比 Rule 2 的高。現(xiàn)在,注意在 Rule 1 的結(jié)果塊中,objectB 從工作內(nèi)存中撤回(也就是說,objectB 不再是知識(shí)的一部分)。該動(dòng)作更改了規(guī)則引擎的 “執(zhí)行日程”,因?yàn)楝F(xiàn)在將不啟動(dòng) Rule 2 。原因在于曾經(jīng)為真值的用于啟動(dòng) Rule 2 的條件不再為真,因?yàn)樗昧艘粋€(gè)不再是知識(shí)的一部分的對(duì)象(objectB )。如果清單 15 中還有其他規(guī)則引用了 objectB ,且這些規(guī)則尚未啟動(dòng),則它們將不會(huì)再啟動(dòng)了。
作為關(guān)于如何修改工作內(nèi)存中當(dāng)前知識(shí)的具體例子,我將重新編寫前面討論的規(guī)則源文件。業(yè)務(wù)規(guī)則仍然與 “要解決的問題” 小節(jié)中列出的一樣。但是,我將使用這些規(guī)則的不同實(shí)現(xiàn)取得相同的結(jié)果。按照這種方法,任何時(shí)候工作內(nèi)存中惟一可用的知識(shí)是 Machine 實(shí)例。換句話說,規(guī)則的條件元素將只針對(duì) Machine 對(duì)象的屬性執(zhí)行比較。這與之前的方法有所不同,之前的方法還要對(duì) Test 對(duì)象的屬性進(jìn)行比較(參見 清單 12)。 這些規(guī)則的新實(shí)現(xiàn)被捕獲在示例應(yīng)用程序的 testRules2.drl 文件中。清單 16 展示了 testRules2.drl 中與分配測(cè)試相關(guān)的規(guī)則:
清單 16. testRules2.drl 中與分配測(cè)試相關(guān)的規(guī)則
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 中第一個(gè)規(guī)則的定義與 清單 10 中的定義相比較,可以看到,新方法沒有將分配給 Machine 對(duì)象的 Test 實(shí)例插入到工作內(nèi)存中,而是由規(guī)則的結(jié)果塊調(diào)用 update() 方法,讓規(guī)則引擎知道 Machine 對(duì)象已被修改。(Test 實(shí)例被添加/指定給它。) 如果看看清單 16 中其他的規(guī)則,應(yīng)該可以看到,每當(dāng)將測(cè)試分配給一個(gè) Machine 對(duì)象時(shí),都采用這種方法:一個(gè)或多個(gè) Test 實(shí)例被分配給一個(gè) Machine 實(shí)例,然后,修改工作知識(shí),并通知規(guī)則引擎。
還應(yīng)注意清單 16 中使用的 active-lock 屬性。該屬性的值被設(shè)為 true ;如果不是這樣,在執(zhí)行這些規(guī)則時(shí)將陷入無限循環(huán)。將它設(shè)為 true 可以確保當(dāng)一個(gè)規(guī)則更新工作內(nèi)存中的知識(shí)時(shí),最終不會(huì)導(dǎo)致對(duì)規(guī)則重新求值并重新執(zhí)行規(guī)則,也就不會(huì)導(dǎo)致無限循環(huán)。可以將 active-lock 屬性 看作 no-loop 屬性的加強(qiáng)版。 no-loop 屬性確保當(dāng)修改知識(shí)的規(guī)則更新后不會(huì)再被調(diào)用,而 active-lock 屬性則確保在修改知識(shí)以后,文件中的任何規(guī)則(其 active-lock 屬性被設(shè)為 true )不會(huì)重新執(zhí)行。
清單 17 展示了其他規(guī)則有何更改:
清單 17. testRules2.drl 中與分配測(cè)試到期日期有關(guān)的規(guī)則
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
|
這些規(guī)則的條件元素現(xiàn)在檢查一個(gè) Machine 對(duì)象的 tests 集合,以確定它是否包含特定的 Test 實(shí)例。因此,如前所述,按照這種方法,規(guī)則引擎只處理工作內(nèi)存中的一個(gè)對(duì)象(一個(gè) Machine 實(shí)例),而不是多個(gè)對(duì)象(Machine 和 Test 實(shí)例)。
要測(cè)試 testRules2.drl 文件,只需編輯示例應(yīng)用程序提供的 TestsRulesEngine 類(參見 清單 7):將 "testRules1.drl" 字符串改為 "testRules2.drl" ,然后運(yùn)行 TestsRulesEngineTest JUnit 測(cè)試。所有測(cè)試都應(yīng)該成功,就像將 testRules1.drl 作為規(guī)則源一樣。
關(guān)于斷點(diǎn)的注意事項(xiàng)
如前所述,用于 Eclipse 的 Drools 插件允許在規(guī)則文件中設(shè)置斷點(diǎn)。要清楚,只有在調(diào)試作為 “Drools Application” 的程序時(shí),才會(huì)啟用這些斷點(diǎn)。否則,調(diào)試器會(huì)忽略它們。
例如,假設(shè)您想調(diào)試作為 “Drools Application” 的 TestsRulesEngineTest JUnit 測(cè)試類。在 Eclipse 中打開常見的 Debug 對(duì)話框。在這個(gè)對(duì)話框中,應(yīng)該可以看到一個(gè) “Drools Application” 類別。在這個(gè)類別下,創(chuàng)建一個(gè)新的啟動(dòng)配置。在這個(gè)新配置的 Main 選項(xiàng)卡中,應(yīng)該可以看到一個(gè) Project 字段和一個(gè) Main class 字段。對(duì)于 Project 字段,選擇 Drools4Demo 項(xiàng)目。對(duì)于 Main class 字段,輸入 junit.textui.TestRunner (參見圖 3)。
圖 3. TestsRulesEngineTest 類的 Drools application 啟動(dòng)配置(Main 選項(xiàng)卡)
現(xiàn)在選擇 Arguments 選項(xiàng)卡并輸入 -t demo.test.TestsRulesEngineTest 作為程序參數(shù)(參見圖 4)。輸入該參數(shù)后,單擊對(duì)話框右下角的 Apply 按鈕,保存新的啟動(dòng)配置。然后,可以單擊 Debug 按鈕,開始以 “Drools Application” 的形式調(diào)試 TestsRulesEngineTest JUnit 類。如果之前在 testRules1.drl 或 testRules2.drl 中添加了斷點(diǎn),那么當(dāng)使用這個(gè)啟動(dòng)配置時(shí),調(diào)試器應(yīng)該會(huì)在遇到這些斷點(diǎn)時(shí)停下來。
圖 4. TestsRulesEngineTest 類的 Drools Application 啟動(dòng)配置(Arguments 選項(xiàng)卡)
結(jié)束語
使用規(guī)則引擎可以顯著降低實(shí)現(xiàn) Java 應(yīng)用程序中業(yè)務(wù)規(guī)則邏輯的組件的復(fù)雜性。使用規(guī)則引擎以聲明方法表達(dá)規(guī)則的應(yīng)用程序比其他應(yīng)用程序更容易維護(hù)和擴(kuò)展。正如您所看到的,Drools 是一種功能強(qiáng)大的靈活的規(guī)則引擎實(shí)現(xiàn)。使用 Drools 的特性和能力,您應(yīng)該能夠以聲明方式實(shí)現(xiàn)應(yīng)用程序的復(fù)雜業(yè)務(wù)邏輯。Drools 使得學(xué)習(xí)和使用聲明式編程對(duì)于 Java 開發(fā)人員來說相當(dāng)容易。
本文展示的 Drools 類是特定于 Drools 的。如果要在示例程序中使用另一種規(guī)則引擎實(shí)現(xiàn),代碼需要作少許更改。因?yàn)?Drools 是 JSR 94 兼容的,所以可以使用 Java Rule Engine API(如 JSR 94 中所指定)設(shè)計(jì)特定于 Drools 的類的接口。(Java Rule Engine API 用于 JDBC 在數(shù)據(jù)庫(kù)中的規(guī)則引擎。)如果使用該 API,則可以無需更改 Java 代碼而將規(guī)則引擎實(shí)現(xiàn)更改為另一個(gè)不同的實(shí)現(xiàn),只要這個(gè)不同的實(shí)現(xiàn)也是 JSR 94 兼容的。JSR 94 不解析包含業(yè)務(wù)規(guī)則的規(guī)則文件(在本文示例應(yīng)用程序中為 testRules1.drl)的結(jié)構(gòu)。文件的結(jié)構(gòu)將仍取決于您選擇的規(guī)則引擎實(shí)現(xiàn)。作為練習(xí),可以修改示例程序以使它使用 Java Rule Engine API,而不是使用 Java 代碼引用特定于 Drools 的類。
|