首先你要明確的一點,AOP和OOP是兩種不同的認識事物的角度,并不是說有了AOP就不要用OOP。AOP所關注的是傳統(tǒng)OOP不能優(yōu)雅解決的問題。(程序員通常都是完美主義者,當解決某個問題不優(yōu)雅的時候,那就意味著不完美。)下面將就一個簡單的例子來說明他們到底如何的不同。
作為一個使用OOP多年的人來說,當我聽說AOP可以解決一些OOP一直都不能優(yōu)雅地解決的問題時,我覺得應該去探個究竟了。對兩種技術的比較最能給我們實際應用提供見解。這里我設計了一個例子:一個OOP應用,其中某些方面適合使用AOP。
本文展示了一個簡單的例子。一開始介紹了問題域,然后分別給出OOP與AOP的解決方案。后者使用了JDK5.0,JUnit,和AspectWerkz。最后說明如何編寫代碼。讀完本文后,我希望你能知道AOP到底是什么,解決什么樣的問題。(由于作者在后面AOP的例子中使用了Java5。0的批注(Annotation),建議讀者先有所了解)
問題域描述
一個軟件公司雇傭一個程序員,指定給他一個業(yè)務部門并要求他隨時向經(jīng)理報告。當團隊成員完成他們的目標時,經(jīng)理會給他們相應的獎金。公司所需要的方案必須能夠增加一個新的雇員并給當前的員工增加獎金。為了方便,我們用CSV文件存儲數(shù)據(jù)。
圖1 解決方案模型類Manager(經(jīng)理)繼承自類Employee,包含一個額外的屬性,Managing Project。一個部門可能包含很多員工。多個部門構成了公司。暫不考慮公司這樣的一個類,因為它在問題域之外。
解決方案設計
以下流程圖描述了解決方案設計。
圖2 對象之間的交互(增加一個新的員工,指派給他一個部門和經(jīng)理)出于簡單的考慮,本文只關注必需的細節(jié)。當然你也可以深入代碼得到你想要的其他信息。
[link]http://www.devx.com/assets/sourcecode/13172.zip[/link]
EmployeeServiceTestCase,一個JUnit測試用例,模擬一個最終用戶,創(chuàng)建新員工記錄,指派部門和經(jīng)理。它獲取所有可用的部門和經(jīng)理數(shù)據(jù)并顯示在圖形界面上。
為了實例化域?qū)ο驜usinessUnit和Manager,獲得的記錄將傳遞給工廠類。之后,通過給EmployeeService傳遞一個引用來創(chuàng)建一個Employee對象。
這個服務類使用EmployeeFactory創(chuàng)建對象,并把這個對象傳給EmployeeRepository 來進行持久化操作。
應用程序中需要面向哪些"切面"
到目前為止,對模型和設計的討論還限于一個較抽象的層面。現(xiàn)在,我轉(zhuǎn)向這個應用的其他方面 - 這對理解AOP的價值至關重要。
操作所需的資源
public static Set findAllBusinessUnits()
throws RepositoryException {
Set businessUnits = new HashSet();
try {
FileReader businessUnitFile = null;
BufferedReader bufferedBusinessUnitFile = null;
try {
businessUnitFile = new FileReader(FILE_NAME);
bufferedBusinessUnitFile =
new BufferedReader(businessUnitFile);
String businessUnitRecord;
while((businessUnitRecord =
bufferedBusinessUnitFile.readLine()) != null)
{
BusinessUnit businessUnit = BusinessUnitFactory.
createBusinessUnit(businessUnitRecord);
businessUnits。add(businessUnit);
}
} finally
{
if(bufferedBusinessUnitFile != null)
{
bufferedBusinessUnitFile。close();
}
if(businessUnitFile != null)
{
businessUnitFile。close();
}
}
} catch(IOException ioe)
{
String message =
"IOError. Unable to find Business Unit records";
logger.log(SEVERE, message, ioe);
throw new RepositoryException(message, ioe);
}
logger。log(INFO,
"Manager Records returned:"
+ businessUnits.size());
return businessUnits;
} |
上面的代碼通過FileReader和BUfferedReader來讀取CSV文件中的業(yè)務數(shù)據(jù)。應用程序重復地從資源文件中取得數(shù)據(jù)然后在操作完成后釋放。我們會發(fā)現(xiàn):去掉程序的這兩個"切面"將提高代碼的可讀性并達到一個更好的設計,因為去掉這些"多余"的東西,剩下的代碼才是這個方法真正的精髓。
這個方法的作用是讀取業(yè)務單位數(shù)據(jù)。所以不應該也不需要去知道"如何獲取和釋放資源以及這個過程中出現(xiàn)的異常"這個"切面"。同樣地,使用AOP處理異常也變得不同。(后面將詳細介紹)
持久層
傳統(tǒng)的OOP使用倉庫類(repository classes)來打理應用程序的持久層。即:
public class EmployeeRepository
{
public static void createEmployee
(Employee employee)
throws RepositoryException
{
//使用print writer把數(shù)據(jù)放入csv文件
}
public static String
findEmployeeRecordById(String id)
throws RepositoryException
{
//使用file reader來獲得指定id的員工數(shù)據(jù)
}
public static Employee
findEmployeeById(String id)
throws RepositoryException
{
//使用該方法獲取員工數(shù)據(jù),
Employee對象由工廠類創(chuàng)建
}
public static void
updateEmployee(Employee employee)
{
//更新員工數(shù)據(jù)
}
} |
類EmployeeService 使用一個倉庫類給應用中相關雇員提供服務,在一個企業(yè)應用中,從域模型(domain model)中去掉持久層代碼是一種設計上的改進。模型設計者和程序員就可以關注各自的業(yè)務邏輯和持久層處理。后面你將會看到如何通過AOP來達到這樣的效果。
日志
刪除用于調(diào)試的日志代碼將會極大地改進代碼的可讀性。考慮下面的代碼片斷:
public Employee
createEmployee(String name,
String contactNumber,
BusinessUnit businessUnit,
Manager manager)
throws EmployeeServiceException
{
String id = createNewEmployeeId();
Employee employee =
EmployeeFactory。createEmployee
(id, name, contactNumber,
businessUnit, manager);
try {
EmployeeRepository.createEmployee(employee);
} catch(RepositoryException re)
{
String message =
"Created employee successfully:"
+ employee;
logger。log(SEVERE, message);
throw new EmployeeServiceException
(message, re);
}
logger。log(INFO,
"Created employee successfully:"
+ employee);
return employee;
} |
上面的代碼里包含了一個致命錯誤和一個成功信息。輸出日志這一"切面"同樣可以移到業(yè)務模型外獨立實現(xiàn)。
異常處理
異常處理的例子我這里不再贅述,但這節(jié)已經(jīng)通過上面的代碼討論了潛在的問題。當你調(diào)用EmployeeRepository對象的createEmployee方法時,你可能會得到一個RepositoryException異常。
傳統(tǒng)的解決方法是,在這個類中處理。另一種方法是,當RepositoryException異常被拋出時createEmployee 方法返回null,catch塊中的其他邏輯可以在類外處理這一錯誤。
錯誤處理在不同的情況中也會不同。但是,通過AOP可以區(qū)分開每種情況。
圖3圖3中描述了AOP方法的設計以及在一個更抽象的層次上類間的交互。你可以通過對比圖1和圖3來更好地理解AOP。程序的目的是通過BusinessUnit對象讀取CSV文件中的記錄然后填入類BusinessUnitService中的map。
使用AOP來填充這個map有點類似后門(backdoor)方法 -- 控制被委派給BusinessUnit 來讀取存儲介質(zhì)中的記錄。
AOP就是定義一些切入點(pointcut)和處理方法(advice)。一個"切入點"是源代碼中一個執(zhí)行點。前面的例子定義了一個"切入點"--類BusinessUnitService中的findBusinessUnits方法。一個"處理方法"顧名思義就是當執(zhí)行到某個"切入點"時的一塊代碼。
類BusinessUnitPersistentAspect包括advice方法findAllBusinessUnits,該方法從存儲介質(zhì)中載入數(shù)據(jù),然后使用工廠類創(chuàng)建BusinessUnit對象。然后這個對象被加入map,map對象的引用通過BusinessUnitService對象獲得。
"切入點"和"處理方法"組成了所謂的"切面(Aspect)"為了讀取存儲介質(zhì)中的數(shù)據(jù),OOP方法通過一個DAO類來做。而AOP中,你只要定義一個"切入點"和相應的"處理方法"來讀取數(shù)據(jù)。AOP框架會以advice的形式注入代碼,既可以在執(zhí)行期也可以在編譯期。
總而言之,當類BusinessUnitService 中的findAllBusinessUnits 方法被調(diào)用時,AOP框架會在"切入點"處注入處理方法,通過BusinessUnit 對象預先讀取數(shù)據(jù)來填充map對象。這樣,持久層方面的代碼就可以移到業(yè)務代碼之外了。
新方法里的"切面"
本節(jié)討論如何用AOP為應用程序的各個"切面"建模
操作資源
類BusinessUnitPersistenceAspect 的持久方法使用了一個buffered reader。你甚至可以定義"切面"的"切面",但為了簡單,這里只關注類的查找方法。
@Aspect("perJVM")
public class BufferedFileReaderAspect
{
@Expression("execution
(* org.javatechnocrats.aop.withaop.
aspects.BusinessUnitPersistenceAspect.find*(..))")
Pointcut businessUnitPersistenceAspect;
// 其他"切入點"定義
@Expression("businessUnitPersistenceAspect ||
employeePersistenceAspect ||
managerPersistenceAspect")
Pointcut allPersistencePointcuts;
private Map<Class, String> fileNames;
public BufferedFileReaderAspect()
{
System.out.println
("BufferedFileReaderAspect created");
fileNames = new HashMap<Class, String>();
fillFileNames();
}
@Before("allPersistencePointcuts")
public void assignReader
(JoinPoint joinPoint)
throws Throwable
{
System。out。println
("assignReader advice called");
Object callee =
joinPoint.getCallee();
IBufferedFileReaderConsumable
bufReaderConsumable =
(IBufferedFileReaderConsumable)callee;
Class persistenceClass =
callee.getClass();
String fileName =
fileNames.get(persistenceClass);
FileReader fileReader =
new FileReader(fileName);
BufferedReader bufferedReader =
new BufferedReader(fileReader);
bufReaderConsumable.
setBufferedReader(bufferedReader);
}
@AfterFinally("allPersistencePointcuts")
public void releaseReader
(JoinPoint joinPoint) throws Throwable
{
//釋放buffered reader等資源
}
//其他方法
} |
上面的代碼試圖為每一個方法創(chuàng)建"切入點"--所有以find開頭的方法。無論何時這些方法被調(diào)用,assignReader方法都會被提前執(zhí)行。這里它獲取被調(diào)用的類實例然后設置新建的buffered reader。
同樣地,在releaseReader 方法里,代碼會預先關閉buffered reader集合。本節(jié)只解釋@before和@
AfterFinally 這兩個"切入點"。(以J2SE 5。0的標記定義)。另外,你也可以在方面定義的xml文件中聲明他們。你可以查看例程源代碼中的aop。xml文件。
下載
持久化
前面提到,OOP方法使用BusinessUnit來為應用的持久層填充Map。在下面的高亮代碼中(@before一行,以及while循環(huán)代碼),當BusinessUnitService中的方法findAllBusinessUnits 被調(diào)用時"處理方法"findAllBusinessUnits 也將被調(diào)用。
@Aspect("perJVM")
public class
BusinessUnitPersistenceAspect
implements IBufferedFileReaderConsumable
{
private BufferedReader buffFileReader;
@Before("execution
(Collection org.javatechnocrats.aop.withaop.
BusinessUnitService.findAllBusinessUnits())")
public void findAllBusinessUnits
(JoinPoint joinPoint)
throws Throwable
{
System.out.println
("findAllBusinessUnits advice called");
Map<String, BusinessUnit> businessUnits =
((BusinessUnitService)joinPoint.
getThis()).getBusinessUnits();
String businessUnitRecord;
while((businessUnitRecord =
buffFileReader。readLine()) != null)
{
BusinessUnit businessUnit = BusinessUnitFactory.
createBusinessUnit(businessUnitRecord);
businessUnits.put
(businessUnit.getId(), businessUnit);
}
}
public void setBufferedReader
(BufferedReader buffFileReader)
{
System.out.println
("BusinessUnitPersistenceAspect.
setBufferedReader called");
this.buffFileReader = buffFileReader;
}
public BufferedReader getBufferedReader()
{
System.out.println
("BusinessUnitPersistenceAspect.
getBufferedReader called");
return this.buffFileReader;
}
} |
"處理方法"從數(shù)據(jù)存儲中讀取記錄,使用工廠類創(chuàng)建一個BusinessUnit實例。然后這個實例被加入到Map。該Map掌管程序的所有持久化"切面"。
日志
本文中的例子沒有包含一個完整的日志AOP解決方案。但是,它為java。lang。Object類的toString方法定義了一個"切入點"來獲取類的調(diào)試信息。因此,域中的類不需要實現(xiàn)toString方法。通常可能你可能需要為每一個類都要實現(xiàn)這個方法。
@Aspect("perJVM")
public class LoggingAspect
{
@Around("execution(
String org.javatechnocrats.aop.
withaop..*.toString())")
public Object toStringAdvice
(JoinPoint joinPoint)
throws Throwable
{
System.out.println
("toStringAdvice called");
String toString =
(String)joinPoint.proceed();
Object target = joinPoint.getThis();
Field fields[] =
target.getClass().getDeclaredFields();
List members =
new ArrayList(fields.length + 1);
members.add(toString);
for(Field field : fields)
{
field.setAccessible(true);
Object member = field.get(target);
members.add(field.getName()
+ "=" + member);
}
return members。toString();
} |
你也可以用這個樣例代碼完成錯誤處理"切面"。
深入源代碼
為了理解樣例需求的OOP設計,請參看源代碼并思考以下幾個問題: 下載
* 首先分析oldway包中EmployeeServiceTestCase 類中的代碼
*查看testEmployeeCredit 方法
*搞懂業(yè)務類Employee和BusinessUnit
*學習 service,repository和factory概念。這些是業(yè)務驅(qū)動設計的主要概念。
*更深入地理解oldway包中的service,repository和factory類
而AOP地理解則應該是:
*分析newway包中EmployeeServiceTestCase 類
*查看service,repository和factory類,基本和前一種差不多。只是你要讓"處理方法"截獲程序的執(zhí)行流程。
*研究aspect類學習"切入點"的定義
要執(zhí)行程序,你需要做的工作:
* 下載AspectWerkz 2。0 http://aspectwerkz。codehaus。org/
*設置以下的環(huán)境變量:
set JAVA_HOME=c:\Program Files\Java\jdk1.5.0
set ASPECTWERKZ_HOME=C:\aw_2_0_2
set PATH=%PATH%;%ASPECTWERKZ_HOME%\bin
set CLASSPATH=
C:\aw_2_0_2\lib\aspectwerkz-2.0.RC2.jar;
C:\aw_2_0_2\lib\aspectwerkz-jdk5-2.0.RC2.jar;
classes;
C:\ junit\3.8.1\resources\lib\junit.jar |
*解壓縮源代碼和其他文件
*編譯Java文件,但不要編譯測試用例否則你調(diào)試時會遇到一個錯誤。
*進行離線調(diào)試。假設你把文件解壓縮到c:\aop,類文件解壓到c:\aop\classes,在c:\aop目錄下執(zhí)行以下命令:
%ASPECTWERKZ_HOME%\bin\aspectwerkz
-offline etc/aop。xml -cp classes classes |
*AOP框架會修改類來注入必要的字節(jié)碼
*編譯測試用例,使用JUnit運行它。
后記
當你完成了上面的這些工作,你應該有以下的領悟:
*程序中的交叉關聯(lián)
*關于AOP中深入源代碼
為了理解樣例需求的OOP設計,請參看源代碼并思考以下幾個問題:
下載
* 首先分析oldway包中EmployeeServiceTestCase 類中的代碼
*查看testEmployeeCredit 方法
*搞懂業(yè)務類Employee和BusinessUnit
*學習 service,repository和factory概念。這些是業(yè)務驅(qū)動設計的主要概念。
*更深入地理解oldway包中的service,repository和factory類
而AOP地理解則應該是:
*分析newway包中EmployeeServiceTestCase 類
*查看service,repository和factory類,基本和前一種差不多。只是你要讓advice截取程序的流程。
*研究aspect類學習point cut的定義
要執(zhí)行程序,你需要做的工作:
* 下載AspectWerkz 2.0 http://aspectwerkz。codehaus。org/
*設置以下的環(huán)境變量:
set JAVA_HOME=c:\Program Files\Java\jdk1.5.0
set ASPECTWERKZ_HOME=C:\aw_2_0_2
set PATH=%PATH%;
%ASPECTWERKZ_HOME%\bin
set CLASSPATH=
C:\aw_2_0_2\lib\aspectwerkz-2.0.RC2.jar;
C:\aw_2_0_2\lib\aspectwerkz-jdk5-2.0.RC2.jar;
classes;
C:\ junit\3.8.1\resources\lib\junit.jar |
*解壓縮源代碼和其他文件
*編譯Java文件,但不要編譯測試用例否則你調(diào)試時會遇到一個錯誤。
*進行離線調(diào)試。假設你把文件解壓縮到c:\aop ,類文件解壓到c:\aop\classes,在c:\aop目錄下執(zhí)行以下命令:
%ASPECTWERKZ_HOME%\bin\aspectwerkz
-offline etc/aop。xml -cp classes classes |
*AOP框架會修改類來注入必要的字節(jié)碼
*編譯測試用例,使用JUnit運行它。
后記
當你完成了上面的這些工作,你應該有以下的領悟:
*程序中的交叉關聯(lián)
*關于AOP中"切面"的含義
*如何用AOP來把程序業(yè)務層中的交叉關聯(lián)分離出來,使用"切入點"和"處理方法"
*OOP和AOP時在程序控制流上的不同
從本文你應該也得到一種看待實際開發(fā)的新視角。你應該有信心使用AOP來改進項目中的設計,建模,提高代碼的重用性。至少,你可以開始使用AOP來處理日志,錯誤和持久化。
個人覺得,AOP的學習曲線相對較陡,尤其在理解定義"切入點"的句法時。理想的情況是,使用OOP來設計業(yè)務模型,使用AOP把業(yè)務模型中的交叉關聯(lián)移出,從而使代碼簡潔并提高可讀性。
AOP的一個缺點是會使調(diào)試變得困難,因為不同于OOP,程序流變的復雜了,交互是由編譯期或執(zhí)行期決定。我準備將來做一些自動化工具來解決這個問題。
posted on 2005-11-04 13:57
Sung 閱讀(307)
評論(0) 編輯 收藏 所屬分類:
Thinking in Design