簡介:在這篇由兩部分組成的關於組合使用元數據和 AOP 的系列文章的第二 部分中,作者及 AOP 實踐者 Ramnivas Laddad 將推薦一種把元數據視為多維關 注點空間中的簽名的全新方法。他還將介紹有效組合使用元數據與 AOP 的一組准 則,並討論元數據注釋將如何影響面向方面的編程的應用。
在本文的第一部分 中,我介紹了新的 Java 元數據功能,並說明了如何以及 在什麼地方可以用元數據注釋最有效地增強 AOP 的連接點模型。我還概括三種最 重要 AOP 系統對元數據的已有支持,這三種系統是:AspectWerkz、AspectJ 和 JBoss AOP。在第一部分中,還介紹了元數據對 AOP 系統的模塊性的影響,最後 ,還重新構建了一個例子,以說明如何在 AOP 系統中逐步添加元數據注釋。
在本文的第二部分中,我將介紹一種全新的方法,該方法將元數據看成通往多 維簽名空間的一種途徑。這種方法具有分解元素簽名的實際用途,並且在總體上 對設計注釋類型很有用,甚至對於那些不從事 AOP 的開發人員也很有幫助。在本 文的第二部分中,我仍然將重點討論元數據方法在面向對象的編程中的最有效用 法。其中,我將展示一種用 AOP 有效減少使用元數據注釋所帶來的模塊性損失的 用法。在本文的最後,我將提供一組確定什麼時候以及如何最佳利用元數據的准 則。我還會考慮添加元數據對采用 AOP 的影響。
主導簽名的專制
程序元素的簽名,如類型、方法或者由幾個組件組成的字段,這些組件包括元 素名、訪問規范、基本類型、方法參數、異常規則等。並不是每種程序元素都要 使用所有組件,但是不管在什麼情況下,程序元素的名字是最有價值的東西,特 別是當它對外公開時,因為它會通知其使用者該元素的作用。
如果不使用元數據編程,那麼每個程序元素只能使用一個名字。在這種情況下 ,常見的作法是將程序元素按它們的主要或者首要功能命名。例如,請看下面 credit 方法的簽名:
public void credit(float amount);
這個方法名只反映了方法的一個屬性:執行信貸業務的業務邏輯。該元素的所 有其他功能都丟失了。雖然這種簽名可以告訴使用這個方法的客戶該元素的主要 功能,但是它無法通知其他客戶 —— 那些實現橫切關注點的客戶,比如安全和 事件管理客戶。這就是主導簽名的專制,由只使用一個名稱來表示元素的限制所 導致的。
在 AOP 研究文獻中經常會討論到類似的情況:主要分解(decomposition)的 專制,其中主要的關注點(通常是業務邏輯)支配了類的設計,特別是繼承層次 (inheritance hierarchy)。
簽名糾結
確保元素的簽名與其橫切功能匹配的一種方法是修改簽名的名稱,使它反映所 有功能。例如,還需要在某個事務中運行並獲得認證的信貸業務可以具有以下簽 名:
public void transactional_authorized_credit(float amount);
在看到像上面的這樣的名字時,確實能讓您想到方法的橫切事務和認證要求, 但是這會使代碼變得很糟糕,只有很少的開發人員會使用它。事實上,只有在特 殊環境中才會看到這種名字,比如使用特殊的命名約定將方法標識為內部消費。
隨著系統的發展,上面的方法還會帶來一個嚴重的問題。如果程序元素的橫切 特性發生改變,那麼它的名字和它的所有引用者都需要做相應的改變。例如,如 果在上面的例子中,系統的發展使得信貸業務不再需要認證,那麼這個方法名就 需要改為 transactional_credit(),而這個方法的所有調用都要做相應的改變。
這種方法的另一個問題是組合使用來自多關注點的元素的名稱增加了調用者的 識別負擔。例如,上述方法的業務調用者對該方法的其他特性並不感興趣,但是 仍然要受其他關注點的影響。
如果熟悉 AOP,那麼您已經知道代碼糾結 —— 將多關注點代碼混合到一個模 塊中 —— 會導致難於理解和難於維護的代碼。簽名糾結 與此類似。就像在上面 的例子中,當程序元素的名字反映其在多個關注點中的作用時,我們就稱之為糾 結的簽名。
在簽名糾結與意義不明的簽名(忽略程序元素的其他功能,並簡化上面的方法 名,例如, credit())之間選擇時,大多數開發人員會選擇簡單性。不過,真正 需要的是第三種選擇。在元素簽名中加入元數據注釋(metadata annotation)可 以讓您用系統的方式表達非核心功能。
元數據,幫我解脫出來吧!
利用 Java 5.0 中的元數據功能,可以使用有類型的注釋通過編程的方式傳達 非核心功能。例如,出於安全考慮,需要在事務管理中執行並獲得認證的貨款方 法可以加上注釋,如下所示:
@Transactional
@Authorized
public void credit(float amount);
表面上,使用注釋與上述簽名糾結方法名之間的區別好像並不明顯,然而,我 將非核心功能指定從方法名轉移到了注釋中。這種方法的真正好處是在調用者這 方面,方法的業務客戶不需要理解其他關注點。業務客戶也不需要在附加到方法 上的注釋改變時修改其代碼。
除了排除元素簽名的糾結外,還可以使用注釋來表達與代碼的橫切關注相關的 任何數據。在這裡,注釋可以與關注點有關的參數相結合。例如,下面的方法聲 明其事務必須具有 Required 語義,並且選中的權限必須是 “accountModification”。
@Transactional(Required)
@Authorized("accountModification")
public void credit(float amount);
可以看出,對於將橫切元素的名字分解為多個部分,使用注釋很方便。實際上 ,注釋可以在不使名稱交織在一起、或者不增加調用者負擔的情況下表示程序元 素的非核心功能。
元數據作為一個多維簽名
概念上,元數據可以視為額外的維上的值,它使我們可以在多維空間中表示程 序元素的功能。這種理解有助於用系統的方式處理元數據,並使這些數據符合 AOP 的目標。即使對於不使用面向方面方法的開發人員,從多維的角度看待元數 據對設計注釋類型也會提供概念上的幫助。
可以將元數據看成以系統性的方式進行多維簽名的一種促動因子(enabler) :空間中的每一維都代表一個關注點,每一個注釋中的值表示對多維簽名空間的 一個投射。考慮剛討論過的 credit() 方法的簽名。圖 1 顯示在三維簽名空間中 的 credit() 方法。
圖 1. 使用元數據的多維簽名空間
圖 1 中操作的名字實質上是一維簽名空間中的一個值。這個簽名空間中的惟 一的維是業務維,而在這一維中操作的值(也稱為這一維中的投射)是“credit ”。使用 Transactional 注釋、同時將 value 屬性設置為 Required,我就可以 將這個例子發展為兩維簽名空間。值 Required 是這個方法在新增加的事務管理 維中的名字。
與此類似,現在來考慮一下 Authorized 注釋,將 value 屬性設置為 “accountModification”。在這個例子中,增加的注釋加入了另一維,從而成為 三維簽名空間。大多數簽名會將重要的值只放入少數維中。這與將兩維幾何點投 身到三維空間中一樣:這種投射的值在其中一維上將為零。
沒有注釋,元素簽名通常會完全忽略非核心維。這種疏忽的主要原因是沒有合 適的方法表達它們。新的 Java 元數據功能提供了在每一維中表達程序功能的精 確方式。
將簽名空間映射到關注點空間
AOP 系統,如 Hyper/J (現在發展為 Concern Manipulation Environment, 請參閱 參考資料)注重關注點空間的多維視圖,利用元數據,我們同時有了一種 提供多維簽名空間的方法。
在將元數據看成多維簽名這種思路中,簽名的每位消費者只使用與其關注點相 關的投射。因此,在上面的例子中,將由簽名所表示的點投射到業務維會獲得名 稱“credit”。對於業務關注點的實現,重要的就是所執行的操作是 “信貸業務 ”。業務關注點不知道(或者不需要知道)任何其他維中的值。例如,業務關注 點不需要知道事務屬性。
與此類似,當同一操作投射到事務管理維上時,我們就得到值 Required。現 在,從事務管理實現的角度看,這一點在業務維中的值是不重要的 —— 它可以 是 credit、debit 或者任何其他值。您可以猜到,對於認證關注點的實現也是一 樣:該點在業務和事務管理維中的投射無關緊要。
啟用多維接口
延伸多維簽名的概念,注釋可以用來表示多維接口。與只表示核心維的常規一 維接口不同,注釋可以有多個接口 —— 應用程序中的每一個關注點有一個。一 個關注點的實現只要考慮投射到相關維中的接口。就像傳統接口可以很好地服務 於面向對象的世界視圖那樣,支持元數據的接口(方面化的接口)也可以服務於 面向方面的世界視圖。
AOP 實踐者都知道,在第一次介紹 AOP 時,常常要說明一個多維分解,該分 解類似於用元數據所進行的多維分解。因此,元數據概念與 AOP 概念非常匹配。 在通過元數據增強的 AOP 實現中,就像平時做的那樣,可以將核心關注點映射到 類。區別是現在將系統的橫切關注點映射到使用了投射到相關維中的多維接口的 方面。
接口示例
參見清單 1 中的 Account 類這個例子。注意元數據注釋如何使將類投射到多 維接口更順利。
清單 1. 包含已注釋方法的 Account 類
public class Account {
@Transactional(kind=Required)
@WriteOperation
@Authorization(kind="bankModification")
public void credit(float amount) {
... credit operation business logic
}
@Transactional(kind=Required)
@WriteOperation
@Authorization(kind="bankModification")
public void debit(float amount) {
... debit operation business logic
}
@Transactional(kind=None)
@ReadOperation
@Authorization(kind="bankQuery")
public float getBalance() {
... balance query operation business logic
}
...
}
從業務角度看,對這個接口的投射會映射到以下接口:
public class Account {
public void credit(float amount) {
... credit operation business logic
}
public void debit(float amount) {
... debit operation business logic
}
public float getBalance() {
... balance query operation business logic
}
...
}
上面的接口包括了所有成員,但沒有包含注釋。投射同一個接口到事務管理維 會得到以下接口:
public class Account {
@Transactional(kind=Required)
* *.*(..)) {
}
@Transactional(kind=Required)
* *.*(..)) {
}
@Transactional(kind=None)
* *.*(..)) {
}
...
}
在該例中,我使用了 AspectJ 通配符符號來表示方法,沒有考慮名字、類型 、參數等因素。其他維中的投射與此類似。
其他維
像前面介紹的那樣,當業務客戶要使用一個方法時,他只需要理解到業務維的 投射即可。例如,AccountServices 類的 transfer() 方法可以像清單 2 中那樣 實現。
清單 2. Account 類的業務客戶
public class AccountServices {
@Transactional(Required)
public void transfer(Account to, Account from, float amount) {
to.credit(amount);
from.debit(amount);
}
}
除了它所攜帶的注釋(在這裡的討論中可以忽略)之外,這個方法實現沒有什 麼特別的。特別是,業務客戶(transfer() 方法)不知道或者不關心 credit() 或者 debit() 方法的事務維或認證維。
與業務客戶一樣,事務管理方面只關心它自己的維。(注意,清單 3 對本文 第 1 部分的清單 2 中所示的方面進行了重構。)
清單 3. 事務管理方面
public aspect TxMgmt {
public pointcut transactedOps (Transactional tx)
: execution(@Transactional * *.*(..))
&& @this(tx);
Object around(Transactional tx) : transactedOps(tx) {
if(tx.value() == Required) {
...
}
}
}
事務管理方面只使用投射到事務管理方面中的接口。換句話說,不管所考慮的 方法叫 credit() 還是 foo(),這個方面所關心的都是一樣的。
利用元數據描述方面化的接口是一種強大的概念。將元數據考慮為類中的一個 方面化接口,通過這種方法來橫切系統中的一些關注點,這樣,可以將面向對象 世界中的一些最佳實踐應用到 AOP 中。例如,不會將一個方法命名為 creditUsingJDBC(),因為希望只描述方法在業務關注空間的功能,而“JDBC”不 是這個空間的一部分。對於方面化的接口也是一樣:您可能不想使用名為 ReadLock 的注釋類型,這類注釋的用途是描述類型的使用。 ReadOperation 是 更好的類型,它向外界描述了這個接口。
在 AOP 編程中使用元數據將類與方面之間的耦合限制為附加在程序元素上的 元數據。將這個概念延伸到支持元數據的多維接口時,可以看到每一個方面都只 耦合到相關的接口。因此,方面與類之間的耦合與面向對象的編程中類之間的耦 合沒有什麼不同。
正確使用元數據的准則
雖然元數據在與 AOP 共同使用時特別有用,但是會有過度使用的危險。過度 使用注釋會使包含在程序元素的簽名和動態上下文中的原有信息無法得到充分利 用。在代碼中加入元數據還會增加代碼的復雜性。首先,程序元素必須包含元數 據注釋。其次,不正確地在 AOP 中使用元數據會導致 macros on steroids,初 看之下,它非常有用,可以節省大量重復的代碼,但它會使程序變得越來越難以 理解。因此,除了了解組合使用 AOP 與元數據所涉及的機制之外,掌握組合使用 這兩種技術所必需的概念和最佳實踐也很重要。在這一節中,我將提供一組正確 結合元數據與 AOP 的准則。
1. 如果不需要就不要使用它
如果所考慮的程序元素在其本來的簽名和動態上下文中包含捕獲所需的連接點 的足夠信息,那麼就沒有理由附加注釋。加入了注釋並寫下切入點來使用這些注 釋之後,任何沒有 注釋的元素都不能參與關注點的橫切。因此,總是要首先考慮 使用元數據的這些替代方法:
要全局地對待全局關注點:程序元素不應當對關注點(比如在橫切實現中進行 跟蹤和分析)的參與起什麼作用。例如,在需要分析的元素上附加 @Profiled 注 釋沒有什麼意義。所分析的元素應當是在全局范圍內選取的,並且它們的選取取 決於當前分析目標。在這種情況下使用注釋意味著系統分析目標發生改變時要修 改程序元素。在這種情況下,利用基於包和繼承層次結構的簽名模式的全局方面 可以做得更好。
利用命名約定:如果項目使用良好的命名約定,那麼最好在橫切時也使用它們 。遵循好的命名約定本身也很有用。例如,可以使用一個 execution(* *.get* ()) || execution(boolean *.is*()) 切入點捕獲所有 getter 方法的執行,對 所有 setter 使用 execution(void *.set*(*))。注意切入點是如何指定類型和 通配符的 —— getter 沒有使用任何參數,並且以“is”開始的 getter 返回一 個布爾值,而 setter 采用了一個參數並返回 void。在某些情況下,這種命名風 格會捕獲錯誤的元素,比如有一個 settle() 方法,當它返回一個 void 並且采 用某一個參數時(請參閱參考資料中 Sam Pullara 的 blog 對這種情況的介紹) 。
在切入點中排除符合這個表達式、但並不需要的連接點通常會更好一些,特別 是考慮到忘記在上面例子中加入 @Getter 和 @Setter 注釋的時候。
使用標准 API 模式:標准 API 和擴展點的調用都將獲得良好定義,並且可以 用使用了通配符的簽名模式很容易地捕獲這些調用。比如考慮想要捕獲對任意 Swing 組件的所有監聽器的調用時。一個使用類似 call(void JComponent+.add*Listener(EventListener+) 這樣的方法簽名的切入點可能不需 要任何注釋。這種切入點只在極其例外的條件下才會匹配不需要的方法。如果使 用元數據注釋,那麼忘記用注釋標注方法的可能(請參閱 @ListenerManagement )要大於捕獲不需要的方法。
利用組件和框架邊界:組件和框架邊界(如對 Web 服務組件、JDBC API 或者 Hibernate API 的調用或者其擴展點)通常都是經過定義良好的,並且不使用顯 式元數據就可以很容易地通過切入點表達式捕獲它們。下面考慮一個監視遠程調 用的方面實現。可以通過關注類是否實現 Remote 來很方便地推斷遠程方法調用 ,或者用方法簽名的異常部分作為參數,像 Remote+.*(..) throws RemoteException 這樣的內容。在這種情況下,使用元數據不但增加了工作的難 度,而且還可能導致方法丟失。
這裡的核心思想是避免將元數據作為第一選擇的誘惑,而是堅持提取程序元素 的簽名模式,編寫沒有元數據的切入點。有了通配符、繼承層次、包結構、好的 命名約定以及好的切入點組合,在許多情況下,您可以得到可以接受的切入點。 但要正確地應用這些技術,則必須對所使用的 AOP 系統的切入點語言有很好的了 解,而這種學習的努力是值得的。
2. 使用方面繼承
AOP blog 和討論組中一直在繼續的討論表明,許多開發人員對日志和跟蹤例 子感到迷惑。對於這些操作,一般會找到在整個系統中都很穩定的切入點,這意 味著可以對整個系統編寫一個方面。不過,在試圖對其他橫切功能使用同樣的方 法時,它變得不可伸縮。如果可以編寫一個切入點表達式,那麼它會隨著系統的 發展而變得復雜,並且通常不很穩定。這會使新來者認為 AOP 的實現是困難和或 者不完善的。
技巧在於:當不能為整個系統定義一個簽名時,要知道將它分解成小的子系統 ,並為每個子系統定義切入點。有很多將系統分解為幾部分並找出每個部分的好 的簽名模式的可能。前面的准則可以在子系統級別上提供幫助。
使用方面繼承首先要創建一個抽象的方面,它包含實現的大部分(通知、內部 類型聲明等)和幾個抽象切入點(請參閱 清單 4)。然後將系統分解為幾部分, 使得每個子系統都可以有好的切入點表達式。子系統可以像整個系統那麼大,也 可以像類那麼小。這種技術是我在 第 1 部分 討論的 Participant 模式的核心 。Participant 模式可以將簡單的方面轉換為更復雜的方面,如負責事務管理、 線程安全、安全性等方面。
3. 利用為其他目的而存在的注釋
方面可以利用服務於不同目的的元數據注釋。例如,Enterprise JavaBeans (EJB) 3.0 注釋允許您基於與事務、安全性和方法角色編寫切入點(即 remove、 init 等)。Hibernate 注釋允許您基於表、列名和關系信息編寫切入點。而 Eclipse Metadata Framework (EMF) 允許您基於類之間的關系(如復合、聯合、 基數等)編寫切入點。
4. 使用抽象注釋類型
如果在仔細分析要求後,確定使用注釋是最好的選擇,那麼如何使用它們呢? 理想情況是,注釋類型應當表示程序元素的抽象功能,而不是它們預期的消費。 這種考慮和實現避免了下面討論的遺忘損失(loss of obliviousness),因為元 素只是提供有關它們自己的額外信息,沒有意識到那些方面。例如,如果將 @ReadOperation 和 @WriteOperation 看作擴展的簽名,那麼類就可以保持對讀 寫鎖定模式的實現的遺忘。
考慮清單 4 中已實現的基本方面,它遵守了前面討論的使用方面繼承的准則 。(如果需要關於 perthis() 方面的信息,請參閱 參考資料,這裡的重點是定 義切入點。)
清單 4. ReadWriteLockSynchronizationAspect 的基本方面
public abstract aspect ReadWriteLockSynchronizationAspect
perthis(readOperations() || writeOperations()) {
public abstract pointcut readOperations();
public abstract pointcut writeOperations();
private ReadWriteLock lock = new ReentrantReadWriteLock(true);
before() : readOperations() {
lock.readLock().lock();
}
after() : readOperations () {
lock.readLock().unlock();
}
before() : writeOperations() {
lock.writeLock().lock();
}
after() : writeOperations() {
lock.writeLock().unlock();
}
}
清單 5 顯示了銀行系統的 ReadWriteLockSynchronizationAspect 方面的、 元數據驅動的子方面。
清單 5. 並發管理子方面
public aspect BankingReadWriteLockSynchronizationAspect
extends ReadWriteLockSynchronizationAspect {
pointcut lockManagedExecution()
: execution(* banking.Customer.* (..))
|| execution(* banking.Account.*(..));
public pointcut readOperations()
: execution(@ReadOperation * *.* (..))
&& lockManagedExecution();
public pointcut writeOperations()
: execution(@WriteOperation * *.*(..))
&& lockManagedExecution();
}
注意,並不是簡單地在所有 read 和 write 操作外面加上鎖,因為不是所有 類都需要這種並發控制機制。也不應該讓類指出它們的操作是否要用鎖管理,因 為所選擇的類將取決於應用程序對這些類的使用。(這種實現可能要對 Java 標 准庫中新的集合類的線程不安全性負責。)在上面的例子中, lockManagedExecution() 切入點選擇了需要用鎖管理的連接點子集。
抽象注釋類型的能力
有了描述功能的抽象注釋後,就可以用同樣的注釋實現其他關注點。例如,可 以使用 @ReadOperation 和 @WriteOperation 進行髒跟蹤和髒緩沖(dirty tracking and caching)。甚至可以編寫加強的方面來確保讀操作的控制流中不 會調用對同一對象的寫操作!
如果使用 @ReadLock 和 @WriteLock 注釋類型“標記”需要用鎖管理的方法 ,那麼這個類會與鎖管理功能緊密耦合。而且,將同一個注釋用於其他目的很容 易出錯,因為注釋將要額外考慮特定的關注點,而不是考慮如何表示方法的狀態 修改特征。
抽象注釋類型的更多例子
讓我們再討論幾個注釋類型的例子。考慮使用 WaitCursor 作為注釋類型來模 塊化等待游標的管理,這類管理是圍繞具有這種類型的注釋實例的所有方法進行 的。可以省略的代碼非常多(特別是當考慮每個方法所需要的 try/finally 塊時 )。不過,還有使用更能描述功能的注釋類型的多種可能。考慮另一種注釋類型 Timing,它類似於 mean、 variance 和 distribution 這樣的屬性,如下所示:
@Timing(mean=5, variance=0.34, distribution=Gaussian)
可以用幾種方法消費這個注釋:
進行速率單調性分析(Rate Monotonic Analysis,即 RMA,請參閱參考資料 )。
提供運行時間長的操作的反饋(如等待游標)。
當然,這種高度特定 的功能表達只在擁有定性的計時數據時才適合。否則,可以使用定性信息。例如 ,注釋實例可以是 @Timing(value=Long)。這種方案遵守了描述連接點功能的原 則,並提供了下面這些有意義的可能性:
進行選擇,只對那些帶有某種計時功能的方法進行分析。
檢查計時信息的自我一致性,如查找在標記為 Large 值的方法的計算路徑中 出現的、用 ExtraLong 值標記的方法。
考慮另一個例子,在該例中,方法是 用 RetryOnFailure 注釋標記的,這樣一個方面就可以再次嘗試失敗的服務。如 果使用 Idempotent 作為注釋類型(表示對同一操作的多次調用會產生同樣的結 果),那麼不僅可以重試失敗的操作,而且還可以同時執行多個服務,如果需要 這樣的優化的話。
最後,考慮上面的授權和身份驗證的例子。權限屬性可以是所需權限的直接表 達。對這一概念的更好表述是指定對操作進行分類的一個屬性。然後,方面就可 以將分類映射到實際的權限,這些權限在不同的系統之間可以發生改變。例如, 一個系統可能無法從訪問控制的角度區分讀寫操作,而另一些系統則可以對此進 行區分。比這更好的一種方法是使用業務相關的注釋類型,如 AccountModification、CustomerInformationAccess 和 Purchase。包含這些類 型的注釋的程序元素不僅能被授權和身份驗證方面所捕獲,而且還可被其他實現 捕獲,比如隱私策略增強方面。不過,從頭創建這種類型很復雜。一種實用方法 是從一個可以工作的入門 級的抽象開始重新構建這種類型,以獲得更好的應用。
智慧來源於經驗
如果正在實現橫切關注點,並且不能遵循上面的准則,那麼也可以使用面向方 面的解決方案,即使這意味著“標記”每一個需要一個通知的方法。使用帶標記 的 AOP 至少實現了某種模塊化,並使您具有獲得某種程度的修改實現的自由度。
標記方法要求使用元數據驅動的切入點編寫方面,然後用在切入點中使用的注 釋類型來注釋程序元素。這種用法類似於使用美化了的宏功能。雖然通過注釋類 型,代碼使核心元素與方面之間有了強烈的依存關系,但是這樣可以節省很多代 碼(通過將共同代碼轉移到一個方面或者通知中,而不是將它放到每一個方法中 )。所得到的模塊性還有助於限制對方面自身實現的關注點的修改。
智慧來源於經驗。即使從標記方法開始,您也很快就會了解更深入的內容,並 相應地對實現進行修改(通常是重新構建)。還記得您用 OOP 最初做的幾個設計 嗎?利用您現在掌握的知識,重新評價最初的 OOP 設計,您可能會發現它們沒有 達到應有的抽象程度,盡管這種設計仍然好於純粹的過程式方法。與此類似,在 AOP 中第一次使用元數據可能會有一個從 OOP 到 AOP 的類似的短暫過程。
元數據和遺忘
如果不考慮元數據對遺忘的影響,對元數據和 AOP 的討論就不能說是完整的 ,盡管在實踐中真正重要的是關注點的模塊性而不是遺忘。在 AOP 上下文中, 遺忘屬性 要求核心系統不知道橫切功能。例如,考慮一個需要在事務中調用某些 操作的銀行系統。遺忘屬性要求核心銀行系統不知道是事務管理方面建議進行這 些操作。
注釋可以有兩種表現方式。首先,可以用注釋標記元素,這樣,切入點就可以 選取注釋的元素。該元素實質上是客戶驅動的參數,其中,注釋只是為了支持切 入點而存在。其次,可以將元素標記為它們總是聲明其非核心功能,不管是否需 要切入點。這兩種方式的區別可能只在於角度的不同,不過:在後一個例子中, 帶有注釋的元素不知道方面(或者其他元數據消費者),因而保持了遺忘屬性。
更進一步進行分析,當您將元數據視為“方面化的接口”,並遵守好的接口設 計通常所采用的好的做法時,就會自動保持遺忘屬性。事實上,保持遺忘性就是 在正確的地方使用正確的注釋類型,如在前面准則中所討論的那樣。
元數據與 AOP 采用
恰當地消費元數據是 Java 開發人員在未來幾年所面臨的有意義的挑戰之一。 Java 程序的元數據相對來說是一個比較新的概念(即使考慮 XDoclet 所做的努 力),而新的 Java 元數據功能是第一個將元數據一直保留到運行時的實用方法 。因此,Java 開發人員需要通過實踐來完全了解和優化新功能的使用,不管有沒 有 AOP。
也就是說,在使用正確的情況下,元數據可以使新人和有經驗的開發人員更容 易理解 AOP,並使 AOP 更有可能被廣泛采用。結合元數據與 AOP 的好處是雙重 的:首先,用傳統的切入點語法不能捕獲某些具有非核心橫切功能的連接點,這 一直是令 AOP 的早期使用者頭痛的一個問題。雖然像 Participant 模式這樣的 設計模式可能不真正需要元數據支持就能得到類似的結果,但是它們並不容易使 用。正如我在本文中所展示的,元數據可以方便地捕獲這些連接點來充實橫切功 能。
在 AOP 中使用元數據對 AOP 的采用帶來的第二個好處是它減緩了采用曲線。 即使在當前的初期階段,元數據也可以成為學習面向方面的編程的某種“培訓輪 (training wheel)”(正如 Bruce Tate 所說的,請參閱 參考資料)。對於編 寫第一個 AOP 類型的程序的開發人員而言,道路仍然不是很平坦。經驗的欠缺會 使在本文中討論的准則難以得到遵循,結果可能類似(正如前面提到的)美化的 宏。
不過,按這個過程進行是有好處的。當新的范式更方便時,它自然會深入到連 接點簽名的內在功能(以及類似 Participant 的模式)中並使用更抽象的元數據 。這種使用表示了元數據使用與 AOP 使用的一種平衡,還可能成為向釋放 AOP 終極功能方向邁進的一步。同時,我們都會摸索出在結合這兩種技術時可以遵循 的更多最佳實踐。
結束語
標准元數據功能提供了表示關於程序元素的額外信息的方便方法。如果使用得 當,這個功能可簡化軟件系統的創建。雖然有許多利用元數據的方法,但是在與 AOP 結合時它可以工作得最好。在本文中討論的最佳實踐是邁向在 AOP 中充分利 用元數據的第一步。除了准則,我還介紹了一些最適合結合元數據的 AOP 場景, 討論了在三種最重要的 AOP 系統中對元數據的支持,並對一個 AOP 系統進行系 統化的重構,以結合元數據。我還展示了將元數據視為多維關注點空間的簽名的 一種全新思路,這是一種與使用 AOP 范式和不使用該規范的開發人員都非常有關 的創建元數據類型的用法。
AOP 和元數據的結合就像是天作之合。在 Java 平台上加上標准元數據功能, 並在不同的 AOP 系統上支持它,可以很好排除 AOP 進入主流的最後障礙。不過 ,就像其他天作之合一樣,這需要雙方了解彼此的長處和短處,並尊重對方的界 線。我祝雙方能夠幸福地生活在一起!