http://www.dsc.com.tw/newspaper/43/43-2.htm
前言
物件導(dǎo)向程式設(shè)計(jì)(以下簡稱:OOP)經(jīng)過一段時(shí)間的演進(jìn)與發(fā)展,目前已漸漸成為設(shè)計(jì)系統(tǒng)的主流,由於物件(object)的概念與實(shí)際世界的個(gè)體非常相似,因此用這種方式來進(jìn)行系統(tǒng)的設(shè)計(jì),可以很真實(shí)的模擬實(shí)際個(gè)體的動(dòng)作,縮短系統(tǒng)與真實(shí)世界的距離。但是,以O(shè)OP設(shè)計(jì)系統(tǒng),有時(shí)候還是會遇到一些困擾,以下面的類別(class)為例:
public class SomeBusinessClass {
//Core data members //此class的核心資料
//Other data members: Log stream, data-consistency //其他Class的資料
public void performSomeOperation(OperationInformation) {
//Ensure authentication//進(jìn)行身分驗(yàn)證
//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 ==== //處理核心作業(yè)
//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”部分所宣告的物件,基本上並不屬於這個(gè)類別主要的關(guān)心事項(xiàng)(concern),雖然還是必須要用到它。
2. “performSomeOperation()”這個(gè)method除了處理自己的核心作業(yè)外,還必須負(fù)責(zé)呼叫身分驗(yàn)證、鎖定欲處理的資料、記錄日誌等額外的工作,這些工作似乎與他的責(zé)任不太相關(guān),但又找不出必須由誰來做這些事情。
3. 如果save()及l(fā)oad()兩個(gè)method也是這個(gè)類別的核心程式的話,我們很容易就忽略了這層關(guān)係,使注意力集中到別的地方了。
基於這一類的問題,我們可以看出,OOP雖然可以很適當(dāng)?shù)谋憩F(xiàn)出系統(tǒng)的模組化,但當(dāng)遇到需要跨越模組(或類別)的應(yīng)用時(shí)(這些模組(或類別)可能與主要作業(yè)邏輯是沒有什麼關(guān)係的,如身分驗(yàn)證、日誌記錄等),OOP就無法以較自然的方式來表現(xiàn)或處理。這種被稱為橫切關(guān)係(crosscutting)的行為,常會造成軟體設(shè)計(jì)或?qū)嵶鲿r(shí)不夠簡潔,不易了解,甚至?xí)斐扇蔗峋S護(hù)上的困難。雖然我們可以利用extend的方式來加以萃取,或引入design pattern來減少這種情形,但由於使用的地方不同,就必須引入新的設(shè)計(jì),不但造成設(shè)計(jì)的困難度增加,也造成日後他人學(xué)習(xí)、維護(hù)上的障礙;圖一 是另一個(gè)例子,紅色字是所謂的橫切關(guān)係的部分,程式的錯(cuò)綜複雜可想而知。為了解決上述橫切關(guān)係所造成的困擾,AOP的概念便被提出。
圖 一
AOP(Aspect Oriented Programming)
AOP主要的概念是萃取互相獨(dú)立的橫切關(guān)係事項(xiàng)而加以模組化,使之可以有效的集中、管理,而不會分散在程式碼的各個(gè)地方。AOP能有這樣的能力,主要是其特殊的實(shí)作架構(gòu),實(shí)現(xiàn)AOP的運(yùn)作原理,首先必須先告訴aspect的實(shí)作者(例如稍後會提到的AspectJ),程式的哪些特殊點(diǎn)為橫切關(guān)係(我們可以稱其為連接點(diǎn)(join point),一般如method被呼叫的點(diǎn)),將被攔截並加入aspect的規(guī)則,這些aspect規(guī)則可能是額外的動(dòng)作或取代原來程式功能等,接著透過aspect實(shí)作者特有的compiler,將相對應(yīng)的aspect程式碼加入之前設(shè)定的連接點(diǎn)中(一般稱為aspect weaver,參考 圖二),然後再加以執(zhí)行,便可以使原來的程式不用在程式碼中呼叫這些特殊橫切關(guān)係,而仍能得到所要的結(jié)果(以log的例子來說,我們不用再呼叫l(wèi)og,但程式執(zhí)行的結(jié)果卻會幫我們產(chǎn)生log)。
圖 二
所以加入了AOP的概念後,我們可以看到 圖 1 的程式可以有這樣的改變(如:圖 三)
圖三
由於AOP在使用上有這種特殊性,當(dāng)我們在使用它的時(shí)候,一般會搭配其他主要的程式設(shè)計(jì)的方法來共同建構(gòu)一個(gè)系統(tǒng),例如,以O(shè)OP來當(dāng)成它的主要底層結(jié)構(gòu),建構(gòu)系統(tǒng)主要的核心作業(yè),然後再用AOP填補(bǔ)OOP不足的特殊橫切關(guān)係。
有了aspect的理論架構(gòu),接著我們來看看怎麼將它實(shí)際的應(yīng)用在我們的另一個(gè)主題“單元測試(Unit Test)”上;我們將使用Xerox的PARC實(shí)驗(yàn)室發(fā)展出來的AspectJ來作為接下來實(shí)作的語言。AspectJ(Java base AOP implementation language)是實(shí)作AOP的語言之一,它擴(kuò)充自Java語言,並與Java語言互相搭配,一同實(shí)現(xiàn)aspect的機(jī)制。
傳統(tǒng)的單元測試(Unit Test)方式
當(dāng)系統(tǒng)開發(fā)時(shí),常常需要對某一個(gè)個(gè)別的物件進(jìn)行單元測試,以求得物件的正確性;圖 4 是一個(gè)簡單的單元測試模型。
當(dāng)我們針對某個(gè)物件進(jìn)行單元測試時(shí),如果這個(gè)被測的物件又呼叫到別的物件(如 圖 四 中LoginView物件用到AccessController物件時(shí)),此時(shí)為了使這個(gè)被呼叫的物件不會影響到我們的被測物件,我們常常就會用一個(gè)假的物件來取代這個(gè)額外物件,這個(gè)假物件就是單元測試中常常被提到的Mock Object(如 圖 5 的例子)。使用Mock Object的好處是我們可以保留唯一一個(gè)真實(shí)的被測物,使其他額外的物件都是模擬的,如此便可以很容易的餵入測試資料或模擬其他如資料庫或網(wǎng)路連線等狀態(tài),來達(dá)到測試的需求。 |
![]() 圖四 |
雖然Mock Object可以解決這些問題,但隨著系統(tǒng)的發(fā)展及變更,Mock Object會跟著越來越多,反而造成Mock Object維護(hù)不易,單元測試的成本愈來愈高;於是,我們必須找出一種方法,既可以不用維護(hù)Mock Object的程式碼,又可以達(dá)到Mock Object的功能與好處。
由於AOP的概念之一是攔截特殊method的呼叫,並取代其內(nèi)容,這樣的行為與Mock Object的功能非常類似,再加上部分單元測試的研究文獻(xiàn),也提到這樣的做法,於是我們認(rèn)為使用AspectJ,應(yīng)該可以解決Mock Object所產(chǎn)生的問題。
圖五
使用Aspect的概念來進(jìn)行單元測試?yán)肁OP的概念,整個(gè)測試架構(gòu)可以修改如 圖 六。
圖六
詳細(xì)的動(dòng)作過程,說明如下:
1. 首先設(shè)定攔截連接點(diǎn)的條件(利用AspectJ的程式來實(shí)作,如 圖 6 MethodInterceptor部分),我們假設(shè)所有被測物件及其相關(guān)的所有物件的method呼叫都是可攔截的連接點(diǎn),而Unit Test class及AspectBasedTest class則不在攔截的範(fàn)圍,因?yàn)閠est case內(nèi)的程式碼,並沒有被攔截取代的必要。
2. 接著提供一個(gè)可以設(shè)定哪些method將被取代的設(shè)定機(jī)制(如 圖六 的AspectBasedTest class);當(dāng)程式執(zhí)行時(shí),很多相關(guān)的method呼叫都會被攔截,我們必須可以設(shè)定哪一些method要用我們的設(shè)定值來取代其執(zhí)行結(jié)果,哪一些method被攔截後不會被取代,而是進(jìn)行正常的執(zhí)行動(dòng)作。這樣,當(dāng)測試程式執(zhí)行時(shí),我們才能掌握所有的測試環(huán)境。
3. 完成了上述的機(jī)制後,我們看一下整個(gè)動(dòng)作的過程;當(dāng)測試程式開始執(zhí)行時(shí),會先設(shè)定要被取代的method call(即連接點(diǎn)),取代後又會傳回哪一些回傳值等,接著便開始執(zhí)行測試的程式碼,當(dāng)它呼叫設(shè)定的method call時(shí),AspectJ的程式便會去判斷這些method call是否被指定要被取代,如果不需要被取代,則程式便會執(zhí)行原來的method,如果這些method call已經(jīng)被設(shè)定必須被取代,則AspectJ便會傳回被指定的回傳值,使被測物件能在執(zhí)行時(shí)取到事先設(shè)定的測試資料,最後,測試完成並顯示測試的結(jié)果。
4. 以下是AspectJ攔截點(diǎn)與設(shè)定模擬method機(jī)制的程式碼及說明:
/** 此class讓使用者可以設(shè)定哪一些method call要在攔截時(shí)被取代以及取代後要傳回什麼要的值,這就好像模擬一個(gè)Mock Object的method call,只是我們不需真的去寫Mock Object
*/
public class ComponentTestCase extends TestCase
{
public ComponentTestCase(String name)
{
super(name);
}
//設(shè)定哪些method call要被當(dāng)成mock的method call以及當(dāng)它被呼叫時(shí),要傳回什麼值。這裡我們主要是使用一個(gè)Hashtable來儲存使用者設(shè)定的回傳值,當(dāng)程式執(zhí)行時(shí)呼叫到這個(gè)method,取代的機(jī)制就會啟動(dòng),於是就會到Hashtable內(nèi)去取得相對應(yīng)的回傳值;如果找不到相對應(yīng)的值,就傳回null,代表該method並未被設(shè)定要被取代
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());
}
//取回之前設(shè)定的回傳值
public static Object getMockReturnValue(String className, String methodName)
{
return testData.get(makeKey(className, methodName));
}
//驗(yàn)證設(shè)定的mock method call是否如預(yù)期的被測試程式使用到
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");
}
//驗(yàn)證使用mock method call的參數(shù)是否如預(yù)期正確輸入的相關(guān)程式
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;
}
//驗(yàn)證設(shè)定的mock method call是否如預(yù)期的被測試程式使用到的相關(guān)程式
public static void indicateCalled(String className, String methodName, Hashtable arguments)
{
callsMade.put(makeKey(className, methodName), arguments);
}
//驗(yàn)證設(shè)定的mock method call是否如預(yù)期的被測試程式使用到的相關(guān)程式
public static boolean isCalled(String className, String methodName)
{
return callsMade.get(makeKey(className, methodName)) != null;
}
//驗(yàn)證使用mock method call的參數(shù)是否如預(yù)期正確輸入
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攔截點(diǎn)設(shè)定程式,主要是設(shè)定哪些method call會被此程式加以取代,也就是說,被設(shè)定了攔截的method call,在別人呼叫時(shí),會先跑到這個(gè)程式來執(zhí)行,再依程式的邏輯決定要執(zhí)行真正的method或用其他值取代
*/
aspect AspectBasedMethodInterceptor
{
pointcut allCalls():execution(* *.*(..)) && !within(ajmock.*); //設(shè)定哪些method call是攔截點(diǎn)
Object around() : allCalls() //攔截點(diǎn)攔截後要做的事(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的回傳設(shè)定值
Object returnValue = ajmock.ComponentTestCase.getMockReturnValue(className, methodName);
//回傳值如果不是空的,表示此method call是使用者設(shè)定的mock method call必須用使用者設(shè)定的回傳值來取代
if (returnValue != null)
{
Hashtable arguments = (Hashtable)getArguments(thisJoinPoint);
//呼叫此method主要是為之後的method call驗(yàn)證之用
ComponentTestCase.indicateCalled(className, methodName, arguments);
return returnValue;
} else {
//如果回傳值是空的,表示此method call不是使用者設(shè)定的mock method call,必須執(zhí)行原來的method,而不被取代
return proceed();
}
}
//取得method call的參數(shù)值以便做事後的驗(yàn)證(驗(yàn)證是否與使用者之前設(shè)定的參數(shù)值相同)
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 { //繼承設(shè)定模擬method機(jī)制的class
public TestLoginView(String s) {
super(s);
}
protected void setUp() {
}
public void testValidateValidUser() {
LoginView view = new LoginView();
Integer mockResult = new Integer(AccessController.USER_INVALID);
//設(shè)定ajusage.AccessController的login method要被取代,取代值是mockResult
setMock("ajusage.AccessController","login", mockResult);
/呼叫被測試method
view.validate();
//驗(yàn)證測試結(jié)果
assertEquals("login successful", view.getStatus());
}
如此便達(dá)到我們的需求,不但可以使用Mock Object的好處,又可以不須維護(hù)額外的程式碼。
總結(jié)
麻省理工學(xué)院在2001一月份出刊的Technology Review雜誌中,特別選出可改變未來世界的10大創(chuàng)新科技(http://www.technologyreview.com/articles /tr10_toc0101.asp),在其中一項(xiàng)”解開糾結(jié)的程式碼”(Untangling Code)中提到,AOP的出現(xiàn)能有效幫助軟體發(fā)展者開發(fā)出容易解讀的程式碼,降低軟體開發(fā)的複雜度,所以將其列為可以改變未來世紀(jì)的科技之一。對AOP所擁有的功能來說,Unit Test祇是其中一小部份的應(yīng)用,它對程式的模組化或重整等,也都可發(fā)揮不小的益處,端看我們對它的活用程度而定。而這篇文章,也不過是一個(gè)開端而已。
1. 參考資料
如果你想更詳細(xì)的了解AOP的內(nèi)容,也可以參考以下資料:
1. AspectJ網(wǎng)站: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網(wǎng)站: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。