天天看點

Drools業務邏輯架構

大部分 web 以及企業級 Java 應用可被分成三部分:與使用者互動的前台,與資料庫這樣的背景系統互動的服務層,以及它們之間的業務邏輯。最近這段時間,通常我們會使用架構來實作前台和背景的需求(例如:Struts, Cocoon, Spring, Hibernate, JDO, 以及實體 Beans),但是卻沒有一種标準手段很好的組織業務邏輯。像 EJB 和 Spring 這樣的架構都以 high level 方式處理,這無助于組織我們的代碼。除非我們改變這種淩亂,否則系統将不會健壯,架構中雜亂的 if...then 語句能帶給我們可配置性、可讀性的優點,以及在其他地方複用代碼的愉悅嗎?本文将介紹如何使用 ​​Drools​​ 規則引擎架構來解決這些問題。

    下列的範例代碼展示了我們正要試圖努力避免的問題。展示了包含一些業務邏輯的 Java 典型應用。

if ((user.isMemberOf(AdministratorGroup)

      && user.isMemberOf(teleworkerGroup))

     || user.isSuperUser(){

         // more checks for specific cases

         if((expenseRequest.code().equals("B203")

           ||(expenseRequest.code().equals("A903")

                        &&(totalExpenses<200)

                &&(bossSignOff> totalExpenses))

           &&(deptBudget.notExceeded)) {

               //issue payments

           } else if {

               //check lots of other conditions

           }

} else {

     // even more business logic

}

    我們經常寫出類似的(甚至更複雜)業務邏輯。當這些用 Java 實作的業務邏輯成為标準方式時,将存在下列問題:

        業務使用者怎樣在這些難以了解的代碼基礎上添加另一個條件(比如"C987")?一旦最初開發這些程式的員工離開了,你想成為維護這些代碼的人嗎?

        我們怎樣檢查規則的正确性?對業務夥伴的技術人員來說都夠複雜的了,更不要說檢查。我們可以有系統的測試這些業務邏輯嗎?

        很用應用都有相似的業務規則--當其中的一個規則改變,我們能保證這一改變可貫穿整個系統?當新應用使用這些規則,該應用已經部分添加了新的規則,但不完全,我們要把邏輯重寫過嗎?

        我們經常需要對每個細小調整所帶來的改變進行重編譯/重部署,而不是堅實的依靠 Java 代碼,業務邏輯是否易于配置?

        可否複用已存在的用其他(腳本)語言編寫的業務規則邏輯?

    J2EE/EJB 以及“IoC inversion of control”架構(比如 Spring,Pico,以及 Avalon)給我們帶來的是 high level 代碼組織能力。在提供良好複用性、可配置性、以及安全性的同時,沒有一個能替代(解決)以上的“spaghetti 代碼”範例出現的問題。理想地,無論選擇何種架構,不僅僅适合 J2EE 應用,而且也可用于“normal”Java(J2SE)程式,以及大部分普遍采用的表現層以及持久層架構。這種理想架構應該允許我們這樣做:

        業務使用者應該可以友善的閱讀和校驗業務邏輯。

        業務規則應該可被複用,并可以通過程式進行配置。

        這種架構應該是可更新的,并在高負載情況下運作。

        Java 程式員可以像使用現有的前台(Struts,Spring)和背景(ORM object-relational mapping)架構一樣友善地使用這個架構。

    另外的問題是,有許多的 web 頁面、資料庫通路組織方式,業務邏輯在這兩種應用中應趨于不同。而架構應該能應付這些并促進代碼複用。理想的架構将能“frameworks all the way down.”,通過這種方式使用架構,我們能在應用中大量的“out of the box”,這樣我們隻為客戶記錄添加值的部分。

規則引擎前來救援

    我們怎樣解決問題呢?一種方案是通過規則引擎擷取 traction。規則引擎是為組織業務邏輯應運而生的架構,它讓開發者專注于做被認為正确的事情上,而不是以 low-level 方式作出決定。

    通常,業務使用者舒适的表達他們知道的正确的事,而不是 if...else 格式的表達方式。你也許能從業務專家聽見這些東西:

    “FORM 10A 用來索取額外 200 歐元費用。(FORM 10A is used for expense claims over 200 Euro.)”

    “我們隻進行數量在 10,000 以上的貿易。”

    “購買大于 €10m 的要經過公司董事準許。”

    通過專注于我們認為正确的事情上,而不是隻知道怎樣用 Java 代碼表達,那麼上面的叙述将比之前的代碼範例更清晰。我們仍然需要一種機制為我們知道和做決定的事實應用這些規則。這種機制就是規則引擎。

Java 中的規則引擎

    ​​JSR 94​​,如同 JBDC 允許我們與多種資料庫互動一樣,javax.rules 是一組與規則引擎互動的通用标準 API。為什麼 JSR-94 沒有詳細說明實際的規則怎樣書寫,有下面大量的 Java 規則引擎可供選擇:

    ​​Jess​​ 或許是最成熟的 Java 規則引擎,有良好的工具支援(包括 Eclipse 插件)以及文檔。但是,它是商業軟體,而且用 Prolog-style 符号書寫規則,對 Java 程式員來說是很晦澀的。

    ​​Jena​​ 是一套開源架構,最初由惠普發起。雖然它有規則引擎以及在 Web 語義方面特别強大,但它并不與 JSR-94 相容。

    ​​Drools​​ 是與 JSR-94 相容的規則引擎,并且在 Apache-style 許可下完全開源。它不僅用熟悉的 Java 和 XML 文法表述規則,而且它還有強大的使用者、開發者社群。在本文中有範例,我們将使用 Drools,因為它有最容易使用的類似 Java 的文法以及完全開發許可。

利用 Drools 開始 Java 開發

    假設有這樣的場景:在閱讀本文的數分鐘後,你老闆要求你做一個股票交易應用原型。這時,業務使用者尚未被完全定義業務邏輯,你馬上會想到最好的辦法是用規則引擎實作。最終系統将可通過内部網通路,而且還要和背景資料庫以及消息系統通訊。在着手行動前,先下載下傳 Drools 架構(與支援庫一起)。在你喜歡的 IDE 中建立新項目,确定所有 .jar 檔案被引用進項目,如圖 1 中所示。截圖是基于 Eclipse 的,不過在其他 IDE 中建立也是相似的。

                   圖 1. 運作 Drools 所需要的庫

    如果我們的股票交易系統很混亂,将失去大量潛在客戶(商機),是以在系統的整個步驟中放入一些模拟器(simulator)是至關重要的。這種模拟器給了你決心采用該系統的信心,甚至規則改變以後所帶來的麻煩。我們将借助靈活工具箱中的工具,以及 JUnit(​​http://www.junit.org/​​) 架構進行模拟。

    如下,我們寫的第一段代碼是 JUnit 測試/模拟器。即使我們無法測試每個對應用有價值的輸入組合,但有測試也比沒有測試的好。在這個範例中,所有的檔案和類(包括單元測試)都放入一個檔案夾/包中,但實際上,你可能會用一種适當的包、檔案夾結構。範例代碼中我們用 Log4j 代替 System.out 調用。

import junit.framework.TestCase;

/*

 * JUnit test for the business rules in the 

 * application.

 * 

 * This also acts a 'simulator' for the business 

 * rules - allowing us to specify the inputs,

 * examine the outputs and see if they match our 

 * expectations before letting the code loose in  

 * the real world.

 */

public class BusinessRuleTest extends TestCase {

  /**

  * Tests the purchase of a stock

  */

  public void testStockBuy() throws Exception{

    //Create a Stock with simulated values

    StockOffer testOffer = new StockOffer();

    testOffer.setStockName("MEGACORP");

    testOffer.setStockPrice(22);

    testOffer.setStockQuantity(1000);

    //Run the rules on it

    BusinessLayer.evaluateStockPurchase(testOffer);

    //Is it what we expected?

    assertTrue(

      testOffer.getRecommendPurchase()!=null);

    assertTrue("YES".equals(

      testOffer.getRecommendPurchase()));               

   }

    這是最基本的 JUnt 測試,我們知道我們的系統應該買所有低于 100 歐元的股票。很明顯,要是沒有資料持有類(StockOffer.java)和業務層類(BusinessLayer.java)它将無法編譯。這兩個類如下。

/**

 * Facade for the Business Logic in our example.

 * In this simple example, all our business logic

 * is contained in this class but in reality it 

 * would delegate to other classes as required.

*/

public class BusinessLayer {

   * Evaluate whether or not it is a good idea

   * to purchase this stock.

   * @param stockToBuy

   * @return true if the recommendation is to buy 

   *   the stock, false if otherwise

   */

  public static void evaluateStockPurchase

    (StockOffer stockToBuy){

                return false;

  }

    StockOffer 是這樣:

 * Simple JavaBean to hold StockOffer values.

 * A 'Stock offer' is an offer (from somebody else)

 * to sell us a Stock (or Company share).

public class StockOffer {

  //constants

  public final static String YES="YES";

  public final static String NO="NO";

  //Internal Variables

  private String stockName =null;

  private int stockPrice=0;

  private int stockQuantity=0;

  private String recommendPurchase = null;

   * @return Returns the stockName.

  public String getStockName() {

        return stockName;

   * @param stockName The stockName to set.

  public void setStockName(String stockName) {

        this.stockName = stockName;

   * @return Returns the stockPrice.

  public int getStockPrice() {

        return stockPrice;

   * @param stockPrice The stockPrice to set.

  public void setStockPrice(int stockPrice) {

        this.stockPrice = stockPrice;

   * @return Returns the stockQuantity.

  public int getStockQuantity() {

        return stockQuantity;

   * @param stockQuantity to set.

  public void setStockQuantity(int stockQuantity){

        this.stockQuantity = stockQuantity;

   * @return Returns the recommendPurchase.

  public String getRecommendPurchase() {

        return recommendPurchase;

    通過 IDE 的 JUnit 插件運作 BusinessRuleTest。如果你不熟悉 JUnit,可在 ​​JUnit​​ 網站找到更多資訊。不必驚訝,如圖 2 所示第二個斷言測試失敗,這是因為還沒把業務邏輯放在适當的地方。測試結果用高亮顯示了模拟器/單元測試所出現的問題,這是很保險的。

                圖 2. JUnit 測試結果

用規則編寫業務邏輯

    在這裡,我們要寫一些業務邏輯,來表達“一旦股票價格低于 100 歐元,就馬上購買。” 要達到這個目的,需調整 BusinessLayer.java:

import java.io.IOException;

import org.drools.DroolsException;

import org.drools.RuleBase;

import org.drools.WorkingMemory;

import org.drools.event.DebugWorkingMemoryEventListener;

import org.drools.io.RuleBaseLoader;

import org.xml.sax.SAXException;

 * @author default

  //Name of the file containing the rules

  private static final String BUSINESS_RULE_FILE=

                              "BusinessRules.drl";

  //Internal handle to rule base

  private static RuleBase businessRules = null;

   * Load the business rules if we have not 

   * already done so.

   * @throws Exception - normally we try to 

   *          recover from these

  private static void loadRules()

                       throws Exception{

    if (businessRules==null){

      businessRules = RuleBaseLoader.loadFromUrl(

          BusinessLayer.class.getResource(

          BUSINESS_RULE_FILE ) );

    }

  }     

   * Evaluate whether or not to purchase stock.

   * @return true if the recommendation is to buy

   * @throws Exception

       (StockOffer stockToBuy) throws Exception{

    //Ensure that the business rules are loaded

    loadRules();

    //Some logging of what is going on

    System.out.println( "FIRE RULES" );

    System.out.println( "----------" );

    //Clear any state from previous runs 

    WorkingMemory workingMemory 

            = businessRules.newWorkingMemory();

    //Small ruleset, OK to add a debug listener 

    workingMemory.addEventListener(

      new DebugWorkingMemoryEventListener());

    //Let the rule engine know about the facts

    workingMemory.assertObject(stockToBuy);

    //Let the rule engine do its stuff!!

    workingMemory.fireAllRules();

    這個類有些重要方法:

    loadRules(),從 BusinessRules.drl 檔案加載規則。

    更新後的 evaluateStockPurchase(),用于評估業務規則。這個方法的注解如下:

        可以反複複用相同的 RuleSet(記憶體中的業務規則是無狀态的)。

        為每次評估構造新的 WorkingMemory,因為我們的知識知道這個時刻是正确的。使用 assertObject() 放置已知事實(作為 Java 對象)到記憶體中。

        Drools 有個事件監聽模式,允許我們“檢視”事件模型中到底發生了什麼。在這裡我們用它列印 debug 資訊。

        working memory 類中的 fireAllRules() 方法評估和更新規則(在本例中是股票出價)。

    再次運作該範例前,需要建立我們的 BusinessRules.drl 檔案:

<?xml version="1.0"?>

<rule-set name="BusinessRulesSample"

  xmlns="http://drools.org/rules"

  xmlns:java="http://drools.org/semantics/java"

  xmlns:xs

    ="http://www.w3.org/2001/XMLSchema-instance"

  xs:schemaLocation

    ="http://drools.org/rules rules.xsd

  http://drools.org/semantics/java java.xsd">

  <!-- Import the Java Objects that we refer 

                          to in our rules -->        

  <java:import>

    java.lang.Object

  </java:import>

    java.lang.String

    net.firstpartners.rp.StockOffer

  <!-- A Java (Utility) function we reference 

    in our rules-->  

  <java:functions>

    public void printStock(

      net.firstpartners.rp.StockOffer stock)

        {

        System.out.println("Name:"

          +stock.getStockName()

          +" Price: "+stock.getStockPrice()     

          +" BUY:"

          +stock.getRecommendPurchase());

        }

  </java:functions>

<rule-set>

  <!-- Ensure stock price is not too high-->      

  <rule name="Stock Price Low Enough">

    <!-- Params to pass to business rule -->

    <parameter identifier="stockOffer">

      <class>StockOffer</class>

    </parameter>

    <!-- Conditions or 'Left Hand Side' 

        (LHS) that must be met for 

         business rule to fire -->

    <!-- note markup -->

    <java:condition>

      stockOffer.getRecommendPurchase() == null

    </java:condition>

      stockOffer.getStockPrice() < 100

    <!-- What happens when the business 

                      rule is activated -->

    <java:consequence>

        stockOffer.setRecommendPurchase(

                              StockOffer.YES);  

          printStock(stockOffer);

    </java:consequence>

  </rule>

</rule-set>

    該規則檔案有些有趣部分:

        隻有在 XML-Schema 定義 Java 對象之後,我們才能引用進規則。這些對象可以是來自于任何必須的 Java 類庫。

        接下來是 functions,它們可以與标準 Java 代碼進行混合。既然這樣,我們幹脆混入些日志功能來幫助我們觀察發生了什麼。

        再下來是我們的 rule set,rule set 由一到多個規則組成。

        每個規則可持有參數(StockOffer 類),并需要實作一個或多個條件,當條件符合時,将會執行相應結果。

    在修改和編譯完代碼後,再次運作 JUnit 測試。這次調用了業務規則,我們的邏輯進行正确地評估,并且測試通過,參看圖 3。恭喜--你已經建構了第一個基于規則的應用!

    圖 3.成功的 JUnit 測試

使規則更聰明

    剛剛建構好應用,你就向業務使用者示範上面的原型,他們卻忽然想起先前并沒有提出的規則。其中一個新規則是當數量是負數時(<0)不能進行股票交易。“沒關系,”你說,接着回到辦公桌上,緊扣已有知識,快速演化你的系統。

    首先要更新模拟器,把以下代碼添加到 BusinessRuleTest.java:

   * Tests the purchase of a stock 

   * makes sure the system will not accept 

   * negative numbers.

  public void testNegativeStockBuy() 

                                throws Exception{

    //Create a Stock with our simulated values

      StockOffer testOffer = new StockOffer();

        testOffer.setStockName("MEGACORP");

        testOffer.setStockPrice(-22);

        testOffer.setStockQuantity(1000);

        //Run the rules on it

        BusinessLayer

              .evaluateStockPurchase(testOffer);

        //Is it what we expected?

        assertTrue("NO".equals(

          testOffer.getRecommendPurchase()));

    這個測試是為業務使用者描述的新規則建立的。正如意料之中的,如果運作 JUnit 測試,我們的新測試将失敗。是以,我們要添加新的規則到 .drl 檔案:

<!-- Ensure that negative prices 

                            are not accepted-->      

  <rule name="Stock Price Not Negative">

    <!-- Parameters we can pass into 

                          the business rule -->

    <!-- Conditions or 'Left Hand Side' (LHS) 

       that must be met for rule to fire -->

      stockOffer.getStockPrice() < 0

    <!-- What happens when the business rule 

                              is activated -->

      stockOffer.setRecommendPurchase(

                                  StockOffer.NO);       

      printStock(stockOffer);

    這個規則的格式和前面的相似,除了 (用于測試負數)以及 用于設定推薦購買為 No 以外。我們再次運作測試,這次通過了。

    這時,如果你習慣于過程化程式設計(像大多數 Java 程式員一樣),你也許要搔頭皮了:在一個檔案中包含兩個獨立的業務規則,而且我們也沒告訴規則引擎哪個更重要。不管怎樣,股票價格(對于 -22)都滿足兩個規則(也就是少于 0 和少于 100)。盡管這樣,我們仍能得到正确結果,即使交換規則順序。這是怎麼做到的呢?

    下面的控制台輸出有助于我們了解到底怎麼回事。我們看見兩個規則都執行了([activationfired] 這行),Recommend Buy 第一次被設定為 Yes 接着又被設定成 No。Drools 怎麼知道執行這些規則的正确順序呢?如果你觀察 Stock Price Low Enough 規則,将發現 recommendPurchase() 其中一個條件為空。通過這點,Drools 規則引擎足以判斷 Stock Price Low Enough 規則應該在 Stock Price Not Negative 規則之前執行。這個過程稱為 conflict resolution。

FIRE RULES

----------

[ConditionTested: rule=Stock Price Not Negative;

  condition=[Condition: stockOffer.getStockPrice()

  < 0]; passed=true; tuple={[]}]

[ActivationCreated: rule=Stock Price Not Negative;

  tuple={[]}]

[ObjectAsserted: handle=[fid:2];

   ​​object=net.firstpartners.rp.StockOffer@16546ef​​]

[ActivationFired: rule=Stock Price Low Enough;

   tuple={[]}]

[ActivationFired: rule=Stock Price Not Negative;

Name:MEGACORP Price: -22 BUY:YES

Name:MEGACORP Price: -22 BUY:NO

    如果你是一名過程化程式員,無論你用怎樣聰明的方式考慮這些,你都不會完全相信。這就是為什麼要進行單元/模拟器測試的原因:進行 "堅固的" JUnit 測試(使用一般 Java 代碼)確定規則引擎所作出的決定是按照我們所想要的路線進行。(不會花費大量金錢在無價值的股票上)同時,規則引擎的強大和伸縮性允許我們快速開發業務邏輯。

    稍後,我們将學習如何用更加精練的解決方案進行沖突處理。

沖突結局方案

    現在業務夥伴被打動了,并且開始考慮進行選擇了。随即他們遇到了個 XYZ 公司股票的問題,那麼我們來實作新規則吧:隻有 XYZ 公司股票低于 10 歐元才可購買。

    像以前一樣,添加測試到模拟器,接着在規則檔案中包含新業務規則。首先在 BusinessRuleTest.java 中添加新方法:

 /**

 * Makes sure the system will buy stocks 

 * of XYZ corp only if it really cheap

public void testXYZStockBuy() throws Exception{

  //Create a Stock with our simulated values

  StockOffer testOfferLow = new StockOffer();

  StockOffer testOfferHigh = new StockOffer();

  testOfferLow.setStockName("XYZ");

  testOfferLow.setStockPrice(9);

  testOfferLow.setStockQuantity(1000);

  testOfferHigh.setStockName("XYZ");

  testOfferHigh.setStockPrice(11);

  testOfferHigh.setStockQuantity(1000);

  //Run the rules on it and test

  BusinessLayer.evaluateStockPurchase(

    testOfferLow);

  assertTrue("YES".equals(

    testOfferLow.getRecommendPurchase()));

    testOfferHigh);

  assertTrue("NO".equals(

    testOfferHigh.getRecommendPurchase()));             

    接下來向 BusinessRules.drl 中添加新 :

  <rule name="XYZCorp" salience="-1">

   <!-- Parameters we pass to rule -->

   <parameter identifier="stockOffer">

     <class>StockOffer</class>

   </parameter>

   <java:condition>

     stockOffer.getStockName().equals("XYZ")

   </java:condition> 

     stockOffer.getRecommendPurchase() == null

   </java:condition>

     stockOffer.getStockPrice() > 10

   <!-- What happens when the business 

                                rule is activated -->

   <java:consequence> 

     stockOffer.setRecommendPurchase(

       StockOffer.NO);  

     printStock(stockOffer);

   </java:consequence>

    注意業務規則檔案,在 rule name 後面,我們把 salience 設定成 -1(到目前為止了解的最低優先級)。大多數規則在系統中是沖突的,這意味着 Drools 必須為規則的執行順序做判斷,假設這些條件都與規則比對。預設的判斷方式是:

    Salience:賦予的值。

    Recency:使用規則的次數。

    Complexity:首先執行有複雜值的特定規則。

    LoadOrder:規則載入的順序。

    如果沒有顯示的在規則中詳細指明,将會發生:

    XYZ 公司規則("當價格高于 10 歐元就不購買 XYZ 的股票")将先執行(Recommend Buy 标志被設定為 No)。

    接着更多的一般規則("購買所有 100 歐元以下的股票")被執行,把 Recommend Buy 标志設定為 yes。

    這會給我們一個不想要的結果。然而,一旦在範例中設定了 saliency 要素,最終的測試和業務規則将像預期的那樣順利運作。

    大多數時間,編寫清晰的規則和設定 saliency 将給 Drools 足夠資訊以選擇合适的順序執行規則,有時我們想改變整個規則沖突處理方式。下面的例子說明了如何改變,告訴規則引擎首先執行最簡單的規則。要注意的是:改變沖突解決方案要小心,它可能從根本上改變規則引擎的行為。

  //Generate our list of conflict resolvers

  ConflictResolver[] conflictResolvers = 

    new ConflictResolver[] {

      SalienceConflictResolver.getInstance(),

      RecencyConflictResolver.getInstance(),

        SimplicityConflictResolver.getInstance(),

        LoadOrderConflictResolver.getInstance()

    };

  //Wrap this up into one composite resolver

  CompositeConflictResolver resolver = 

    new CompositeConflictResolver(

      conflictResolvers);

  //Specify this resolver when we load the rules

  businessRules = RuleBaseLoader.loadFromUrl(

    BusinessLayer.class.getResource( 

      BUSINESS_RULE_FILE),resolver);

    我們的簡單應用由 JUnit 測試驅動,我們不必改變 Drools 處理規則沖突的方式。知道沖突解決方案怎樣運作是很有用的,尤其當你的應用為了迎合更複雜、更苛刻的需求時。

結束   

    本文示範了大部分程式員不得不面對的問題:怎樣安排複雜業務邏輯的順序。我們示範了一個使用 Drools 作為解決方案并引入基于規則程式設計概念的簡單應用,包括了怎樣在運作時處理規則。接着,後續文章使用這些技術并展示了怎樣在企業級 Java 應用中使用。

上一篇: 業務邏輯層