簡介:在開發企業軟件時,Java 代碼經常需要與外部組件交互。不管應用程 序必須與遺留應用程序、外部系統還是第三方庫通信,使用不能控制的組件會引 入非預期結果的風險。IBM 的 IT 專家 Filippo Diotalevi 展示了,面向方面 的 編程 (AOP) 如何通過幫助您在保持代碼的干淨和靈活性的同時,設計和定義 組 件之間的明確契約,從而降低這種風險。
契約式設計(Design by Contract)(DBC) 是面向對象的軟件設計中的一 種 技術,它的目的是保證軟件質量、可靠性和可重用性。DBC 中的關鍵考慮是可以 通過以下做法實現這個目標:
盡可能准確地規定組件之間的通信。
定義通信過程中的相互責任和預期的結果。
這些相互責任稱為 契約,用 斷言檢查應用程序是否滿足契約。簡單地說, 斷 言是插入到程序執行中的特定點的布爾表達式,它必須為真。失敗的斷言通常是 軟件 bug 的症兆,所以必須將它報告給使用者。
在處理外部組件或者庫,並需要保證應用程序傳遞給它們的數據和從它們那 裡 接收的數據是正確的時候,DBC 特別有用。本文將展示一個抽象的基礎設施和一 個示例應用程序,前者使用面向方面的編程(AOP)實現 DBC,後者與外部組件 建 立契約。
斷言和 Java 語言
DBC 識別三種基本的斷言類型:
前置條件: 客戶為了正確調用外部組件而必須滿足的責任。
後置條件: 執行外部組件後的預期結果。
不變量: 在執行了外部組件後維持不變的條件。
Java 語言原來沒有提供對斷言的天然支持。 assert 語句是在版本 1.4 中 加 入的。不過,在日常編碼中使用 DBC 會是一種挑戰。事實上,大多數常用的方 法 ── 在應用程序代碼中直接加入前置和後置斷言 ── 在代碼模塊化和可重用 性 方面有嚴重的缺點。這種方法是 糾纏的代碼 的一個活生生的例子:它混合了業 務邏輯代碼與斷言所需的非功能代碼。這種代碼是不靈活的,因為不能在不改變 應用程序代碼的情況下改變或者刪除斷言。
對這個問題的理想解決方案要滿足四個要求:
透明性: 前置和後置條件代碼不與業務邏輯混合。
可重用性: 解決方案的大多數部件是可重用的。
靈活性: 可以用簡單的方式增加、刪除和修改斷言模塊。
簡單性: 可以用簡單的語法指定斷言。
用 AOP 進行透明的契約式設計
如果目的是分離關注點、透明性和靈活性,那麼面向方面的編程 (AOP) 通常 就是正確的答案。前置條件、後置條件和不變量是 橫切關注點 (crosscutting concern)── 常用於應用程序的各種模塊中常常包含的功能中,在某種程度上 與應用程序代碼相混合。AOP 的目標是讓開發人員可以在單獨的模塊中編寫這些 功能並以靈活和聲明式的方式應用它們。
本文假定您對 AspectJ 中的 AOP 有一般性的了解,並且不准備介紹 AOP。 有 關這個主題的介紹文章清單請參閱 參考資料。
實現基礎設施
滿足我在前面列出的四項條件的解決方案包含三部分,如圖 1 所示:
應用程序代碼(不包含與 DBC 有關的元素)。
契約實現(有前置條件、後置條件和不變量檢查)。
一個作為代碼與契約之間 橋梁的對象,可以將契約應用到代碼中正確的部分 中並有正確的邏輯。
圖 1. 契約式設計的一個模塊化得很好的解決方案
圖 1 所展示的設計保證了高度靈活的解決方案,它使您可以不用改變契約實 現或者應用程序代碼就可以應用或者刪除契約。並且它對於應用程序是完全透明 的。
實現細節
契約是實現了特定接口的 Java 類,“橋梁” 是 AspectJ 方面。這個方面 指 定了應用契約的特定點和應用契約所需要的邏輯,如圖 2 所示。
圖 2. 以操作圖表示的契約式設計邏輯
圖 2 中的邏輯圖對於所有契約都是相同的,所以可以開發一個指定它的公共 抽象方面。圖 3 顯示了這個解決方案的類和方面圖:
圖 3. 契約檢查器系統的基本組件
AbstractContract 方面指定在 圖 2 中的操作圖中聲明的控制邏輯。它在程 序的執行中留下了未表示的(即抽象的)點,(由 ContractManager 接口的實 現 定義的)契約將應用到這裡。 ConcreteContract 方面(擴展 AbstractContract 方面) 負責:
通過 targetPointcut pointcut 指定進行契約檢查的准確位置。
通過 getContractManager() 方法指定負責檢查契約的類。
包含檢查應用程序與外部模塊之間契約的代碼的類是 ContractManager 接口 的一個實現。清單 1 所示的 ContractManager 是一個定義契約檢查類的基本行 為的簡單 Java 接口:
清單 1. ContractManager 接口
public interface ContractManager
{
/**
* Check the preconditions
*/
public void checkPreConditions(Object thisObject, Object[] args)
throws ContractBrokeException;
/**
* Check the postconditions
*/
public void checkPostConditions(Object thisObject, Object returnValue, Object [] args)
throws ContractBrokeException;
/**
* Check the invariants
*/
public void checkInvariants(Object thisObject) throws ContractBrokeException;
}
ContractManager 接口為要檢查的每一種斷言定義了不同的方法。每一個方 法 可以通過 thisObject 參數訪問 Java 對象,該對象調用要保證其契約的函數。 前置條件和後置條件方法可以看到作為函數參數 ( args ) 傳遞的值。只有後 置條件方法可以通過 returnValue 參數接收最終的返回值。通過結合使用這三 種 方法,可以檢查幾乎所有常見的條件。
AbstractContract 方面執行進行契約檢查所需要的控制邏輯。這個邏輯是在 around():targetPointcut() advice 中表述的。清單 2 顯示了 AbstractContract 方面:
清單 2. AbstractContract 方面
public abstract aspect AbstractContract
{
/**
* Define the pointcut to apply the contract checking
* MUST CONTAIN A METHOD CALL
*/
public abstract pointcut targetPointcut();
/**
* Define the ContractManager interface implementor to be used
*/
public abstract ContractManager getContractManager();
/**
* Perform the logic necessary to perform contract checking
*/
Object around(): targetPointcut()
{
ContractManager cManager = getContractManager();
System.out.println("Checking contract using:" + cManager.getClass ().getName());
if (cManager!=null)
{
System.out.println("Performing initial invariants check");
cManager.checkInvariants(thisJoinPoint.getTarget());
}
if (cManager!=null)
{
System.out.println ("Performing pre-conditions check");
cManager.checkPreConditions(thisJoinPoint.getTarget(), thisJoinPoint.getArgs());
}
Object obj = proceed ();
if (cManager!=null)
{
System.out.println("Performing post conditions check");
cManager.checkPostConditions(thisJoinPoint.getTarget(), obj, thisJoinPoint.getArgs());
}
if (cManager!=null)
{
System.out.println("Performing final invariants check");
cManager.checkInvariants(thisJoinPoint.getTarget ());
}
return obj;
}
}
AbstractContract 方面表示兩個抽象方法,在實現具體的契約檢查器方面時 必須實現這兩個方法:
public abstract pointcut targetPointcut() 表示其中必須應用 advice 的 pointcut。pointcut 必須是一個方法調用。
public abstract ContractManager getContractManager() 必須返回實現了 正確的契約檢查的 ContractManager 的一個實例。
一定要注意不變量檢查執行了兩次,是在服務執行之前和之後。這使您可以 檢 查服務的執行有沒有影響一些外部字段的值。
契約的失敗會導致 ContractBrokeException ,這會停止 advice 的執行。
實際的契約檢查
理解了用 AOP 實現契約式設計的必要基礎設施後,就可以讓它工作了。假定 需要查詢一個外部客戶關系管理 (CRM) 系統以獲取客戶的數據。可能像下面 這 樣調用 CRM 系統:
Customer cus = companyCustomerSystem.getCustomer("Pluto");
從開發人員的角度看, getCustomer 函數的實現是不重要的,因為 getCustomer 是一個外部組件。但是檢查它是否返回破壞性的結果是非常重要的 。它與保證應用程序不傳遞錯誤或者無意義的輸入給 CRM 系統同樣重要。可以 通 過開發一個擴展了 AbstractContract 的具體方面解決這兩種意外情況。具體的 方面覆蓋兩個方法:
targetPointcut(),定義應用契約檢查的 pointcut。
getContractManager(),定義負責執行所有檢查的 ContractManager實現。
清單 3 顯示了示例應用程序的具體方面:
清單 3. 具體契約方面
public aspect CcCompanySystem extends AbstractContract
{
public pointcut targetPointcut(): call(Customer CompanySystem.getCustomer(String));
public ContractManager getContractManager()
{
return new CompanySystemContractManager();
}
}
CcCompanySystem方面指定契約檢查器調用的 CompanySystemContractManager將對由 CompanySystem類的 getCustomer方法的 調用所表示的 pointcut 應用。不需要定義契約檢查操作的控制邏輯,因為它繼 承自 清單 2中的前輩 AbstractContract抽象方面。
最後一步是開發一個進行契約檢查的 Java 類。如前所述,這個類必須實現 ContractManager接口。清單 4 顯示了一個示例 CompanySystemContractManager類:
清單 4. 示例應用程序的 ContractManager 實現
public class CompanySystemContractManager implements ContractManager
{
/**
* Check preconditions
*/
public void checkPreConditions(Object thisObject, Object[] args)
throws ContractBrokeException
{
Object arg = args[0];
if (arg == null)
{
throw new ContractBrokeException("PRECONDITION ERROR: " +
" Argument of getCustomer shouldn't be null");
}
}
/**
* Check postconditions
*/
public void checkPostConditions(Object thisObject, Object value, Object[] args)
throws ContractBrokeException
{
if (value == null)
{
throw new ContractBrokeException("POSTCONDITION ERROR: " +
" Return value of getCustomer shouldn't be null");
}
}
/**
* Check invariants
*/
public void checkInvariants(Object thisObject) throws ContractBrokeException
{
//invariants check
}
}
清單 4 中的 CompanySystemContractManager類只檢查參數或者返回值是否 為 nulll,但是可以使其增強為加入特別復雜的檢查。
要注意的重要一點:每一個契約檢查實例化一個 CompanySystemContractManager對象,因此可以通過在第一個不變量檢查期間將 數據存儲到私有字段中,並在執行完 CRM 系統調用後驗證它們有沒有改變,來 檢查不變量。
恭喜!您已經開發了應用程序與 CRM 系統之間的一個簡單的契約。在用 AspectJ 編譯器編譯這個應用程序後,這個契約將應用到對 CompanySystem類的 getCustomer方法的每一次調用上,並檢查應用程序與它之間的交互的一致性。 而且,如果 CompanySystemContractManager足夠一般化,就可以重復使用它, 只需要重新定義 targetPointcut就可以將它用於其他的契約檢查。
這個示例解決方案完全滿足我在本文開始時列出的四項要求:
它是 透明的,因為業務邏輯代碼不包含對契約檢查的引用,前者絕對不知道 後者。
它是 可重用的,因為它依賴於一個簡單的基礎設(一個接口和一個抽象方面 ),並使您可以在多種情況下重復使用一個 ContractManager。
它是 靈活的,因為可以使用 AspectJ 編譯器幫助選擇使用哪些方面,從而 選擇要檢查哪些契約。
它是 簡單的,因為它只由幾個類組成。
結束語
本文描述了在使用 AspectJ 和 AOP 進行 Java 應用程序開發時采用契約式 設計的可能方式。建議的解決方案保證了干淨而靈活的解決方案,因為它使用一 個特別簡單且很好地模塊化的設計,使您可以將契約與業務邏輯分開編寫並聲明 式地應用它們。
本文配套源碼