引自:http://spring.jactiongroup.net/viewtopic.php?t=67
原文:http://www.javaworld.com/javaworld/jw-01-2002/jw-0118-aspect.html
了解AOP(第一部分)--用面向方面的編程方式分離軟件關注點
摘要
多數軟件系統都包含幾個跨越多個模塊的關注點。用面向對象技術實現這些關注點會使系統難以實現,難以理解,并且不利于軟件的演進。新的AOP(面向角度的編程方法)利用模塊化來分離軟件中橫切多模塊的關注點。使用AOP,你可以建立容易設計,易于理解和維護的系統。此外,AOP可以帶來更高的產出,更好的質量,更好的擴展性,這篇文章是這個系列里三篇文章中的第一章,介紹AOP的概念和它所解決的問題。
作者:Ramnivas Laddad
一個關注點就是一個特定的目的、一塊我們感興趣的的區域。從技術的角度來說,一個典型的軟件系統包含一些核心的關注點和系統級的關注點。舉個例子來說,一個信用卡處理系統的核心關注點是借貸/存入處理,而系統級的關注點則是日志,事務完整性,授權,安全性及性能問題等,許多關注點——我們叫它橫切關注點——會在多個模塊中出現,使用現有的編程方法,橫切關注點會橫越多個模塊,結果是使系統難以設計、理解、實現和演進。
AOP(面向角度的編程方式)能夠比上述方法更好的分離系統關注點,從而提供模塊化的橫切關注點。
在這篇文章里——關于AOP的三篇文章的第一章,我首先會 解釋橫切關注點在一些即使是中等復雜度的軟件系統中也會引起的問題,接著我會介紹AOP的核心概念并演示AOP是怎樣解決橫切關注點問題的。
軟件編程方法的演進
在計算機科學的早期階段,開發人員使用直接的機器級代碼來編程,不幸的是,程序員得花費更多時間來考慮一種特定機器的指令集而不是手中需要解決的問題本身。慢慢的我們轉而使用允許對底層機器做某種抽象的高級語言。然后是結構化語言,我們可以把問題分解成一些必要的過程來完成任務。但是,隨著復雜程度的增加,我們又需要更適合的技術。面向對象的編程方式(OOP)使我們可以把系統看作是一批相互合作的對象。類允許我們把實現細節隱藏在接口下。多態性為相關概念提供公共的行為和接口,并允許特定的組件在無需訪問基礎實現的前提下改變特定行為。
編程方法和語言決定了我們和計算機交流的方式。每一種新的方法學都提出一種新的分解問題的方法:機器碼、偽代碼、過程和類等。每種新的方法學都使得從系統需求到編程概念的映射更加自然。編程方法學的發展讓我們可以建立更加復雜的系統,這句話反過來說也對,我們能夠建立更加復雜的系統是因為這些技術允許我們處理這種復雜度。
現在,大多數軟件項目都選擇OOP的編程方式。確實,OOP已經表明了它處理一般行為的能力,但是,我們一會兒會看到(或許你已經感覺到了),OOP不能很好的處理橫越多個——經常是不相關的——模塊的行為,相比之下,AOP填補了這個空白,它很可能會是編程方法學發展的下一個里程碑。
把系統看作一批關注點
我們可以把一個復雜的系統看作是由多個關注點來組合實現的,一個典型的系統可能會包括幾個方面的關注點,如業務邏輯,性能,數據存儲,日志和調試信息,授權,安全,線程,錯誤檢查等,還有開發過程中的關注點,如易懂,易維護,易追查,易擴展等,圖一演示了由不同模塊實現的一批關注點組成了一個系統。
圖 1. 把模塊作為一批關注點來實現
圖二把需求比作一束穿過三棱鏡的光,我們讓需求之光通過關注點鑒別三棱鏡,就會區別出每個關注點,同樣的方法也適用于開發階段的關注點。
圖 2. 關注點分解: 三棱鏡法則
開發人員建立一個系統以滿足多個需求,我們可以大致的把這些需求分類為核心模塊級需求和系統級需求。很多系統級需求一般來說是相互獨立的,但它們一般都會橫切許多核心模塊。舉個例子來說,一個典型的企業應用包含許多橫切關注點,如驗證,日志,資源池,系統管理,性能及存儲管理等,每一個關注點都牽涉到幾個子系統,如存儲管理關注點會影響到所有的有狀態業務對象。
讓我們來看一個簡單,但是具體的例子,考慮一個封裝了業務邏輯的類的實現框架:
代碼: |
public class SomeBusinessClass extends OtherBusinessClass { // 核心數據成員
// 其它數據成員:日志流,保證數據完整性的標志位等
// 重載基類的方法
public void performSomeOperation(OperationInformation info) { // 安全性驗證
// 檢查傳入數據是否滿足協議
// 鎖定對象以保證當其他線程訪問時的數據完整性
// 檢查緩存中是否為最新信息
// 紀錄操作開始執行時間
// 執行核心操作
// 紀錄操作完成時間
// 給對象解鎖 }
// 一些類似操作
public void save(PersitanceStorage ps) { }
public void load(PersitanceStorage ps) { } } |
在上面的代碼中,我們注意到三個問題,首先,其它數據成員不是這個類的核心關注點,第二,performSomeOperation()的實現做了許多核心操作之外的事,它要處理日志,驗證,線程安全,協議驗證和緩存管理等一些外圍操作,而且這些外圍操作同樣也會應用于其他類,第三,save()和load()執行的持久化操作是否構成這個類的核心清楚的。
橫切關注點的問題
雖然橫切關注點會跨越多個模塊,但當前的技術傾向于使用一維的方法學來處理這種需求,把對應需求的實現強行限制在一維的空間里。這個一維空間就是核心模塊級實現,其他需求的實現被嵌入在這個占統治地位的空間,換句話說,需求空間是一個n維空間,而實現空間是一維空間,這種不匹配導致了糟糕的需求到實現的映射
表現
用當前方法學實現橫切關注點是不好的,它會帶來一些問題,我們可以大致把這些問題分為兩類:
- 代碼混亂:軟件系統中的模塊可能要同時兼顧幾個方面的需要。舉例來說,開發者經常要同時考慮業務邏輯,性能,同步,日志和安全等問題,兼顧各方面的需要導致相應關注點的實現元素同時出現,引起代碼混亂。
- 代碼分散:由于橫切關注點,本來就涉及到多個模塊,相關實現也就得遍布在這些模塊里,如在一個使用了數據庫的系統里,性能問題就會影響所有訪問數據庫的模塊。這導致代碼分散在各處
結果
混亂和分散的代碼會在多個方面影響系統的設計和開發:
- 可讀性差:同時實現幾個關注點模糊了不同關注點的實現,使得關注點與其實現之間的對應關系不明顯。
- 低產出:同時實現幾個關注點把開發人員的注意力從主要的轉移到外圍關注點,導致產能降低。
- 低代碼重用率:由于這種情況下,一個模塊實現多個關注點,其他需要類似功能的系統不能馬上使用該模塊,進一步降低了產能。
- 代碼質量差:混亂的代碼掩蓋了代碼中隱藏的問題。而且,由于同時要處理多個關注點,應該特別注意的關注點得不到應有的關注
- 難以擴展:狹窄的視角和有限的資源總是產生僅注意當前關注點的設計。新的需求導致從新實現。由于實現不是模塊化的,就是說實現牽涉到多個模塊,為了新需求修改子系統可能會帶來數據的不一致,而且還需相當規模測試來保證這些修改不會帶來bug。
當前解決方法
由于多數系統中都包含橫切關注點,自然的已經形成了一些技術來模塊化橫切關注點的實現,這些技術包括:混入類,設計模式和面向特定問題域的解決方式
使用混入類,你可以推遲關注點的最終實現。基本類包含一個混入類的實例,允許系統的其他部分設置這個實例,舉個例子來說,實現業務邏輯的類包含一個混入的logger,系統的其他部分可以設置這個logger已得到合適的日志類型,比如logger可能被設置為使用文件系統或是消息中間件.在這種方式下,雖然日志的具體實現被推遲啦,基本類還是得包含在所有的寫日志的點調用日志操作和控制日志信息的代碼。
行為型設計模式,如Visitor和Template模式,也允許你推遲具體實現。但是也就像混入類一樣,操作的控制——調用visitor或template的邏輯——仍然留給了基本類
面向特定問題域的解決方式,如框架和應用服務器,允許開發者用更模塊化的方式處理某些橫切關注點。比如EJB(Enterprise JavaBean,企業級javabean)架構,可以處理安全,系統管理,性能和容器管理的持久化(container-managed persistence)等橫切關注點。Bean的開發者僅需關心業務邏輯,而部署者僅需關心部署問題,如bean與數據庫的映射。但是大多數情況下,開發者還是要了解存儲結構。這種方式下,你用基于XML的映射關系描述器來實現于數據持久化相關的橫切關注點。
面向特定問題域的解決方式提供了解決特定問題的專門機制,它的缺點是對于每一種這樣的解決方式開發人員都必須重新學習,另外,由于這種方式是特定問題域相關的,屬于特定問題域之外的橫切關注點需要特殊的對待
設計師的兩難局面
好的系統設計師不僅會考慮當前需求,還會考慮到可能會有的需求以避免到處打補丁。這樣就存在一個問題,預知將來是很困難的,如果你漏過了將來可能會有的橫切關注點的需求,你將會需要修改或甚至是重新實現系統的許多部分;從另一個角度來說,太過于關注不一定需要的需求會導致過分設計(overdesigned)的,難以理解的,臃腫的系統。所以系統設計師處在這么一個兩難局面中:怎么設計算是過分設計?應該寧可設計不足還是寧可過分設計?
舉個例子來說,設計師是否應該在系統中包含現在并不需要的日志機制?如果是的話,哪里是應該寫日志的點?日志應該記錄那些信息?相似的例子還有關于性能的優化問題,我們很少能預先知道瓶頸的所在。常用的方法是建立系統,profile它,然后翻新系統以提高性能,這種方式可能會依照profiling修改系統的很多部分,此外,隨著時間的流逝,由于使用方式的變化,可能還會產生新的瓶頸,類庫設計師的任務更困難,因為他很難設想出所有對類庫的使用方式。
總而言之,設計師很難顧及到系統可能需要處理的所有關注點。即使是在已經知道了需求的前提下,某些建立系統時需要的細節也可能不能全部得到。整體設計就面臨著設計不足/過分設計的兩難局面。
AOP基礎
到目前為止的討論說明模塊化橫切關注點是有好處的。研究人員已經嘗試了多種方法來實現這個任務,這些方法有一個共同的主題:分離關注點。AOP是這些方法中的一種,它的目的是清晰的分離關注點來解決以上提到的問題。
AOP,從其本質上講,使你可以用一種松散耦合的方式來實現獨立的關注點,然后,組合這些實現來建立最終系統。用它所建立的系統是使用松散耦合的,模塊化實現的橫切關注點來搭建的。與之對照,用OOP建立的系統則是用松散耦合的模塊化實現的一般關注點來實現的。在AOP終,這些模塊化單元叫方面(aspect),而在OOP中,這些一般關注點的實現單元叫做類。
AOP包括三個清晰的開發步驟:
- 方面分解:分解需求提取出橫切關注點和一般關注點。在這一步里,你把核心模塊級關注點和系統級的橫切關注點分離開來。就前面所提到的信用卡例子來說,你可以分解出三個關注點:核心的信用卡處理,日志和驗證。
- 關注點實現:各自獨立的實現這些關注點,還用上面信用卡的例子,你要實現信用卡處理單元,日志單元和驗證單元。
- 方面的重新組合:在這一步里,方面集成器通過創建一個模塊單元——方面來指定重組的規則。重組過程——也叫織入或結合——則使用這些信息來構建最終系統,還拿信用卡的那個例子,你可以指定(用某種AOP的實現所提供的語言)每個操作的開始和結束需要紀錄,并且每個操作在涉及到業務邏輯之前必須通過驗證。
圖 3. AOP 開發的步驟
AOP與OOP的不同關鍵在于它處理橫切關注點的方式,在AOP中,每個關注點的實現都不知道其它關注點是否會‘關注’它,如信用卡處理模塊并不知道其它的關注點實現正在為它做日志和驗證操作。它展示了一個從OOP轉化來的強大的開發范型。
注意:一個AOP實現可以借助其它編程范型作為它的基礎,從而原封不動的保留其基礎范型的優點。例如,AOP可以選擇OOP作為它的基礎范型,從而把OOP善于處理一般關注點的好處直接帶過來。用這樣一種實現,獨立的一般關注點可以使用OOP技術。這就像過程型語言是許多OOP語言的基礎一樣。
織入舉例
織入器——一個處理器——組裝一個個關注點(這個過程叫做織入)。就是說,它依照提供給它的規則把不同的執行邏輯段混編起來。
為了說明代碼織入,讓我們回到信用卡處理的例子,為了簡單起見,我們只考慮兩個操作:存入和取出,并且我們假設已經有了一個合適的logger.
來看一下下面的信用卡模塊:
代碼: |
public class CreditCardProcessor { public void debit(CreditCard card, Currency amount) throws InvalidCardException, NotEnoughAmountException, CardExpiredException { // 取出邏輯 } public void credit(CreditCard card, Currency amount) throws InvalidCardException { // 存入邏輯
} } |
下面是日志接口
代碼: |
public interface Logger { public void log(String message); } |
所需組合需要如下織入規則,這里用自然語言來表達(本文的后面會提供這些織入規則的程序版本):
[list=a]
- 紀錄每個公共操作的開始
- 紀錄每個公共操作的結束
- 紀錄所有公共方法拋出的異常
織入器就會使用這些織入規則和關注點實現來產生與如下代碼有相同效果的代碼:
代碼: |
public class CreditCardProcessorWithLogging { Logger _logger;
public void debit(CreditCard card, Money amount) throws InvalidCardException, NotEnoughAmountException, CardExpiredException { _logger.log("Starting CreditCardProcessor.credit(CreditCard, Money) " + "Card: " + card + " Amount: " + amount); // 取出邏輯 _logger.log("Completing CreditCardProcessor.credit(CreditCard, Money) " + "Card: " + card + " Amount: " + amount); } public void credit(CreditCard card, Money amount) throws InvalidCardException { System.out.println("Debiting"); _logger.log("Starting CreditCardProcessor.debit(CreditCard, Money) " + "Card: " + card + " Amount: " + amount); // 存入邏輯 _logger.log("Completing CreditCardProcessor.credit(CreditCard, Money) " + "Card: " + card + " Amount: " + amount);
} } |
AOP語言剖析
就像其他編程范型的實現一樣,AOP的實現有兩部分組成:語言規范和實現。語言規范描述了語言的基礎單元和語法。語言實現則按照語言規范來驗證代碼的正確性并把代碼轉成目標機器的可執行形式。這一節,我來解釋一下AOP組成部分。
AOP語言規范
從抽象的角度看來,一種AOP語言要說明下面兩個方面:
- 關注點的實現:把每個需求映射為代碼,然后,編譯器把它翻譯成可執行代碼,由于關注點的實現以指定過程的形式出現,你可以使用傳統語言如C,C++,Java等。
- 織入規則規范:怎樣把獨立實現的關注點組合起來形成最終系統呢?為了這個目的,需要建立一種語言來指定組合不同的實現單元以形成最終系統的規則,這種指定織入規則的語言可以是實現語言的擴展,也可以是一種完全不同的語言。
AOP語言的實現
AOP的編譯器執行兩步操作:
- 組裝關注點。
- 把組裝結果轉成可執行代碼
AOP實現可以用多種方式實現織入,包括源碼到源碼的轉換。它預處理每個方面的源碼產生織入過的源碼,然后把織入過的源碼交給基礎語言的編譯器產生最終可執行代碼。比如,使用這種方式,一個基于Java的AOP實現可以先把不同的方面轉化成Java源代碼,然后讓Java編譯器把它轉化成字節碼。也可以直接在字節碼級別執行織入;畢竟,字節碼本身也是一種源碼。此外,下面的執行系統——Java虛擬機——也可以是方面認知的,基于Java的AOP實現如果使用這種方式的話,虛擬機可以先裝入織入規則,然后對后來裝入的類都應用這種規則,也就是說,它可以執行just-in-time的方面織入。
AOP的好處
AOP可幫助我們解決上面提到的代碼混亂和代碼分散所帶來的問題,它還有一些別的好處:
- 塊化橫切關注點:AOP用最小的耦合處理每個關注點,使得即使是橫切關注點也是模塊化的。這樣的實現產生的系統,其代碼的冗余小。模塊化的實現還使得系統容易理解和維護
- 系統容易擴展:由于方面模塊根本不知道橫切關注點,所以很容易通過建立新的方面加入新的功能,另外,當你往系統中加入新的模塊時,已有的方面自動的橫切進來,使系統的易于擴展
- 設計決定的遲綁定:還記得設計師的兩難局面嗎?使用AOP,設計師可以推遲為將來的需求作決定,因為它可以把這種需求作為獨立的方面很容易的實現。
- 更好的代碼重用性:由于AOP把每個方面實現為獨立的模塊,模塊之間是松散耦合的,舉例來說,你可以用另外一個獨立的日志寫入器方面(替換當前的)把日志寫入數據庫,以滿足不同的日志寫入要求。
總的來說,松散耦合的實現意味著更好的代碼重用性, AOP在使系統實現松散耦合這一點上比OOP做得更好。
AspectJ:一個Java的AOP實現
AspectJ是一個可免費獲得的由施樂公司帕洛阿爾托研究中心(Xerox PARC)開發Java的AOP實現,它是一個多功能的面向方面的Java擴展。它使用Java作為單個關注點的實現語言,并擴展Java以指定織入規則。這些規則是用切入點(pointcuts)、聯結點(join points),通知(advice)和方面(aspect)來說明的。聯結點是定義在程序執行過程之間的點,切入點由用來指定聯結點的語言構造,通知定義了要在切入點上執行的代碼片,而方面則是這些基礎元素的組合。
另外,AspectJ允許以多種方式用方面和類建立新的方面,你可以引入新的數據成員和方法,或是聲明一個新的類來繼承和實現另外的類或接口。
AspectJ的織入器——AspectJ的編譯器——負責把不同的方面組合在一起,由于由AspectJ編譯器建立的最終系統是純Java字節碼,它可以運行在任何符合Java標準的虛擬機上。而且,AspectJ還提供了一些工具如調試器和Java IDE集成等,我將會在本系列的第二、三部分詳細講解這些。
下面是我在上面用自然語言描述的日志方面的織入規則的AspectJ實現,由于我將會在第二部分詳細介紹AspectJ,所以如果你不能透徹的看懂它的話也不必擔心。關鍵是你應該注意到信用卡處理過程本身一點都不知道日志的事。
代碼: |
public aspect LogCreditCardProcessorOperations { Logger logger = new StdoutLogger();
pointcut publicOperation(): execution(public * CreditCardProcessor.*(..));
pointcut publicOperationCardAmountArgs(CreditCard card, Money amount): publicOperation() && args(card, amount);
before(CreditCard card, Money amount): publicOperationCardAmountArgs(card, amount) { logOperation("Starting", thisjoin point.getSignature().toString(), card, amount); }
after(CreditCard card, Money amount) returning: publicOperationCardAmountArgs(card, amount) { logOperation("Completing", thisjoin point.getSignature().toString(), card, amount); }
after (CreditCard card, Money amount) throwing (Exception e): publicOperationCardAmountArgs(card, amount) { logOperation("Exception " + e, thisjoin point.getSignature().toString(), card, amount); }
private void logOperation(String status, String operation, CreditCard card, Money amount) { logger.log(status + " " + operation + " Card: " + card + " Amount: " + amount); } }
|
我需要AOP嗎?
AOP僅僅是解決設計上的缺點嗎?在AOP里,每個關注點的實現的并不知道是否有其它關注點關注它,這是AOP和OOP的主要區別,在AOP里,組合的流向是從橫切關注點到主關注點,而OOP則相反,但是,OOP可以和AOP很好的共存。比如,你可以使用一個混入類來做組合,既可以用AOP實現,也可以用OOP實現,這取決你對AOP的接受程度。在這兩種情況下,實現橫切關注點的混入類實現都無需知道它自己是被用在類中還是被用在方面中。舉個例子來說,你可以把一個日志寫入器接口用作某些類的混入類或是用作一個日志方面。因而,從OOP到AOP是漸進的。
了解AOP
在這篇文章里,你看到了橫切關系帶來的問題,這些問題的當前解決方法,以及這些方法的缺點。你也看到了AOP是怎樣克服這些缺點的。AOP的編程方式試圖模塊化橫切關注點的實現,提供了一個更好更快的軟件開發方式。
如果你的系統中涉及到多個橫切關注點,你可以考慮進一步了解AOP,它的實現,它的好處。AOP很可能會是編程方式的下一個里程碑。請繼續關注本系列的第二、第三部分。
,yanger(y-ge@263.net) Nov 14 ,2003
_________________
HelloSpring