让开发自动化持续重构 --使用静态分析工具识别代码味道
系列內容:
此內容是該系列的一部分:讓開發自動化
在過去的幾年里,我曾看過很多項目的大量源代碼,從精美的設計到像是用膠帶綁定到一起的代碼。我寫過新的代碼也維護過其他開發人員的源代碼。我喜歡編寫新的代碼,但也喜歡采用一些現有的代碼,以某種方法將其簡化或將重復的代碼提取到一個公共類中。在我早期的工作生涯中,許多人都認為如果不編寫新的代碼就不會有好的效率。幸好,在 20 世紀 90 年代末,Martin Fowler 編寫了?Refactoring一書(參見?參考資料),它使得在不改變外部行為的前提下改進現有代碼成為可能。
我在?本系列中所一直推崇的就是?效率:如何減少耗時過程的冗余度,更快速地執行它們。在本文的任務中,我一樣推崇這個目標,并且將論述怎樣更?有效地執行它們。
關于本系列
作為開發人員,我們致力于為用戶自動化流程;但許多開發人員疏忽了自動化我們自己的開發流程的機會。為此,我們編寫了?讓開發自動化系列文章,專門探討軟件開發流程自動化的實踐應用,為您介紹?何時以及?如何成功應用自動化。
重構的一個典型方法是在引入新代碼或更改方法時對現有代碼做出小小的變動。該技巧面臨的挑戰在于一個開發團隊的開發人員的應用方法不一致,并且很容易錯失重構的機會。這也正是我提倡使用靜態分析工具識別編碼違規的原因所在。有了這些工具,您就能夠從總體上了解代碼庫,并且處于類或方法的級別。幸運的是,在 Java?程序設計中,您可以選擇的可免費下載的開源靜態分析工具很多:CheckStyle、PMD、FindBugs、JavaNCSS、JDepend 等等。
在本文中,您將學習如何:
- 使用 CheckStyle 度量?圈復雜度(cyclomatic complexity),并提供諸如?Replace Conditional with Polymorphism之類的重構,以此來減少?條件復雜度代碼味道
- 使用 CheckStyle 評估?代碼重復率,并提供諸如?Pull Up Method之類的重構,以此來移除?重復代碼
- 使用 PMD(或 JavaNCSS)計算?源代碼行,并提供諸如?Extract Method之類的重構,以此來淡化?大類代碼味道
- 使用 CheckStyle(或 JDepend)確定一個類的?傳出耦合度(efferent coupling),并提供諸如?Move Method之類的重構,以此來除掉?過多的導入代碼味道
我將使用如下的通用格式來檢查每一種代碼味道:
實質上,這個方法提供了一個找到和修復整個代碼庫中的代碼味道的一個框架。這樣您就可以更好地了解到代碼庫中較危險的部分,然后再做出更改。更好的是,我還會向您展示如何將這個方法集成到自動構建中。
您的代碼有 么?
所謂代碼味道其實只是一種?提示,提示一些內容可能存在錯誤。和模式類似,代碼味道提供了一個通用詞匯表,您可以用它來快速識別這些類型的潛在問題。在文章中?真實地示范代碼味道是很有難度的,因為它可能包括很多行代碼,這樣就過分地加大了文章的篇幅。因此,我會只針對其中的一些味道進行示范,然后您就可以根據查看特定代碼味道的經驗進行推斷,識別出剩余的代碼味道。
條件復雜度
味道:條件復雜度
度量:圈復雜度
工具:CheckStyle、JavaNCSS 以及 PMD
重構:Replace Conditional with Polymorphism、Extract Method
味道
條件復雜度可以以幾種不同的方式出現在源代碼中。這種代碼味道的一個例子就是含有多個條件語句,如?if、while或者?for語句。另一種條件復雜度是以?switch語句的形式呈現出來的,如清單 1 所示:
清單 1. 使用?switch語句來執行條件行為
| 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 | ... switch (beerType) { ?case LAGER:? ???System.out.println("Ingredients are..."); ????... ????break; ?case BROWNALE: ???System.out.println("Ingredients are..."); ????... ????break; ?case PORTER? ???System.out.println("Ingredients are..."); ????... ????break; ?case STOUT: ???System.out.println("Ingredients are..."); ????... ????break; ?case PALELAGER: ???System.out.println("Ingredients are..."); ????... ????break; ?... ?default: ???System.out.println("INVALID."); ????... ????break; } ... |
switch語句本身并沒有不妥。但當一個語句包含太多的選擇和代碼時,它就可能暗示有需要重構的代碼。
度量
要確定條件復雜度代碼味道,需要確定方法的?圈復雜度。圈復雜度是一種度量方法,由 Thomas McCabe 于 1975 年定義。圈復雜度數(Cyclomatic Complexity Number,CCN)度量一個方法中某一路徑的數量。無論一個方法中有多少條路徑,它的起始 CNN 都從 1 開始。每一個條件構造,如?if、switch、while和?for語句,都被分配一個 1 值和異常路徑。一個方法的總的 CCN 表明了它的復雜度。很多人認為當 CCN 為 10 或超過 10 時,就表明該方法過于復雜。
工具
CheckStyle、JavaNCSS、以及 PMD 都是度量圈復雜度的開源工具。清單 2 展示了用 XML 定義的 CheckStyle 規則文件的一個代碼片斷。CyclomaticComplexity模塊定義了一個方法的 CCN 的最大限度。
清單 2. 配置 CheckStyle,查找圈復雜度為 10 或大于 10 的方法
| 1 2 3 | <module name="CyclomaticComplexity"> ?<property name="max" value="10"/> </module> |
用清單 2 的 CheckStyle 規則文件、清單 3 的 Gant 例子來示范如何將 CheckStyle 作為一個自動構建的一部分來運行。(參見?什么是 Gant ?側邊欄):
清單 3. 使用 Gant 腳本來執行 CheckStyle 檢查
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | target(findHighCcn:"Finds method with a high cyclomatic complexity number"){ ?Ant.mkdir(dir:"target/reports") ?Ant.taskdef(name:"checkstyle", ???classname:"com.puppycrawl.tools.checkstyle.CheckStyleTask", ???classpathref:"build.classpath") ?Ant.checkstyle(shortFilenames:"true", config:"config/checkstyle/cs_checks.xml", ???failOnViolation:"false", failureProperty:"checks.failed", classpathref:"libdir") { ???formatter(type:"xml", tofile:"target/reports/checkstyle_report.xml") ???formatter(type:"html", tofile:"target/reports/checkstyle_report.html") ???fileset(dir:"src"){ ?????include(name:"**/*.java") ???} ?} } |
什么是 Gant ?
Gant 是一個自動構建工具,它提供了一個支持構建依賴關系的表達能力強的編程語言。開發人員利用 Groovy 編程語言的強大功能編寫 Gant 腳本。由于 Gant 提供對 Ant 的 API 的完全訪問,所以任何可以運行于 Ant 的東西都可以從 Gant 腳本運行。(參見 “用 Gant 構建軟件” 教程,了解 Gant。)
清單 3 中的 Gant 腳本創建了圖 1 中展示的 CheckStyle 報告。該圖下面的部分指示出了一個方法的 CheckStyle 圈復雜度違規。
圖 1. CheckStyle 報告根據過高的 CCN 來指示一種方法失敗
重構
圖 2 為用 UML 表示的?Replace Conditional with Polymorphism重構:
圖 2. 用多態替代條件語句
點擊查看大圖
在圖 2 中,我:
為了使文章保持簡潔,我僅為每一個類提供一個方法的實現。顯然,創建一個界面的方法可能不只一個。重構能夠使代碼更易于維護,如 Replace Conditional with Polymorphism 和 Extract Method(本文稍后將會討論)。
重復代碼
味道:重復代碼
度量:代碼重復率
工具:CheckStyle、PMD
重構:Extract Method、Pull Up Method、Form Template Method、Substitute Algorithm
味道
重復代碼可能在代碼庫中悄然發生。有時,復制粘貼某些代碼要比將該行為泛化到另一個類更簡單。但復制粘貼的方法存在一個問題,即它強制將代碼復制多份,并且需要維護。而且當復制出的代碼發生輕微的變化而引發行為不一致時,就會發生更不易察覺的問題,具體取決于哪個方法在執行該行為。清單 4 是一個關閉代碼庫連接的代碼示例,相同的代碼出現在兩種方法中:
清單 4. 重復代碼
| 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 32 33 34 35 36 37 | public Collection findAllStates(String sql) { ... ?try { ???if (resultSet != null) { ?????resultSet.close(); ???} ???if (stmt != null) { ?????stmt.close(); ???} ???if (conn != null) { ????conn.close(); ??} ??catch (SQLException se) { ????throw new RuntimeException(se); ??} } ... } ... public int create(String sql, Beer beer) { ... ?try { ???if (resultSet != null) { ?????resultSet.close(); ???} ???if (stmt != null) { ?????stmt.close(); ???} ???if (conn != null) { ????conn.close(); ??} ??catch (SQLException se) { ????throw new RuntimeException(se); ??} } ... } |
我有性能更好的 IDE
雖然在本文的例子中我使用 Gant 來運行查找特定味道的工具,但是使用 IDE 也同樣可以解決這些問題。Eclipse IDE 就有很多靜態分析工具的插件。但我仍然推薦使用自動構建工具,因為這樣可以在其他環境中運行集成構建,無需使用 IDE。
度量
查找重復代碼的度量方法是在代碼庫中的類的內部和其他類之間搜索代碼重復。沒有工具的話,類間的重復就更難評估。由于復制的代碼通常都會發生一些輕微的變化,因此不僅要度量完全相同的代碼,而且要度量?相似的代碼,兩者都很重要。
工具
PMD 的 Copy/Paste Detector(CPD)與 CheckStyle 這兩種開源工具可以用于在整個 Java 代碼庫中查找相似的代碼。清單 5 中的 CheckStyle 配置文件例子示范了如何使用?StrictDuplicateCode模塊:
清單 5. 使用 CheckStyle 找到至少 10 行重復代碼
| 1 2 3 | <module name="StrictDuplicateCode"> ?<property name="min" value="10"/> </module> |
清單 5 中的?min屬性設置了 CheckStyle 將會標記出的最小重復行數,以供查閱。在這樣的情況下,它將只指示出那些至少有 10 行類似或重復的代碼塊。
圖 3 展示了自動構建運行后,清單 5 中的模塊設置的結果:
圖 3. CheckStyle 報告指示代碼重復度過高
重構
在清單 6 中,我用了?清單 4中的重復代碼,使用了?Pull Up Method重構來降低重復度 —將行為從較大方法提取到一個抽象類方法中:
清單 6. Pull Up Method
| 1 2 3 4 5 | ... } finally { ?closeDbConnection(rs, stmt, conn); } ... |
不要忘記編寫測試程序
任何時候改變現有代碼,您都需要用諸如 JUnit 這樣的框架編寫相應的自動測試程序。修改現有代碼是存在風險的;而將這個風險降到最低的一種方法就是通過測試來驗證該行為在現在和將來都有效。
重復代碼是難以避免的。我永遠不會建議一個團隊去努力實現什么?無重復之類的目標,這是不切實際的。然而,確保代碼庫中的重復代碼不會增多這樣的目標是可以實現的。使用諸如 PMD 的 CPD 或 CheckStyle 這樣的靜態分析工具,您能夠將整個分析過程作為自動構建的一部分,持續分析,確定代碼重復度高的區域。
長方法(大類)
味道:長方法(大類)
度量:源代碼行數(SLOC)
工具:PMD、JavaNCSS、CheckStyle
重構: Extract Method、Replace Temp with Query、Introduce Parameter Object、Preserve Whole Object、Replace Method with Method Object
味道
我一直在嘗試堅持的一條經驗法則是將方法限制在 20 行或 20 行以內。當然,這個原則也可能會有例外,但如果我的方法超過 20 行的話,我就會更仔細地去了解它。通常情況下,長方法和條件復雜度是息息相關的。而大類與長方法之間又有著必然的聯系。我可以給您展示一個 2200 行的方法,這個方法是我在需要維護的一個項目上發現的。我將整個含有 25000 行的代碼的類打印了出來,讓我的同事來找出里面的錯誤。這么說吧,當我把打印出來的代碼沿著走廊卷起來的時候,他們就已經同意我的看法了。
清單 7 中高亮顯示的部分展示了一個長方法代碼味道示例的一小部分:
清單 7. 長方法代碼味道
| 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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | public void saveLedgerInformation() { ... try { ?if (ledger.getId() != null && filename == null) { ???getLedgerService().saveLedger(ledger); ?} else { ???accessFiles().decompressFiles(files, filenames); ?} ?if (!files.get(0).equals(upload)) { ???upload = files.get(0); ???filename = filenames.get(0); ?} ?if (invalidFiles.isUnsupported(filename)) { ???setError(fileName, message.getMessage()); ?} else { ?LedgerFile entryFile = accessFiles().add(upload, filename); ?if (fileType != null && FileType.valueOf(fileType) != null) { ???entryFile.setFileType(FileType.valueOf(fileType)); ?} ?getFileManagementService().saveLedger(ledger, entryFile); ?if (!FileStatus.OPENED.equals(entryFile.getFileStatus())) { ???getFileManagementService().importLedgerDetails(ledger); ?} ?if (uncompressedFiles.size() > 1) { ???Helper.saveMessage(getText("ledger.file")); ?} ?? ?if (user.getLastName() != null) { ???SearchInfo searchInfo = ServiceLocator.getSearchInfo(); ???searchInfo.setLedgerInfo(null); ???isValid = false; ???setDefaultValues(); ???resetSearchInfo(); ???if (searchInfoValid && ledger != null) { ?????isValid = true; ???} ?} } catch (InvalidDataFileException e) { ?ResultType result = e.getResultType(); ?for (ValidationMessage message : result.getMessages()) { ???setError(fileName, message.getMessage()); ?} ?ledger.setEntryFile(null); } ... |
度量
在過去的幾年里,SLOC 度量方法被誤認為是高效率的象征。盡管我們都知道,并不一定是行數越多越好。但說到復雜度,SLOC 可是一個有用的度量方法。一個方法(或類)的行數越多,將來維護其代碼就可能越難。
工具
清單 8 中的腳本為長方法(大類)找到了 SLOC 度量方法:
清單 8. 識別過大的類和方法的 Gant 腳本
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | target(findLongMethods:"runs static code analysis"){ Ant.mkdir(dir:"target/reports") Ant.taskdef(name:"pmd", classname:"net.sourceforge.pmd.ant.PMDTask", ??classpathref:"build.classpath") Ant.pmd(shortFilenames:"true"){ ??codeSizeRules.each{ rfile -> ???ruleset(rfile) ??} ??formatter(type:"xml", tofile:"target/reports/pmd_report.xml") ??formatter(type:"html", tofile:"target/reports/pmd_report.html") ??fileset(dir:"src"){ ????include(name:"**/*.java") ??} ?}? } |
我又使用了 Gant 訪問 Ant API 來執行 Ant 任務。在清單 8 中,我調用 PMD 靜態分析工具來搜索代碼庫中的長方法。PMD(連同 JavaNCSS 與 CheckStyle)也可以用于查找長方法、大類以及其他代碼味道。
重構
清單 9 展示了用?Extract Method重構來減少?清單 7中的長方法代碼味道的一個例子。將清單 7 的方法中的行為提取到清單 9 的代碼中以后,我就可以從清單 7 的?saveLedgerInformation()方法中調用新建的?isUserValid()方法了:
清單 9. Extract Method 重構
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | private boolean isUserValid(User user) { ?boolean isValid = false; ?if (user.getLastName() != null) { ???SearchInfo searchInfo = ServiceLocator.getSearchInfo(); ???searchInfo.setLedgerInfo(null); ???setDefaultValues(); ???resetSearchInfo(); ???if (searchInfoValid && ledger != null) { ?????isValid = true; ???} ?} ?return isValid; } |
通常,長方法和大類也暗示著存在其他代碼味道,如條件復雜度和重復代碼。因此,找到這些長方法和大類也就可以修復其他的問題了。
太多導入
味道:太多導入
度量:傳出耦合(每個類的扇出(fan-out))
工具:CheckStyle
重構:Move Method、Extract Class
味道
太多導入表明一個類過多地依賴于其他的類。您會注意到,由于一個類與很多其他的類耦合得太緊密,修改這個類會導致必須對很多其他的類進行修改,這時就說明這個類存在這種代碼味道了。清單 10 中的多個導入就是一個例子:
清單 10. 一個類中的多個導入
| 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 | import com.integratebutton.search.SiteQuery; import com.integratebutton.search.OptionsQuery; import com.integratebutton.search.UserQuery; import com.integratebutton.search.VisitsQuery; import com.integratebutton.search.SiteQuery; import com.integratebutton.search.DateQuery; import com.integratebutton.search.EvaluationQuery; import com.integratebutton.search.RangeQuery import com.integratebutton.search.BuildingQuery; import com.integratebutton.search.IPQuery; import com.integratebutton.search.SiteDTO; import com.integratebutton.search.UrlParams; import com.integratebutton.search.SiteUtil; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.log4j.Logger; ... |
度量
找到帶有太多責任的類的一個方法就是通過?傳出耦合度量方法,亦指?扇出復雜度。扇出復雜度給被分析的類所依附的每一個類賦值 1。
工具
清單 11 展示了一個用 CheckStyle 設置最大扇出復雜度數的例子:
清單 11. 使用 CheckStyle 設置最大扇出復雜度
| 1 2 3 | <module name="ClassFanOutComplexity"> ?<property name="max" value="10"/> </module> |
Refactoring to Patterns
工廠方法模式是應用重構時可以實現的多種設計模式之一。用工廠方法創建類無需顯式定義正在創建的實際的類。這個模式可以使界面比實現類更簡單。當根據代碼味道實現重構時,您也可以使用其他的設計模式;參見?參考資料查看專門研究這個概念的書籍的鏈接。
重構
修復由于太多導入而引發的耦合過緊的方法有很多種。對于諸如?清單 10中那樣的代碼來說,可用的重構就包括?Move Method重構:將方法從單獨的 *Query類移動到 Java 界面,并定義所有?Query類必須實現的通用方法。然后再使用?工廠方法模式,這樣耦合度就與界面相關聯了。
通過使用 Gant 自動構建腳本執行 CheckStyle Ant 任務,我可以搜索代碼庫,查找過多依賴于其他類的類。當修改這些類中的代碼時,就能夠實現特定的重構(比如 Move Method)和特定的設計模式,以逐步改進可維護性。
重構……要盡早且要經常進行
持續集成(Continuous Integration,CI)就是經常集成變更。正如其典型的實現方式一樣,每當對項目的版本控制儲存庫做出一個更改時,運行于獨立機器上的自動 CI 服務器就會觸發一個自動構建。為了確保?清單 3和?清單 8中的腳本可以在對數據庫做出更改時一致地運行,您需要配置一個諸如 Hudsona 這樣的 CI 服務器(參見?參考資料)。Hudson 是以 WAR 文件的形式發布的,您可以將它放入任何 Java Web 容器中。
由于?清單 3和?清單 8中的例子使用了 Gant,下面我就簡要介紹一下配置 Hudson CI 服務器以運行 Gant 腳本的步驟:
配置 Hudson,使其運行使用 Gant 編寫的自動構建腳本。一旦諸如長方法和條件復雜度這樣的代碼味道被引入到代碼庫中,您立刻就會得到與它們相關的度量方法的反饋。
其他味道與重構
并非所有的味道都有相關的度量方法。但是,靜態分析工具能夠揭露的味道不止我所示范的這些。表 1 列舉了其他的代碼味道、工具、以及可能的重構例子:
表 1. 其他味道與重構
| 死代碼 | PMD | Remove Code |
| 臨時字段 | PMD | Inline Temp |
| 不一致 / 拘謹(uncommunicative)的名稱 | CheckStyle、PMD | Rename Method、Rename Field |
| 長參數列表 | PMD | Replace Parameter with Method、Preserve Whole Object、Introduce Parameter Object |
本文提供了一種使代碼味道與一種度量方法相關的模式,這種度量方法可以配置為通過自動靜態分析工具標記。您可以使用或不使用特定的設計模式來進行重構。這為您提供了一個以可重復的方式一致地查找和修復代碼味道的框架。我堅信本文的例子也有助于您使用靜態分析工具來查找本文未涉及到代碼味道。
相關主題
- 您可以參閱本文在 developerWorks 全球網站上的?英文原文。
- 讓開發自動化(Paul Duvall,developerWorks):閱讀整個系列的文章。“持續檢查”(2006 年 8 月)以及 “用 Eclipse 插件提高代碼質量”(2007 年 1 月)部分與本文的主題密切相關。
- Smells to Refactorings:一個表格,其中列出了特定的代碼味道的推薦重構方法。
- Refactoring:Improving the Design of Existing Code(Martin Fowler,Addison-Wesley Professional,1999 年):關于改進現有代碼庫設計的最基本的書籍。
- Alpha List of Refactorings:Martin Fowler 編寫的重構列表。
- Refactoring to Patterns(Joshua Kereviesky,Addison-Wesley Professional,2004 年):將設計模式應用于重構來改進代碼。我是在聽了 Josh 在丹佛的 Agile 2005 上的講話后才有了將一個大的類打印出來的想法的。
- “追求代碼質量:監視圈復雜度”(Andrew Glover,IBM developerWorks,2006 年 3 月):介紹當代碼復雜度太高時該怎么辦。
- “追求代碼質量:用代碼度量進行重構”(Andrew Glover,IBM developerWorks,2006 年 5 月):用代碼度量和 Extract Method 模式進行有目的地重構。
- “Continuous Integration: Improving Software Quality and Reducing Risk”(Paul Duvall 等,Addison-Wesley Signature Series,2007 年):第 7 章(持續檢查)涵蓋了本文中所涉及的很多工具。
- “(Ant to Gant) automagically”(Andrew Glover,The Disco Blog,2008 年 4 月):基于現有 Ant 腳本生成 Gant 腳本。
- “Gant with Hudson in 5 steps”(Andrew Glover,The Disco Blog,2008 年 5 月):配置 Hudson 持續集成服務器,以運行 Gant 構建腳本。
- “追求代碼質量:軟件架構的代碼質量”(Andrew Glover,IBM developerWorks,2006 年 4 月):使用耦合度量支持系統架構。
- Gant:下載 Gant,開始以一種可預測、可重復的方式構建軟件。
- CheckStyle:下載 CheckStyle,搜集度量并更好地評估代碼味道。
- PMD:下載 PMD,搜集度量并更好地評估代碼味道。
- Hudson:一個免費且開源的持續集成服務器。
- developerWorks Java 技術專區:這里有數百篇關于 Java 編程的文章。
總結
以上是生活随笔為你收集整理的让开发自动化持续重构 --使用静态分析工具识别代码味道的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 第三部分:Idea重构总结
- 下一篇: 31 天重构学习笔记索引