http://www.dsc.com.tw/newspaper/43/43-2.htm
前言
物件導向程式設計(以下簡稱:OOP)經過一段時間的演進與發展,目前已漸漸成為設計系統的主流,由於物件(object)的概念與實際世界的個體非常相似,因此用這種方式來進行系統的設計,可以很真實的模擬實際個體的動作,縮短系統與真實世界的距離。但是,以OOP設計系統,有時候還是會遇到一些困擾,以下面的類別(class)為例:
public class SomeBusinessClass {
//Core data members //此class的核心資料
//Other data members: Log stream, data-consistency //其他Class的資料
public void performSomeOperation(OperationInformation) {
//Ensure authentication//進行身分驗證
//Lock the object to ensure data-consistency in case other threads access it
//Ensure the cache is up to date
//Log the start of operation //記錄日誌
// ==== Perform the core operation ==== //處理核心作業
//Log the completion of operation //記錄日誌
//Unlock the object
}
//More operations similar to above
public void save(PersitanceStorage ps) {
}
public void load(PersitanceStorage ps) {
}
}
即使是這樣簡單的類別,還是有一些地方值得我們注意:
1. “Other data members”部分所宣告的物件,基本上並不屬於這個類別主要的關心事項(concern),雖然還是必須要用到它。
2. “performSomeOperation()”這個method除了處理自己的核心作業外,還必須負責呼叫身分驗證、鎖定欲處理的資料、記錄日誌等額外的工作,這些工作似乎與他的責任不太相關,但又找不出必須由誰來做這些事情。
3. 如果save()及load()兩個method也是這個類別的核心程式的話,我們很容易就忽略了這層關係,使注意力集中到別的地方了。
基於這一類的問題,我們可以看出,OOP雖然可以很適當的表現出系統的模組化,但當遇到需要跨越模組(或類別)的應用時(這些模組(或類別)可能與主要作業邏輯是沒有什麼關係的,如身分驗證、日誌記錄等),OOP就無法以較自然的方式來表現或處理。這種被稱為橫切關係(crosscutting)的行為,常會造成軟體設計或實作時不夠簡潔,不易了解,甚至會造成日後維護上的困難。雖然我們可以利用extend的方式來加以萃取,或引入design pattern來減少這種情形,但由於使用的地方不同,就必須引入新的設計,不但造成設計的困難度增加,也造成日後他人學習、維護上的障礙;圖一 是另一個例子,紅色字是所謂的橫切關係的部分,程式的錯綜複雜可想而知。為了解決上述橫切關係所造成的困擾,AOP的概念便被提出。
圖 一
AOP(Aspect Oriented Programming)
AOP主要的概念是萃取互相獨立的橫切關係事項而加以模組化,使之可以有效的集中、管理,而不會分散在程式碼的各個地方。AOP能有這樣的能力,主要是其特殊的實作架構,實現AOP的運作原理,首先必須先告訴aspect的實作者(例如稍後會提到的AspectJ),程式的哪些特殊點為橫切關係(我們可以稱其為連接點(join point),一般如method被呼叫的點),將被攔截並加入aspect的規則,這些aspect規則可能是額外的動作或取代原來程式功能等,接著透過aspect實作者特有的compiler,將相對應的aspect程式碼加入之前設定的連接點中(一般稱為aspect weaver,參考 圖二),然後再加以執行,便可以使原來的程式不用在程式碼中呼叫這些特殊橫切關係,而仍能得到所要的結果(以log的例子來說,我們不用再呼叫log,但程式執行的結果卻會幫我們產生log)。
圖 二
所以加入了AOP的概念後,我們可以看到 圖 1 的程式可以有這樣的改變(如:圖 三)
圖三
由於AOP在使用上有這種特殊性,當我們在使用它的時候,一般會搭配其他主要的程式設計的方法來共同建構一個系統,例如,以OOP來當成它的主要底層結構,建構系統主要的核心作業,然後再用AOP填補OOP不足的特殊橫切關係。
有了aspect的理論架構,接著我們來看看怎麼將它實際的應用在我們的另一個主題“單元測試(Unit Test)”上;我們將使用Xerox的PARC實驗室發展出來的AspectJ來作為接下來實作的語言。AspectJ(Java base AOP implementation language)是實作AOP的語言之一,它擴充自Java語言,並與Java語言互相搭配,一同實現aspect的機制。
傳統的單元測試(Unit Test)方式
當系統開發時,常常需要對某一個個別的物件進行單元測試,以求得物件的正確性;圖 4 是一個簡單的單元測試模型。
當我們針對某個物件進行單元測試時,如果這個被測的物件又呼叫到別的物件(如 圖 四 中LoginView物件用到AccessController物件時),此時為了使這個被呼叫的物件不會影響到我們的被測物件,我們常常就會用一個假的物件來取代這個額外物件,這個假物件就是單元測試中常常被提到的Mock Object(如 圖 5 的例子)。使用Mock Object的好處是我們可以保留唯一一個真實的被測物,使其他額外的物件都是模擬的,如此便可以很容易的餵入測試資料或模擬其他如資料庫或網路連線等狀態,來達到測試的需求。 |
圖四 |
雖然Mock Object可以解決這些問題,但隨著系統的發展及變更,Mock Object會跟著越來越多,反而造成Mock Object維護不易,單元測試的成本愈來愈高;於是,我們必須找出一種方法,既可以不用維護Mock Object的程式碼,又可以達到Mock Object的功能與好處。
由於AOP的概念之一是攔截特殊method的呼叫,並取代其內容,這樣的行為與Mock Object的功能非常類似,再加上部分單元測試的研究文獻,也提到這樣的做法,於是我們認為使用AspectJ,應該可以解決Mock Object所產生的問題。
圖五
使用Aspect的概念來進行單元測試利用AOP的概念,整個測試架構可以修改如 圖 六。
圖六
詳細的動作過程,說明如下:
1. 首先設定攔截連接點的條件(利用AspectJ的程式來實作,如 圖 6 MethodInterceptor部分),我們假設所有被測物件及其相關的所有物件的method呼叫都是可攔截的連接點,而Unit Test class及AspectBasedTest class則不在攔截的範圍,因為test case內的程式碼,並沒有被攔截取代的必要。
2. 接著提供一個可以設定哪些method將被取代的設定機制(如 圖六 的AspectBasedTest class);當程式執行時,很多相關的method呼叫都會被攔截,我們必須可以設定哪一些method要用我們的設定值來取代其執行結果,哪一些method被攔截後不會被取代,而是進行正常的執行動作。這樣,當測試程式執行時,我們才能掌握所有的測試環境。
3. 完成了上述的機制後,我們看一下整個動作的過程;當測試程式開始執行時,會先設定要被取代的method call(即連接點),取代後又會傳回哪一些回傳值等,接著便開始執行測試的程式碼,當它呼叫設定的method call時,AspectJ的程式便會去判斷這些method call是否被指定要被取代,如果不需要被取代,則程式便會執行原來的method,如果這些method call已經被設定必須被取代,則AspectJ便會傳回被指定的回傳值,使被測物件能在執行時取到事先設定的測試資料,最後,測試完成並顯示測試的結果。
4. 以下是AspectJ攔截點與設定模擬method機制的程式碼及說明:
/** 此class讓使用者可以設定哪一些method call要在攔截時被取代以及取代後要傳回什麼要的值,這就好像模擬一個Mock Object的method call,只是我們不需真的去寫Mock Object
*/
public class ComponentTestCase extends TestCase
{
public ComponentTestCase(String name)
{
super(name);
}
//設定哪些method call要被當成mock的method call以及當它被呼叫時,要傳回什麼值。這裡我們主要是使用一個Hashtable來儲存使用者設定的回傳值,當程式執行時呼叫到這個method,取代的機制就會啟動,於是就會到Hashtable內去取得相對應的回傳值;如果找不到相對應的值,就傳回null,代表該method並未被設定要被取代
public static void setMock(String className, String methodName, Object returnValue)
{
testData.put(makeKey(className, methodName), returnValue);
}
public static void setMock(String className, String methodName)
{
setMock(className, methodName, new Object());
}
//取回之前設定的回傳值
public static Object getMockReturnValue(String className, String methodName)
{
return testData.get(makeKey(className, methodName));
}
//驗證設定的mock method call是否如預期的被測試程式使用到
public void assertCalled(String className, String methodName)
{
if ( ! isCalled(className, methodName) )
fail("The method '" + methodName + "' in class '" +
className + "' was expected to be called but it wasn't");
}
//驗證使用mock method call的參數是否如預期正確輸入的相關程式
public Object getArgument(String className, String methodName, String argumentName)
{
Object argument = null;
Hashtable arguments = (Hashtable)callsMade.get(makeKey(className, methodName));
if (arguments != null)
argument = arguments.get(argumentName);
return argument;
}
//驗證設定的mock method call是否如預期的被測試程式使用到的相關程式
public static void indicateCalled(String className, String methodName, Hashtable arguments)
{
callsMade.put(makeKey(className, methodName), arguments);
}
//驗證設定的mock method call是否如預期的被測試程式使用到的相關程式
public static boolean isCalled(String className, String methodName)
{
return callsMade.get(makeKey(className, methodName)) != null;
}
//驗證使用mock method call的參數是否如預期正確輸入
public void assertArgumentPassed(String className, String methodName,
String argumentName, Object argumentValue)
{
Object argument = getArgument(className, methodName, argumentName);
if (argument == null || !argument.equals(argumentValue))
fail("The argument '" + argumentName + "' of method '" +
methodName + "' in class '" +
className + " ' should have the value '" +
argumentValue + "' but it was '" +
argument + "'!");
}
//組合Hashtable所使用的Key的程式
private static String makeKey(String className, String methodName)
{
return className + "." + methodName;
}
private static Hashtable testData = new Hashtable();
private static Hashtable callsMade = new Hashtable(); }
/**此程式是AspectJ攔截點設定程式,主要是設定哪些method call會被此程式加以取代,也就是說,被設定了攔截的method call,在別人呼叫時,會先跑到這個程式來執行,再依程式的邏輯決定要執行真正的method或用其他值取代
*/
aspect AspectBasedMethodInterceptor
{
pointcut allCalls():execution(* *.*(..)) && !within(ajmock.*); //設定哪些method call是攔截點
Object around() : allCalls() //攔截點攔截後要做的事(around()在AspectJ中是取代攔截的call)
{
String className = thisJoinPoint.getSignature().getDeclaringType().getName();
Object receiver = thisJoinPoint.getThis();
if (receiver != null)
className = receiver.getClass().getName();
String methodName = thisJoinPoint.getSignature().getName();
//嘗試去取得mock method call的回傳設定值
Object returnValue = ajmock.ComponentTestCase.getMockReturnValue(className, methodName);
//回傳值如果不是空的,表示此method call是使用者設定的mock method call必須用使用者設定的回傳值來取代
if (returnValue != null)
{
Hashtable arguments = (Hashtable)getArguments(thisJoinPoint);
//呼叫此method主要是為之後的method call驗證之用
ComponentTestCase.indicateCalled(className, methodName, arguments);
return returnValue;
} else {
//如果回傳值是空的,表示此method call不是使用者設定的mock method call,必須執行原來的method,而不被取代
return proceed();
}
}
//取得method call的參數值以便做事後的驗證(驗證是否與使用者之前設定的參數值相同)
private Hashtable getArguments(JoinPoint jp)
{
Hashtable arguments = new Hashtable();
Object[] argumentValues = jp.getArgs();
String[] argumentNames =
((CodeSignature)jp.getSignature()).getParameterNames();
for (int i = 0; i < argumentValues.length; i++)
{
if (argumentValues[i] != null)
arguments.put(argumentNames[i], argumentValues[i]);
}
return arguments;
}
}
5. 測試程式便可以這樣寫
public class TestLoginView extends ComponentTestCase { //繼承設定模擬method機制的class
public TestLoginView(String s) {
super(s);
}
protected void setUp() {
}
public void testValidateValidUser() {
LoginView view = new LoginView();
Integer mockResult = new Integer(AccessController.USER_INVALID);
//設定ajusage.AccessController的login method要被取代,取代值是mockResult
setMock("ajusage.AccessController","login", mockResult);
/呼叫被測試method
view.validate();
//驗證測試結果
assertEquals("login successful", view.getStatus());
}
如此便達到我們的需求,不但可以使用Mock Object的好處,又可以不須維護額外的程式碼。
總結
麻省理工學院在2001一月份出刊的Technology Review雜誌中,特別選出可改變未來世界的10大創新科技(http://www.technologyreview.com/articles /tr10_toc0101.asp),在其中一項”解開糾結的程式碼”(Untangling Code)中提到,AOP的出現能有效幫助軟體發展者開發出容易解讀的程式碼,降低軟體開發的複雜度,所以將其列為可以改變未來世紀的科技之一。對AOP所擁有的功能來說,Unit Test祇是其中一小部份的應用,它對程式的模組化或重整等,也都可發揮不小的益處,端看我們對它的活用程度而定。而這篇文章,也不過是一個開端而已。
1. 參考資料
如果你想更詳細的了解AOP的內容,也可以參考以下資料:
1. AspectJ網站:http://www.eclipse.org/aspectj/。
2. I want my AOP, Part1, Part2, part3 (By Ramnivas Laddad):
http://www.javaworld.com/javaworld/jw-01-2002/jw-0118-aspect.html,
http://www.javaworld.com/javaworld/jw-03-2002/jw-0301-aspect2.html,
http://www.javaworld.com/javaworld/jw-04-2002/jw-0412-aspect3.html。
3. Junit網站:http://www.junit.org。
4. Virtual Mock Objects using AspectJ with JUNIT,
http://www.xprogramming.com/xpmag/virtualMockObjects.htm。
5. AspectJ for JBuilder,http://aspectj4jbuildr.sourceforge.net/。
6. Mock Object,http://www.mockobjects.com。