用靜態橫切的強大功能建立高度鬆散的系統
Andrew Glover (aglover@vanwardtechnologies.com) CTO,Vanward Technologies 2004 年 3 月
許多 Java 開發人員已經接受了面向方面編程(AOP)的非強制性風格和靈活性,特別是在用於建立高度鬆散和可擴充的企業系統時。在本文中,您將看到 AOP 的功能設計概念之一(靜態橫切)如何把可能是一大堆混亂的緊密耦合的代碼轉變成一個強大的、可擴充的公司專屬應用程式程式。
在將商業需求轉換成軟體功能的快速開發週期中,面向方面是一種可以利用的強有力的設計原理。通過將首要的設計重點從物件導向編程(OOP)的傳統化特性轉移出來,AOP 和設計原理允許軟體架構師用一種和物件導向相當而又互補的方式來考慮設計。 在本文裡,您將學習如何? AOP 中最沒有得到充分利用的特性之一。橫切(crosscutting)是一種蘊含強大力量的相對簡單的設計和編程技術,尤其是當它被用於建立鬆散耦合的、可擴充的企業系統時。雖然動態橫切(其中對象的運行時行為可以改變)被認為是 AOP 的根基之一,但靜態橫切卻是一種遠不為人所知的技術。我將在本文中嘗試彌補這個缺憾。我將首先概述動態和靜態橫切,然後迅速切入一個實現情境來展示後一種技術。您將親自體會到靜態橫切是多麼方便地克服了如下這個最常見的企業挑戰之一:如何在利用第三方代碼的同時保持應用程式程式碼程式庫(codebase)的靈活性。 請注意,儘管我首先簡要地從概念上概述面向方面編程,但本文並不是一篇關於 AOP 的介紹。請參閱 參考資料 一節以獲得關於該主題的介紹性文章的列表。 AOP 概述 物件導向設計最根本的魅力在於,它能夠將真實世界領域中的實體及各自的行為建模為抽象的對象。以物件導向方式設計的系統產生了很多有效業務對象,比如 Person、Account、Order 以及 Event。物件導向設計的缺點在於,這樣的業務對象會因為混合的屬性和與對象最初意圖不一致的操作而變得混亂。 通過使設計者運用動態和靜態橫切,用一種非強制性的整潔和模組化的方法來添加對象行為,面向方面編程有效地解決了這一問題。 什麼是橫切? 橫切 是面向方面編程的專有名詞。它指的是在一個給定的編程模型中穿越既定的職責部分(比如日誌記錄和效能最佳化)的操作。在橫切的世界裡,橫切有兩種類型:動態橫切和靜態橫切。在本文中,儘管我將簡要地同時討論二者,但我主要關注靜態橫切。 動態橫切 動態橫切 是通過 切入點 和 連接點 在一個 方面 中建立行為的過程,連接點可以在執行時橫向地應用於現有對象。動態橫切通常用於協助向對象層次中的各種方法添加日誌記錄或身份認證。下面讓我們花點時間瞭解一下動態橫切中的一些實際概念:
- 方面(aspect)類似於 Java 程式設計語言中的類。方面定義切入點和通知(advice),並由諸如 AspectJ 這樣的方面編譯器來編譯,以便將橫切(包括動態和靜態)織入(interweave)現有的對象中。
- 一個 連接點(join point) 是程式執行中一個精確執行點,比如類中的一個方法。例如,對象
Foo 中的方法 bar() 就可以是一個連接點。連接點 是個抽象的概念;不用主動定義一個連接點。
- 一個 切入點(pointcut) 本質上一個用於捕捉連接點的結構。例如,可以定義一個切入點來捕捉對對象
Foo 中的方法 bar() 的所有調用。和連接點相反,切入點需要在方面中定義。
- 通知(advice) 是切入點的可執行代碼。一個經常定義的通知是添加日誌記錄功能,其中切入點捕捉對對象
Foo 中的 bar() 的每個調用,然後該通知動態地插入一些日誌記錄功能,比如捕捉 bar() 的參數。
這些概念是動態橫切的核心,雖然正如我們即將看到的,它們並不全都是靜態橫切所必需的。請參閱 參考資料 來瞭解關於動態橫切的更多內容。 靜態橫切 靜態橫切 和動態橫切的區別在於它不修改一個給定對象的執行行為。相反,它允許通過引入附加的方法欄位和屬性來修改對象的 結構。此外,靜態橫切可以把擴充和實現附加到對象的基本結構中。 雖然現在還無法談及靜態橫切的普遍使用——它看起來是 AOP 的一個相對未被探索(儘管非常具有吸引力)的特性——然而這一技術蘊含的潛力是巨大的。使用靜態橫切,架構師和設計者能用一種真正物件導向的方法有效地建立複雜系統的模型。靜態橫切允許您不用建立很深的階層,以一種本質上更優雅、更逼真於現實結構的方式,插入跨越整個系統的公用行為。 在本文剩下的篇幅中,我將重點講解靜態橫切的技術和應用。 建立靜態橫切 建立靜態橫切的文法和動態橫切有很大的不同,即沒有切入點和通知。給定一個對象(比如下面定義的 Foo),靜態橫切使得建立新方法、添加附加的建構函式,甚至改變繼承層次都變得十分簡單。我們將用一個例子來更好地展示靜態橫切是怎樣在一個現有的類中實現的。清單 1 顯示了一個簡單的沒有方面的 Foo。 清單 1. 沒有方面的 Foo
public class Foo { public Foo() { super(); }}
|
如清單 2 所示,在一個對象中添加一個新的方法和在一個方面中定義一個方法是同樣簡單的。 清單 2. 向 Foo 添加一個新方法
public aspect FooBar { void Foo.bar() { System.out.println("in Foo.bar()"); }}
|
建構函式略有區別的地方在於 new 關鍵字是必需的,如清單 3 所示。 清單 3. 向 Foo 添加一個新的建構函式
public aspect FooNew { public Foo.new(String parm1){ super(); System.out.println("in Foo(string parm1)"); }}
|
改變對象的繼承層次需要一個 declare parents 標籤。比如,為了變成多線程的,Foo 將需要實現 Runnable,或者擴充 Thread。清單 4 顯示了用 declare parents 標籤來改變 Foo 的繼承層次。 清單 4. 改變 Foo 的繼承層次
public aspect FooRunnable { declare parents: Foo implements Runnable; public void Foo.run() { System.out.println("in Foo.run()"); }}
|
現在,您可能開始獨自設想靜態橫切的含意了,特別是在與建立鬆散耦合、高度可擴充的系統有關時。在下面的幾小節中,我將帶您看一個一個真實的設計和實現情境,以展示使用靜態橫切來擴充您的公司專屬應用程式的靈活性是多麼容易。 實現情境 企業系統經常被設計來利用第三方的產品和庫。為了不把整個結構和所需產品耦合在一起,通常在設計來與外部廠商代碼互動的應用中包括進一個抽象層。在插入其他廠商的實現乃至自主開發的代碼時,這個抽象層在對系統的一致性產生最小破壞的情況下,為該體繫結構提供了高度的靈活性。 在這個實現情境中,設想系統在某一操作發生後,通過各不相同的通訊渠道通知客戶。這個例子系統使用了一個 Email 對象來代表直接電子郵件通訊的一個執行個體。如清單 5 所示,Email 對象包含了諸如寄件者地址、收件者地址、主題欄和訊息本文等屬性。 清單 5. 例子 Email 對象
public class Email implements Sendable { private String body; private String toAddress; private String fromAddress; private String subject; public String getBody() {return body; } public String getFromAddress() {return fromAddress; } public String getSubject() { return subject; } public String getToAddress() { return toAddress; } public void setBody(String string) { body = string; } public void setFromAddress(String string) { fromAddress = string; } public void setSubject(String string) { subject = string; } public void setToAddress(String string) { toAddress = string; }}
|
整合第三方代碼 除了建立一個寄送電子郵件、傳真、短訊息等的自訂通訊系統之外,體繫結構團隊決定整合進一個供應商的產品,該產品能遵循特定的規則,發送基於任意對象的訊息。該產品非常靈活,並且通過 XML 提供了一個映射機制,允許將自訂用戶端對象映射到與廠商的特定渠道實現。該廠商的系統嚴重依賴於這一對應檔和 Java 平台的反射能力來與普通 Java 對象協同工作。為了體現靈活性,體繫結構團隊建立了一個 Sendable 介面模型,如清單 6 所示。 清單 6. 例子 Sendable 介面
public interface Sendable { String getBody(); String getToAddress();}
|
圖 1 顯示了 Email 對象和 Sendable 介面的類圖。 圖 1. Email 和 Sendable 的類圖
設計挑戰 除了通過不同渠道發送各種格式的訊息的能力之外,供應商通過一個已給的介面,提供一個鉤子(hook)來允許進行收件者地址驗證。供應商的文檔表明,實現這個介面的任何對象都將遵循一個預定義的生命週期,它 validateAddress() 方法將被調用,並正確地處理相應的結果行為。如果 validateAddress() 返回 false,供應商的通訊系統將不再試圖進行相應的通訊。清單 7 顯示了供應商的 validateAddress() 介面。 清單 7. Sendable 介面的地址驗證
package com.acme.validate;public interface Validatable { boolean validateAddress();}
|
運用基本的物件導向設計原理,體繫結構團隊決定修改 Sendable 介面來擴充供應商的 Validatable 介面。但是,這一決定將導致對供應商代碼的直接依賴和耦合。如果Team Dev接下來決定用另一個供應商的工具,就不得不重構程式碼程式庫,以刪除 Sendable 介面中的 extends 語句和對象層次中已實現的行為。 一個更優雅且根本上更靈活的解決方案就是使用靜態橫切來對預期對象添加行為。 靜態橫切帶來援助 運用面向方面的原理,該團隊可以建立一個方面來聲明 Email 對象實現供應商的 Validatable 介面;此外,在方面中,體繫結構團隊將預期的行為編碼在 ValidataAddress() 方法中。因為 Email 對象中既不包含任何供應商包的匯入,也沒有定義 validateAddress() 方法,這樣代碼就更好地消除了耦合。Email 對象根本沒意識到它是 validatabl e 類型的對象!清單 8 顯示了結果方面,其中 Email 靜態地得到增強以實現供應商的 Validatable 介面。 清單 8. Email 可驗證方面
import com.acme.validate.Validatable;public aspect EmailValidateAspect { declare parents: Email implements Validatable; public boolean Email.validateAddress(){ if(this.getToAddress() != null){ return true; }else{ return false; } }}
|
測試一下! 您可以利用 JUnit 來證明 EmailValidateAspect 確實改變了 Email 對象。在一個 JUnit 測試套件中, Email 對象可以用預設值建立,然後一系列測試案例可以檢驗 Email 的確是 Validatable 的一個執行個體;此外,可以通過一個測試案例來斷言,如果 toAddress 為 null,對 validateAddress() 的調用將返回 false。另外,還可以用另一個測試案例來檢驗:一個非 null 的 toAddress 將導致 validateAddress() 返回 true。 1-2-3,用 JUnit 進行測試 您可以首先建立這樣一個結構,它構造帶有簡單值的 Email 對象執行個體。注意在清單 9 中,該執行個體確實有一個有效(意為非 nul)的toAddress 值。 清單 9. JUnit setUp()
import com.acme.validate.Validatable;public class EmailTest extends TestCase { private Email email; protected void setUp() throws Exception { //set up an email instance this.email = new Email(); this.email.setBody("body"); this.email.setFromAddress("dev@dev.com"); this.email.setSubject("validate me"); this.email.setToAddress("ag@ag.com"); } protected void tearDown() throws Exception { this.email = null; }//EmailTest continued...}
|
對於一個有效 Email 對象,estEmailValidateInstanceof() 確保執行個體是 Validatable 類型的,如清單 10 所示。 清單 10. JUnit 校正執行個體
public void testEmailValidateInstanceof() throws Exception{ TestCase.assertEquals("Email object should be of type Validatable", true, this.email instanceof Validatable); }
|
如清單 11 所示,下一個測試案例故意把 toAddress 欄位設定為 null ,然後檢驗 validateAddress() 將返回 false。 清單 11. JUnit null toAddress 檢查
public void testEmailAddressValidateNull() throws Exception{ //force a false this.email.setToAddress(null); Validatable validtr = (Validatable)this.email; TestCase.assertEquals("validateAddress should return false", false, validtr.validateAddress());}
|
最後一步是出於穩健的考慮:testEmailAddressValidateTrue() 測試案例用 Email 執行個體的初始值調用 validateAddress(),即 toAddress 域的值為 ag@ag.com。 清單 12. JUnit 非 null toAddress 檢查
public void testEmailAddressValidateTrue() throws Exception{ Validatable validtr = (Validatable)this.email; TestCase.assertEquals("validateAddress should return true", true, validtr.validateAddress());}
|
重構該例子 體繫結構團隊想方設法使用 Sendable 介面來抽象通訊實現;然而,他們的第一次嘗試就好像忽略了這個介面。從靜態橫切民Email 對象中吸取教訓之後,他們通過把契約行為在對象層次中提升到 Sendable 基底介面,從而進一步精鍊了其策略。 新的方面建立了一個用供應商的 Validatable 介面來對 Sendable 介面進行的擴充。此外,他們在方面中建立了已實現的行為。這次,validateAddress() 方法是為另一個通訊對象定義的:Fax,如清單 13 所示。 清單 13. 一個更好的方面
import com.acme.validate.Validatable;public aspect SendableValidateAspect { declare parents: Sendable extends Validatable; public boolean Email.validateAddress(){ if(this.getToAddress() != null){ return true; }else{ return false; } } public boolean Fax.validateAddress(){ if(this.getToAddress() != null && this.getToAddress().length() >= 11){ return true; }else{ return false; } }}
|
永遠不要停止重構 您可能注意到清單 13 中的方面稍有不足,因為所有 Sendable 的實現者各自都有在同一個方面中定義的 validateAddress() 方法。這很容易導致代碼膨脹。另外,如果不謹慎處理,改變一個介面的靜態結構將出現很多不希望的副作用:必須找到目標介面的所有實現者。因此,這裡的教訓很簡單:永遠不要停止重構。 |
結束語 雖然這裡的 API 例子是人為的,但它有望證明在企業體繫結構中應用靜態橫切是多麼簡單。靜態橫切應用於本文描述的這一類情境(它可以在其中用於非強制性地改變對象的行為甚至定義)尤為有效,不過它還有其他很多用處。譬如,您可以在開發時用靜態橫切來“EJB 化”POJO(傳統的普通 Java 對象);或者您可以在業務對象中用它來利用諸如 Hibernate 的持久架構的生命週期介面(請參閱參考資料)。 靜態橫切為很多影響企業代碼有效性的輕微缺陷提供了優雅的解決方案。通過本文,您已經學習了該技術的基礎知識和它最基本的應用之一。請參閱 參考資料 來學習更多關於面向方面編程和其他橫切技術的知識。 參考資料
- 下載本文中用到的原始碼。
- 您可以從 eclipse.org/aspectj 下載 AspectJ 及其相關工具。該網站還包括一個FAQ、郵件清單、精彩文檔和關於 AOP 的其他資源的連結,是一個開始進一步研究的好地方。
- AspectWerkz 是一個針對 Java 平台的動態、輕量級和高效能的 AOP/AOSD 架構。
- Eclipse IDE 特別提供了一個 AspectJ 外掛程式。
- 要獲得關於面向方面軟體開發的綜合資訊資源,請試試 AOSD.net。
- JBoss 團隊已經建立了一個有趣的 AOP 架構。
- Hibernate 是針對 Java 平台的一個強大的、超高效能的對象/關係型持久性和查詢服務。
- Codehaus一個包含很多有趣的開放原始碼項目的大型知識庫,其中包括 AspectWerkz 和 Nanning(另外一個針對 Java 平台的 Aspect 實現)。
- 訪問 Developer Bookstore 以獲得技術書籍的全面列表,其中包括 Ivan Kiselev 的 Aspect-Oriented Programming with AspectJ (Sams Publishing,2002)和 Ramnivas Laddad 的 AspectJ in Action (Manning Publishing,2003),以及其他的大量 Java 相關的書籍。
- 在 developerWorks Java 技術專區 可以找到關於 Java 編程各個方面的文章。
- 也可以從 developerWorks 瀏覽 Java 技術專區教程首頁 ,獲得針對 Java 的免費教程的詳盡列表。
關於作者 Andrew Glover 是 Vanward Technologies 的首席技術官(CTO),該公司是一家位於華盛頓特區市中心的公司,專業從事自動化的測試架構的構建,以減少軟體中的 bug 數量,降低整合和測試次數,並提高整體代碼穩定性。 |
|