作為程序開發人員,我們經常需要對一個實際上程序性的系統應用面向對象的方法。商業分析家和管理人員描述這樣的系統時通常不使用類層次和序列圖,而是使用流程圖和工作流圖表。但是不論如何,使用面向對象的方法解決這些問題時會帶來更多的靈活性。面向對象的設計模式提供了有用的結構和行為來描述這種順序的處理,比如模版方法(Template Method)[GoF]和責任鏈(Chain of Responsibility)[GoF]。
Jakarta Commons的子項目Chain將上述兩個模式組合成一個可復用的Java框架用於描述順序的處理流程。這個在Jakarta Commons project社區中開發的框架,已經被廣泛的接受並且使用於許多有趣的應用中,特別的是他被Struts和Shale應用框架作為處理HTTP請求處理的基礎機制。你可以在需要定義和執行一組連續的步驟時使用Commons Chain。
至於經典設計模式,開發者和架構師普遍使用模版方法(Template Method)造型順序處理。模版方法(Template Method)中使用一個抽象的父類定義使用的算法:處理的步驟,具體實現交給子類。當然,父類也可以為算法所使用的方法提供一個缺省實現。
由於模版方法(Template Method)依賴繼承——子類必須繼承定義了算法的父類——因此使用這個模式的軟件表現出緊耦合而且缺少靈活性。又由於實現類添加自己的行為前必須擴展父類,溝每⑷嗽北幌拗朴誒嗖憒沃校佣拗屏順絛蟶杓頻牧榛鈈浴ommons Chain使用配置文件定義算法,在程序運行時解析配置文件,從而很好的解決了這個問題。
現在來看一下Commons Chain是怎樣工作的,我們從一個人造的例子開始:二手車銷售員的商業流程。下面是銷售流程的步驟:
- 得到用戶信息
- 試車
- 談判銷售
- 安排財務
- 結束銷售
現在假設使用模版方法(Template Method)造型這個流程。首先建立一個定義了算法的抽象類:
public abstract class SellVehicleTemplate { public void sellVehicle() { getCustomerInfo(); testDriveVehicle(); negotiateSale(); arrangeFinancing(); closeSale(); } public abstract void getCustomerInfo(); public abstract void testDriveVehicle(); public abstract void negotiateSale(); public abstract void arrangeFinancing(); public abstract void closeSale(); }
現在來看一下怎樣用Commons Chain實現這個流程。首先,下載Commons Chain。你可以直接下載最新的zip或tar文件,也可以從CVS或者SubVersion源碼庫檢出Commons Chain模塊得到最新的代碼。解壓縮打包文件,將commons-chain.jar放入你的classpath中。
使用Commons Chain實現這個商業流程,必須將流程中的每一步寫成一個類,這個類需要有一個public的方法execute()。這和傳統的命令模式(Command pattern)實現相同。下面簡單實現了“得到用戶信息”:
package com.jadecove.chain.sample; import org.apache.commons.chain.Command; import org.apache.commons.chain.Context; public class GetCustomerInfo implements Command { public boolean execute(Context ctx) throws Exception System.out.println("Get customer info"); ctx.put("customerName","George Burdell"); return false; } }
由於只是演示,這個類並沒有做很多工作。這里將用戶名放入了Context對象ctx中。這個Context對象連接了各個命令。暫時先將這個對象想象成根據關鍵字存取值的哈希表。所有后來的命令可以通過它訪問剛才放入的用戶名。TestDriveVehicle,NegotiateSale和 ArrangeFinancing命令的實現只是簡單的打印了將執行什么操作。
CloseSale從Context對象中取出GetCustomerInfo放入的用戶名,並將其打印。
package com.jadecove.chain.sample; import org.apache.commons.chain.Command; import org.apache.commons.chain.Context; public class CloseSale implements Command { public boolean execute(Context ctx) throws Exception { System.out.println("Congratulations " + ctx.get("customerName") +", you bought a new car!"); return false; } }
現在你可以將這個流程定義成一個序列(或者說“命令鏈”)。
package com.jadecove.chain.sample; import org.apache.commons.chain.impl.ChainBase; import org.apache.commons.chain.Command; import org.apache.commons.chain.Context; import org.apache.commons.chain.impl.ContextBase; public class SellVehicleChain extends ChainBase { public SellVehicleChain() { super(); addCommand(new GetCustomerInfo()); addCommand(new TestDriveVehicle()); addCommand(new NegotiateSale()); addCommand(new ArrangeFinancing()); addCommand(new CloseSale()); } public static void main(String[] args) throws Exception { Command process = new SellVehicleChain(); Context ctx = new ContextBase(); process.execute(ctx); } }
運行這個類將會輸出以下結果:
Get customer info
Test drive the vehicle
Negotiate sale
Arrange financing
Congratulations George Burdell, you bought a new car!
在進一步深入之前,讓我們來看一下我們使用了的Commons Chain的類和接口。
Command 類和Chain類的關系就是組合模式(Composite pattern)[GoF]的例子:Chain不僅由多個Command組成,而且自己也是Command。這使你可以非常簡單得將單個命令(Command)替換成由多個命令(Command)組成的鏈(Chain)。這個由Command對象唯一操作定義的方法代表了一個直接的命令:
public boolean execute(Context context);
參數context僅僅是一個存放了名稱-值對的集合。接口Context在這里作為一個標記接口:它擴展了java.util.Map但是沒有添加任何特殊的行為。於此相反,類ContextBase不僅提供了對Map的實現而且增加了一個特性:屬性-域透明。這個特性可以通過使用Map的put和get 方法操作JavaBean的域,當然這些域必須使用標准的getFoo和setFoo方法定義。那些通過JavaBean的“setter”方法設置的值,可以通過對應的域名稱,用Map的get方法得到。同樣,那些用Map的put方法設置的值可以通過JavaBean的“getter”方法得到。
例如,我們可以創建一個專門的context提供顯式的customerName屬性支持。
現在你既可以進行Map的一般屬性存取操作同時也可以使用顯式的JavaBean的訪問和修改域的方法,這兩個將產生同樣的效果。但是首先你需要在運行SellVehicleChain時實例化SellVehiceContext而不是ContextBase。
盡管你不改變GetCustomerInfo中存放用戶名的方法——仍然使用ctx.put("customerName", "George Burdell")——你可以在CloseSale中使用getCustomerName()方法得到用戶名。
public boolean execute(Context ctx) throws Exception { SellVehicleContext myCtx = (SellVehicleContext) ctx; System.out.println("Congratulations " + myCtx.getCustomerName() + ", you bought a new car!"); return false; }
那些依賴類型安全和context的顯式域的命令(Command)可以利用標准的getter和setter方法。當一些新的命令(Command)被添加時,它們可以不用考慮context的具體實現,直接通過Map的get和put操作屬性。不論采用何種機制,ContextBase類都可以保證命令(Command)間可以通過context互操作。
下面這個例子展示了如何使用Commons Chain的API建立並執行一組順序的命令。當然,和現在大多數Java軟件一樣,Commons Chain可以使用XML文件作為配置文件。你可以將“汽車銷售”流程的步驟在XML文件中定義。這個文件有個規范的命名chain- config.xml。
<catalog> <chain name="sell-vehicle"> <command id="GetCustomerInfo" className="com.jadecove.chain.sample.GetCustomerInfo"/> <command id="TestDriveVehicle" className="com.jadecove.chain.sample.TestDriveVehicle"/> <command id="NegotiateSale" className="com.jadecove.chain.sample.NegotiateSale"/> <command id="ArrangeFinancing" className="com.jadecove.chain.sample.ArrangeFinancing"/> <command id="CloseSale" className="com.jadecove.chain.sample.CloseSale"/> </chain> </catalog>
Chain的配置文件可以包含多個鏈定義,這些鏈定義可以集合進不同的編目中。在這個例子中,鏈定義在一個默認的編目中定義。事實上,你可以在這個文件中定義多個名字的編目,每個編目可擁有自己的鏈組。
現在你可以使用Commons Chain提供的類載入編目並得到指定的鏈,而不用像SellVehicleChain中那樣自己在程序中定義一組命令:
package com.jadecove.chain.sample; import org.apache.commons.chain.Catalog; import org.apache.commons.chain.Command; import org.apache.commons.chain.Context; import org.apache.commons.chain.config.ConfigParser; import org.apache.commons.chain.impl.CatalogFactoryBase; public class CatalogLoader { private static final String CONFIG_FILE = "/com/jadecove/chain/sample/chain-config.xml"; private ConfigParser parser; private Catalog catalog; public CatalogLoader() { parser = new ConfigParser(); } public Catalog getCatalog() throws Exception { if (catalog == null) { parser.parse(this.getClass().getResource(CONFIG_FILE)); } catalog = CatalogFactoryBase.getInstance().getCatalog(); return catalog; } public static void main(String[] args) throws Exception { CatalogLoader loader = new CatalogLoader(); Catalog sampleCatalog = loader.getCatalog(); Command command = sampleCatalog.getCommand("sell-vehicle"); Context ctx = new SellVehicleContext(); command.execute(ctx); } }
Chain 使用Commons Digester來讀取和解析配置文件。因此你需要將Commons Digester.jar加入classpath中。我使用了1.6版本並且工作得很好。Digester使用了Commons Collectios(我使用的版本是3.1),Commons Logging(版本1.0.4),Commons BeanUtils(1.7.0),因此你也需要將它們的jar文件加入classpath中。在加入這些jar后,CatalogLoader就可以被編譯和運行,它的輸出和另外兩個測試完全相同。
現在你可以在XML文件中定義鏈,並可以在程序中得到這個鏈(別忘了鏈也是命令),這樣擴展的可能性和程序的靈活性可以說是無限的。假設過程“安排財務”實際上由一個完全分離的商業部門處理。這個部門希望為這種銷售建立自己的工作流程。 Chain提供了嵌套鏈來實現這個要求。因為鏈本身就是命令,因此你可以用指向另一個鏈的引用替換一個單一用途的命令。下面是增加了新流程的鏈的定義:
<catalog name="auto-sales"> <chain name="sell-vehicle"> <command id="GetCustomerInfo" className="com.jadecove.chain.sample.GetCustomerInfo"/> <command id="TestDriveVehicle" className="com.jadecove.chain.sample.TestDriveVehicle"/> <command id="NegotiateSale" className="com.jadecove.chain.sample.NegotiateSale"/> <command className="org.apache.commons.chain.generic.LookupCommand" catalogName="auto-sales" name="arrange-financing" optional="true"/> <command id="CloseSale" className="com.jadecove.chain.sample.CloseSale"/> </chain> <chain name="arrange-financing"> <command id="ArrangeFinancing" className="com.jadecove.chain.sample.ArrangeFinancing"/> </chain> </catalog>
Commons Chain提供了一個常用的命令LookupCommand來查找和執行另一個鏈。屬性optional用於控制當指定的嵌套鏈沒有找到時如何處理。 optional=true時,即使鏈沒找到,處理也會繼續。反之,LookupCommand將拋出 IllegalArgumentException,告知指定的命令未找到。
在下面三種情況下,命令鏈將結束:
- 命令的execute方法返回true
- 運行到了鏈的盡頭
- 命令拋出異常
當鏈完全處理完一個過程后,命令就返回true。這是責任鏈模式(Chain of Responsibility)的基本概念。處理從一個命令傳遞到另一個命令,直到某個命令(Command)處理了這個命令。如果在到達命令序列盡頭時仍沒有處理返回true,也假設鏈已經正常結束。
當有命令拋出錯誤時鏈就會非正常結束。在Commons Chain中,如果有命令拋出錯誤,鏈的執行就會中斷。不論是運行時錯誤(runtime exception)還是應用錯誤(application exception),都會拋出給鏈的調用者。但是許多應用都需要對在命令之外定義的錯誤做明確的處理。Commons Chain提供了Filter接口來滿足這個要求。Filter繼承了Command,添加了一個名為postprocess的方法。
public boolean postprocess(Context context, Exception exception);
只要Filter的execute方法被調用,不論鏈的執行過程中是否拋出錯誤,Commons Chain都將保證Filter的postprocess方法被調用。和servlet的過濾器(filter)相同,Commons Chain的Filter按它們在鏈中的順序依次執行。同樣,Filter的postprocess方法按倒序執行。你可以使用這個特性實現自己的錯誤處理。下面是一個用於處理我們例子中的錯誤的Filter:
package com.jadecove.chain.sample; import org.apache.commons.chain.Context; import org.apache.commons.chain.Filter; public class SellVehicleExceptionHandler implements Filter { public boolean execute(Context context) throws Exception { System.out.println("Filter.execute() called."); return false; } public boolean postprocess(Context context, Exception exception) { if (exception == null) return false; System.out.println("Exception " + exception.getMessage() + " occurred."); return true; } }
Filter在配置文件中的定義就和普通的命令(Command)定義相同:
Filter 的execute方法按定義的序列調用。然而,它的postprocess方法將在鏈執行完畢或拋出錯誤后執行。當一個錯誤被拋出時, postprocess方法處理完后會返回true,表示錯誤處理已經完成。鏈的執行並不會就此結束,但是本質上來說這個錯誤被捕捉而且不會再向外拋出。如果postprocess方法返回false,那錯誤會繼續向外拋出,然后鏈就會非正常結束。
讓我們假設ArrangeFinancing因為用戶信用卡損壞拋出錯誤。SellVehicleExceptionHandler就能捕捉到這個錯誤,程序輸出如下:
Filter.execute() called.
Get customer info
Test drive the vehicle
Negotiate sale
Exception Bad credit occurred.
結合了過濾器(filter)和子鏈技術后,你就可以造型很復雜的工作流程。