追求代码质量: 用 AOP 进行防御性编程
原文出處:?IBM中國
開發人員測試的主要缺點是:絕大部分測試都是在理想的場景中進行的。在這些情況下并不會出現缺陷 —— 能導致出現問題的往往是那些邊界情況。
什么是邊界情況呢?比方說,把?null?值傳入一個并未編寫如何處理?null?值的方法中,這就是一種邊界情況。大多數開發人員通常都不能成功測試這樣的場景,因為這沒多大意義。但不管有沒有意義,發生了這樣的情況,就會拋出一個?NullPointerException,然后整個程序就會崩潰。
本月,我將為您推薦一種多層面的方法,來處理代碼中那些不易預料的缺陷。嘗試為應用程序整合進防御性編程、契約式設計和一種叫做 OVal 的易用的通用驗證框架。
將敵人暴露出來
清單 1 中的代碼為給定的?Class?對象(省去了?java.lang.Object,因為所有對象都最終由它擴展)構建一個類層次。但如果仔細看的話,您會注意到一個有待發現的潛在缺陷,即該方法對對象值所做的假設。
清單 1. 不檢驗 null 的方法
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public static Hierarchy buildHierarchy(Class clzz){ ?Hierarchy hier = new Hierarchy(); ?hier.setBaseClass(clzz); ?Class superclass = clzz.getSuperclass(); ?if(superclass != null && superclass.getName().equals("java.lang.Object")){ ??return hier; ?}else{????? ??while((clzz.getSuperclass() != null) && ????(!clzz.getSuperclass().getName().equals("java.lang.Object"))){ ?????clzz = clzz.getSuperclass(); ?????hier.addClass(clzz); ??}???????? ??return hier; ?} } |
剛編好這個方法,我還沒注意到這個缺陷,但由于我狂熱地崇拜開發人員測試,于是我編寫了一個使用 TestNG 的常規測試。而且,我還利用了 TestNG 方便的?DataProvider?特性,借助該特性,我創建了一個通用的測試用例并通過另一個方法來改變它的參數。運行清單 2 中定義的測試用例會產生兩個通過結果!一切都運轉良好,不是嗎?
清單 2. 驗證兩個值的 TestNG 測試
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | import java.util.Vector; import static org.testng.Assert.assertEquals; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; public class BuildHierarchyTest { ????? ?@DataProvider(name = "class-hierarchies") ?public Object[][] dataValues(){ ??return new Object[][]{ ???{Vector.class, new String[] {"java.util.AbstractList", ??????"java.util.AbstractCollection"}}, ???{String.class, new String[] {}} ??}; ?} ?@Test(dataProvider = "class-hierarchies"}) ?public void verifyHierarchies(Class clzz, String[] names) throws Exception{ ??Hierarchy hier = HierarchyBuilder.buildHierarchy(clzz); ??assertEquals(hier.getHierarchyClassNames(), names, "values were not equal"); ?} } |
至此,我還是沒有發現缺陷,但一些代碼問題卻困擾著我。如果有人不經意地為?Class?參數傳入一個?null?值會怎么樣呢?清單 1?中第 4 行的clzz.getSuperclass()?調用會拋出一個?NullPointerException,是這樣嗎?
測試我的理論很容易;甚至都不用從頭開始。僅僅把?{null, null}?添加到初始?BuildHierarchyTest?的?dataValues?方法中的多維?Object?數組中,然后再次運行它。我定會得到如圖 1 所示的?NullPointerException:
圖 1. 可怕的 NullPointerException
參見這里的?全圖。
防御性編程
一旦出現這個問題,下一步就是要拿出對抗的策略。問題是我控制不了這個方法能否接收這種輸入。對于這類問題,開發人員通常會使用防御性編程技術,該技術專門用來在發生摧毀性后果前捕捉潛在錯誤。
對象驗證是處理不確定性的一項經典的防御性編程策略。相應地,我會添加一項檢驗來驗證clzz?是否為?null,如清單 3 所示。如果其值最終為?null,我就會拋出一個RuntimeException?來警告他人注意這個潛在問題。
清單 3. 添加驗證 null 值的檢驗
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public static Hierarchy buildHierarchy(Class clzz){ ?? ?if(clzz == null){ ??throw new RuntimeException("Class parameter can not be null"); ?} ?Hierarchy hier = new Hierarchy(); ?hier.setBaseClass(clzz); ?Class superclass = clzz.getSuperclass(); ?if(superclass != null && superclass.getName().equals("java.lang.Object")){ ??return hier; ?}else{????? ??while((clzz.getSuperclass() != null) && ????(!clzz.getSuperclass().getName().equals("java.lang.Object"))){ ?????clzz = clzz.getSuperclass(); ?????hier.addClass(clzz); ??}???????? ??return hier; ?} } |
很自然,我也會編寫一個快速測試用例來驗證我的檢驗是否真能避免?NullPointerException,如清單 4 所示:
清單 4. 驗證 null 檢驗
| 1 2 3 4 5 | @Test(expectedExceptions={RuntimeException.class}) public void verifyHierarchyNull() throws Exception{ ?Class clzz = null; ?HierarchyBuilder.buildHierarchy(null);???? } |
在本例中,防御性編程似乎解決了問題。但僅依靠這項策略會存在一些缺陷。
防御的缺陷
盡管防御性編程有效地保證了方法的輸入條件,但如果在一系列方法中使用它,不免過于重復。熟悉面向方面編程(或 AOP)的人們會把它認為是橫切關注點,這意味著防御性編程技術橫跨了代碼庫。許多不同的對象都采用這些語法,盡管從純面向對象的觀點來看這些語法跟對象毫不相關。
而且,橫切關注點開始滲入到契約式設計(DBC)的概念中。DBC 是這樣一項技術,它通過在組件的接口顯式地陳述每個組件應有的功能和客戶機的期望值來確保系統中所有的組件完成它們應盡的職責。從 DBC 的角度講,組件應有的功能被認為是后置條件,本質上就是組件的責任,而客戶機的期望值則普遍被認為是前置條件。另外,在純 DBC 術語中,遵循 DBC 規則的類針對其將維護的內部一致性與外部世界有一個契約,即人所共知的類不變式。
契約式設計
我在以前的一篇關于用 Nice 編程的文章中介紹過 DBC 的概念,Nice 是一門與 JRE 兼容的面向對象編程語言,它的特點是側重于模塊性、可表達性和安全性。有趣的是,Nice 并入了功能性開發技術,其中包括了一些在面向方面編程中的技術。功能性開發使得為方法指定前置條件和后置條件成為可能。
盡管 Nice 支持 DBC,但它與 Java? 語言完全不同,因而很難將其用于開發。幸運的是,很多針對 Java 語言的庫也都為 DBC 提供了方便。每個庫都有其優點和缺點,每個庫在 DBC 內針對 Java 語言進行構建的方法也不同;但最近的一些新特性大都利用了 AOP 來更多地將 DBC 關注點包括進來,這些關注點基本上就相當于方法的包裝器。
前置條件在包裝過的方法執行前擊發,后置條件在該方法完成后擊發。使用 AOP 構建 DBC 結構的一個好處(請不要同該語言本身相混淆!)是:可以在不需要 DBC 關注點的環境中將這些結構關掉(就像斷言能被關掉一樣)。以橫切的方式對待安全性關注點的真正妙處是:可以有效地重用?這些關注點。眾所周知,重用是面向對象編程的一個基本原則。AOP 如此完美地補充了 OOP 難道不是一件極好的事情嗎?
結合了 OVal 的 AOP
OVal 是一個通用的驗證框架,它通過 AOP 支持簡單的 DBC 結構并明確地允許:
- 為類字段和方法返回值指定約束條件
- 為結構參數指定約束條件
- 為方法參數指定約束條件
此外,OVal 還帶來大量預定義的約束條件,這讓創建新條件變得相當容易。
由于 OVal 使用 AspectJ 的 AOP 實現來為 DBC 概念定義建議,所以必須將 AspectJ 并入一個使用 OVal 的項目中。對于不熟悉 AOP 和 AspectJ 的人們來說,好消息是這不難實現,且使用 OVal (甚至是創建新的約束條件)并不需要真正對方面進行編碼,只需編寫一個簡單的自引導程序即可,該程序會使 OVal 所附帶的默認方面植入您的代碼中。
在創建這個自引導程序方面前,要先下載 AspectJ。具體地說,您需要將?aspectjtools?和?aspectjrt?JAR 文件并入您的構建中來編譯所需的自引導程序方面并將其編入您的代碼中。
自引導 AOP
下載了 AspectJ 后,下一步是創建一個可擴展 OVal?GuardAspect?的方面。它本身不需要做什么,如清單 5 所示。請確保文件的擴展名以 .aj 結束,但不要試著用常規的?javac?對其進行編譯。
清單 5. DefaultGuardAspect 自引導程序方面
| 1 2 3 4 5 6 7 | import net.sf.oval.aspectj.GuardAspect; public aspect DefaultGuardAspect extends GuardAspect{?? ?public DefaultGuardAspect(){ ??super();????? ?}? } |
AspectJ 引入了一個 Ant 任務,稱為?iajc,充當著?javac?的角色;此過程對方面進行編譯并將其編入主體代碼中。在本例中,只要是我指定了 OVal 約束條件的地方,在 OVal 代碼中定義的邏輯就會編入我的代碼,進而充當起前置條件和后置條件。
請記住?iajc?代替了?javac。例如,清單 6 是我的 Ant build.xml 文件的一個代碼片段,其中對代碼進行了編譯并把通過代碼標注發現的所有 OVal 方面編入進來,如下所示:
清單 6. 用 AOP 編譯的 Ant 構建文件片段
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <target name="aspectjc" depends="get-deps"> ?<taskdef resource="org/aspectj/tools/ant/taskdefs/aspectjTaskdefs.properties"> ??<classpath> ???<path refid="build.classpath" /> ??</classpath> ?</taskdef> ?<iajc destdir="${classesdir}" debug="on" source="1.5"> ??<classpath> ???<path refid="build.classpath" /> ??</classpath> ??<sourceroots> ???<pathelement location="src/java" /> ???<pathelement location="test/java" /> ??</sourceroots> ?</iajc> </target> |
為 OVal 鋪好了路、為 AOP 過程做了引導之后,就可以開始使用 Java 5 標注來為代碼指定簡單的約束條件了。
OVal 的可重用約束條件
用 OVal 為方法指定前置條件必須對方法參數進行標注。相應地,當調用一個用 OVal 約束條件標注過的方法時,OVal 會在該方法真正執行前驗證該約束條件。
在我的例子中,我想要指定當?Class?參數的值為?null?時,buildHierarchy?方法不能被調用。OVal 通過?@NotNull?標注支持此約束條件,該標注在方法所需的所有參數前指定。也要注意,任何想要使用 OVal 約束條件的類也必須在類層次上指定?@Guarded?標注,就像我在清單 7 中所做的那樣:
清單 7. OVal 約束條件
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | import net.sf.oval.annotations.Guarded; import net.sf.oval.constraints.NotNull; @Guarded public class HierarchyBuilder {? ?public static Hierarchy buildHierarchy(@NotNull Class clzz){ ??Hierarchy hier = new Hierarchy(); ??hier.setBaseClass(clzz); ??Class superclass = clzz.getSuperclass(); ??if(superclass != null && superclass.getName().equals("java.lang.Object")){ ???return hier; ??}else{????? ???while((clzz.getSuperclass() != null) && ?????(!clzz.getSuperclass().getName().equals("java.lang.Object"))){ ???????clzz = clzz.getSuperclass(); ???????hier.addClass(clzz); ????}?????????? ???return hier; ??} ?}????? } |
通過標注指定這個約束條件意味著我的代碼不再會被重復的條件弄得亂七八糟,這些條件檢查?null?值,并且一旦找到該值就會拋出異?!,F在這項邏輯由 OVal 處理,且處理的方法有些相似 —— 事實上,如果違反了約束條件,OVal 會拋出一個?ConstraintsViolatedException,它是?RuntimeException?的子類。
當然,我下一步就要編譯?HierarchyBuilder?類和?清單 5?中相應的?DefaultGuardAspect?類。我用?清單 6?中的?iajc?任務來實現這一目的,這樣我就能把 OVal 的行為編入我的代碼中了。
接下來,我更新?清單 4?中的測試用例來驗證是否拋出了一個?ConstraintsViolatedException,如清單 8 所示:
清單 8. 驗證是否拋出了 ConstraintsViolatedException
| 1 2 3 4 5 | @Test(expectedExceptions={ConstraintsViolatedException.class}) public void verifyHierarchyNull() throws Exception{ ?Class clzz = null; ?HierarchyBuilder.buildHierarchy(clzz);???? } |
指定后置條件
正如您所見,指定前置條件其實相當容易,指定后置條件的過程也是一樣。例如,如果我想對所有調用?buildHierarchy?的程序保證它不會返回?null?值(這樣,這些調用程序就不需要再檢查這個了),我可以在方法聲明之上放置一個?@NotNull?標注,如清單 9 所示:
清單 9. OVal 中的后置條件
| 1 2 3 4 | @NotNull public static Hierarchy buildHierarchy(@NotNull Class clzz){?? ?//method body } |
當然,@NotNull?絕不是 OVal 提供的惟一約束條件,但我發現它能非常有效地限制這些令人討厭的?NullPointerException,或至少能夠快速地暴露?它們。
更多的 OVal 約束條件
OVal 也支持在方法調用前或后對類成員進行預先驗證。這種機制具有限制針對特定約束條件的重復條件測試的好處,如集合大小或之前討論過的非?null?的情況。
例如,在清單 10 中,我使用?HierarchyBuilder?定義了一個為類層次構建報告的 Ant 任務。請注意?execute()?方法是如何調用?validate的,后者會依次驗證?fileSet?類成員是否含值;如果不含,會拋出一個異常,因為沒有了要評估的類,該報告不能運行。
清單 10. 帶條件檢驗的 HierarchyBuilderTask
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | public class HierarchyBuilderTask extends Task { ?private Report report; ?private List fileSet; ?private void validate() throws BuildException{ ??if(!(this.fileSet.size() > 0)){ ???throw new BuildException("must supply classes to evaluate"); ??} ??if(this.report == null){ ???this.log("no report defined, printing XML to System.out"); ??} ?} ?public void execute() throws BuildException { ??validate(); ??String[] classes = this.getQualifiedClassNames(this.fileSet); ??Hierarchy[] hclz = new Hierarchy[classes.length]; ??try{ ???for(int x = 0; x < classes.length; x++){ ????hclz[x] = HierarchyBuilder.buildHierarchy(classes[x]);????????????? ???}??????? ???BatchHierarchyXMLReport xmler = new BatchHierarchyXMLReport(new Date(), hclz); ???this.handleReportCreation(xmler); ??}catch(ClassNotFoundException e){ ???throw new BuildException("Unable to load class check classpath! " + e.getMessage()); ??}? ?} //more methods below.... } |
因為我用的是 OVal,所以我可以完成下列任務:
- 對?fileSet?類成員指定一個約束條件,確保使用?@Size?標注時其大小總是至少為 1 或更大。
- 確保在使用?@PreValidateThis?標注調用?execute()?方法前?驗證這個約束條件。
這兩步讓我能夠有效地去除?validate()?方法中的條件檢驗,讓 OVal 為我完成這些,如清單 11 所示:
清單 11. 經過改進、無條件檢驗的 HierarchyBuilderTask
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | @Guarded public class HierarchyBuilderTask extends Task { ?private Report report; ?@Size(min = 1) ?private List fileSet; ?private void validate() throws BuildException { ??if (this.report == null) { ???this.log("no report defined, printing XML to System.out"); ??} ?} ?@PreValidateThis ?public void execute() throws BuildException { ??validate(); ??String[] classes = this.getQualifiedClassNames(this.fileSet); ??Hierarchy[] hclz = new Hierarchy[classes.length]; ??try{ ???for(int x = 0; x < classes.length; x++){?????????? ????hclz[x] = HierarchyBuilder.buildHierarchy(classes[x]);????????????? ???}??????? ???BatchHierarchyXMLReport xmler = new BatchHierarchyXMLReport(new Date(), hclz); ???this.handleReportCreation(xmler); ??}catch(ClassNotFoundException e){ ???throw new BuildException("Unable to load class check classpath! " + e.getMessage()); ??}? ?} ?//more methods below.... } |
清單 11 中的?execute()?一經調用(由 Ant 完成),OVal 就會驗證?fileSet?成員。如果其為空,就意味著沒有指定任何要評估的類,就會拋出一個?ConstraintsViolatedException。這個異常會暫停這一過程,就像初始代碼一樣,只不過初始代碼會拋出一個?BuildException。
結束語
防御性編程結構阻止了一個又一個缺陷,但這些結構本身卻不免為代碼添加了重復的邏輯。把防御性編程技術和面向方面編程(通過契約式設計)聯系起來是抵御所有重復性代碼的一道堅強防線。
OVal 并不是惟一可用的 DBC 庫,事實上其 DBC 結構對比其他框架來說是相當有限的(例如,它未提供指定類不變式的簡易方法)。從另一方面講,OVal 很容易使用,對約束條件也有很大的選擇余地,若想要花少量力氣就可向代碼添加驗證約束條件,它無疑是個上佳之選。另外,用 OVal 創建定制約束條件也相當簡單,所以請不要再添加條件檢驗了,盡情享用 AOP 吧!
參考資料
學習
- 您可以參閱本文在 developerWorks 全球站點上的?英文原文?。
- “AOP 解決緊密耦合的難題”(Andrew Glover,developerWorks,2004 年 2 月):親自體驗一下 AOP 的功能設計概念之一(靜態橫切)如何把可能亂成一團的緊密耦合的代碼轉變成一個強大的、可擴展的企業應用程序。
- “alt.lang.jre: Nice 的雙倍功能” (Andrew Glover,developerWorks,2004 年 10 月):固定撰稿人,在各方面都很 “Nice” 的 Andrew Glover 將向您說明 Nice 的一些最令人激動的功能,包括 DBC 功能。
- “用 AOP 增強契約”(Filippo Diotalevi,developerWorks,2004 年 7 月):Filippo Diotalevi 介紹了 AOP 如何能在保持代碼簡潔和靈活的同時為組件間定義清晰的契約。
- Limiting conditional complexity with AOP(testearly.com,2006 年 12 月):介紹了 OVal 在 DBC 結構中的簡單用法。
- “AOP@Work: 用 Contract4J 進行組件設計”(Dean Wampler,developerWorks,2006 年 4 月):Dean Wampler 介紹了 Contract4J,一個 DBC 工具,它使用 Java 5 標注指定契約,并使用 AspectJ 方面在運行時評估契約。
- “Merlin 的魔力: 使用斷言”(John Zukowski,developerWorks,2002 年 2 月):John Zukowski 為您詳細介紹了為代碼添加斷言檢驗以及啟用和禁用斷言的基本知識。
- “TestNG 使 Java 單元測試輕而易舉”(Filippo Diotalevi,developerWorks,2005 年 1 月):Filippo Diotalevi 介紹了 TestNG,這是一個測試 Java 應用程序的新框架。
- “追求代碼質量: JUnit 4 與 TestNG 的對比”(Andrew Glover,developerWorks,2006 年 8 月):Andrew Glover 探討了這兩種框架各自的獨特之處,并闡述了 TestNG 獨有的三種高級測試特性。
- 追求代碼質量?系列(Andrew Glover,developerWorks):了解更多有關代碼語法、測試框架以及如何編寫專注于質量的代碼的信息。
- developerWorks Java 技術專區:這里有數百篇關于 Java 編程各方面的文章。
獲得產品和技術
- 下載 OVal:可用于所有 Java 對象的一個通用的驗證框架。
- 下載 TestNG:一個受 JUnit 和 NUnit 啟發而創建的測試框架,但該框架引入了一些新特性,使其功能更強大,也更容易使用。
- 下載 AspectJ:Java 編程語言的一個無縫的面向對象的擴展。
討論
- 參與論壇討論。
轉載于:https://www.cnblogs.com/wozixiaoyao/p/5658943.html
總結
以上是生活随笔為你收集整理的追求代码质量: 用 AOP 进行防御性编程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux内核驱动之延时---内核超时处
- 下一篇: 手把手教你从Core Data迁移到Re