# 讓開發自動化: 持續重構
_使用靜態分析工具識別代碼味道_
重構是公認的改進現有代碼的好方法。然而,如何通過一種一致且可重復的方式 _找到_需要重構的代碼呢?本期的 [_讓開發自動化_](http://www.ibm.com/developerworks/cn/java/j-ap/)闡述了如何使用靜態分析工具來識別需要重構的代碼味道,并舉例說明了如何改進壞味道代碼。
在過去的幾年里,我曾看過很多項目的大量源代碼,從精美的設計到像是用膠帶綁定到一起的代碼。我寫過新的代碼也維護過其他開發人員的源代碼。我喜歡編寫新的代碼,但也喜歡采用一些現有的代碼,以某種方法將其簡化或將重復的代碼提取到一個公共類中。在我早期的工作生涯中,許多人都認為如果不編寫新的代碼就不會有好的效率。幸好,在 20 世紀 90 年代末,Martin Fowler 編寫了 _Refactoring_一書(參見 [參考資料](#resources)),它使得在不改變外部行為的前提下改進現有代碼成為可能。
我在 [本系列](http://www.ibm.com/developerworks/cn/java/j-ap/)中所一直推崇的就是 _效率_:如何減少耗時過程的冗余度,更快速地執行它們。在本文的任務中,我一樣推崇這個目標,_并且_將論述怎樣更 _有效地_執行它們。
## 關于本系列
作為開發人員,我們致力于為用戶自動化流程;但許多開發人員疏忽了自動化我們自己的開發流程的機會。為此,我們編寫了 [_讓開發自動化_](http://www.ibm.com/developerworks/cn/java/j-ap/)系列文章,專門探討軟件開發流程自動化的實踐應用,為您介紹 _何時_以及 _如何_成功應用自動化。
重構的一個典型方法是在引入新代碼或更改方法時對現有代碼做出小小的變動。該技巧面臨的挑戰在于一個開發團隊的開發人員的應用方法不一致,并且很容易錯失重構的機會。這也正是我提倡使用靜態分析工具識別編碼違規的原因所在。有了這些工具,您就能夠從總體上了解代碼庫,并且處于類或方法的級別。幸運的是,在 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**之類的重構,以此來除掉 **過多的導入**代碼味道
我將使用如下的通用格式來檢查每一種代碼味道:
1. 描述可以指示出代碼里面的問題的味道
2. 定義可以找到該味道的度量方法
3. 展示可以度量代碼味道的工具
4. 提供用于修復代碼味道的重構和模式(在某些情況下)
實質上,這個方法提供了一個找到和修復整個代碼庫中的代碼味道的一個框架。這樣您就可以更好地了解到代碼庫中較危險的部分,然后再做出更改。更好的是,我還會向您展示如何將這個方法集成到自動構建中。
## 您的代碼有 _味道_么?
所謂代碼味道其實只是一種 _提示_,提示一些內容可能存在錯誤。和模式類似,代碼味道提供了一個通用詞匯表,您可以用它來快速識別這些類型的潛在問題。在文章中 _真實_地示范代碼味道是很有難度的,因為它可能包括很多行代碼,這樣就過分地加大了文章的篇幅。因此,我會只針對其中的一些味道進行示范,然后您就可以根據查看特定代碼味道的經驗進行推斷,識別出剩余的代碼味道。
## 條件復雜度
**味道**:條件復雜度
**度量**:圈復雜度
**工具**:CheckStyle、JavaNCSS 以及 PMD
**重構**:Replace Conditional with Polymorphism、Extract Method
### 味道
_條件復雜度_可以以幾種不同的方式出現在源代碼中。這種代碼味道的一個例子就是含有多個條件語句,如 `if`、`while`或者 `for`語句。另一種條件復雜度是以 `switch`語句的形式呈現出來的,如清單 1 所示:
##### 清單 1\. 使用 `switch`語句來執行條件行為
```
...
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 的方法
```
<module name="CyclomaticComplexity">
<property name="max" value="10"/>
</module>
```
用清單 2 的 CheckStyle 規則文件、清單 3 的 Gant 例子來示范如何將 CheckStyle 作為一個自動構建的一部分來運行。(參見 [什么是 Gant ?](#whatisgant)側邊欄):
##### 清單 3\. 使用 Gant 腳本來執行 CheckStyle 檢查
```
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 構建軟件](http://www.ibm.com/developerworks/cn/edu/j-dw-java-gant-i.html)” 教程,了解 Gant。)
清單 3 中的 Gant 腳本創建了圖 1 中展示的 CheckStyle 報告。該圖下面的部分指示出了一個方法的 CheckStyle 圈復雜度違規。
##### 圖 1\. CheckStyle 報告根據過高的 CCN 來指示一種方法失敗

### 重構
圖 2 為用 UML 表示的 _Replace Conditional with Polymorphism_重構:
##### 圖 2\. 用多態替代條件語句

單擊 [此處](http://www.ibm.com/developerworks/cn/java/j-ap07088/figure2.html)查看完整圖。
在圖 2 中,我:
1. 創建了一個叫做 `BeerType`的 Java 界面
2. 定義了一個通用的 `showIngredients()`方法
3. 為每一個 `BeerType`創建了一個實現類
為了使文章保持簡潔,我僅為每一個類提供一個方法的實現。顯然,創建一個界面的方法可能不只一個。重構能夠使代碼更易于維護,如 Replace Conditional with Polymorphism 和 Extract Method(本文稍后將會討論)。
* * *
## 重復代碼
**味道**:重復代碼
**度量**:代碼重復率
**工具**:CheckStyle、PMD
**重構**:Extract Method、Pull Up Method、Form Template Method、Substitute Algorithm
### 味道
重復代碼可能在代碼庫中悄然發生。有時,復制粘貼某些代碼要比將該行為泛化到另一個類更簡單。但復制粘貼的方法存在一個問題,即它強制將代碼復制多份,并且需要維護。而且當復制出的代碼發生輕微的變化而引發行為不一致時,就會發生更不易察覺的問題,具體取決于哪個方法在執行該行為。清單 4 是一個關閉代碼庫連接的代碼示例,相同的代碼出現在兩種方法中:
##### 清單 4\. 重復代碼
```
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 行重復代碼
```
<module name="StrictDuplicateCode">
<property name="min" value="10"/>
</module>
```
清單 5 中的 `min`屬性設置了 CheckStyle 將會標記出的最小重復行數,以供查閱。在這樣的情況下,它將只指示出那些至少有 10 行類似或重復的代碼塊。
圖 3 展示了自動構建運行后,清單 5 中的模塊設置的結果:
##### 圖 3\. CheckStyle 報告指示代碼重復度過高

### 重構
在清單 6 中,我用了 [清單 4](#listing4)中的重復代碼,使用了 _Pull Up Method_重構來降低重復度 —將行為從較大方法提取到一個抽象類方法中:
##### 清單 6\. Pull Up Method
```
...
} 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\. 長方法代碼味道
```
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 腳本
```
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](#listing7)中的長方法代碼味道的一個例子。將清單 7 的方法中的行為提取到清單 9 的代碼中以后,我就可以從清單 7 的 `saveLedgerInformation()`方法中調用新建的 `isUserValid()`方法了:
##### 清單 9\. Extract Method 重構
```
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\. 一個類中的多個導入
```
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 設置最大扇出復雜度
```
<module name="ClassFanOutComplexity">
<property name="max" value="10"/>
</module>
```
## Refactoring to Patterns
_工廠方法_模式是應用重構時可以實現的多種設計模式之一。用工廠方法創建類無需顯式定義正在創建的實際的類。這個模式可以使界面比實現類更簡單。當根據代碼味道實現重構時,您也可以使用其他的設計模式;參見 [參考資料](#resources)查看專門研究這個概念的書籍的鏈接。
### 重構
修復由于太多導入而引發的耦合過緊的方法有很多種。對于諸如 [清單 10](#listing10)中那樣的代碼來說,可用的重構就包括 _Move Method_重構:將方法從單獨的 *`Query`類移動到 Java 界面,并定義所有 `Query`類必須實現的通用方法。然后再使用 [工廠方法](#patterns)模式,這樣耦合度就與界面相關聯了。
通過使用 Gant 自動構建腳本執行 CheckStyle Ant 任務,我可以搜索代碼庫,查找過多依賴于其他類的類。當修改這些類中的代碼時,就能夠實現特定的重構(比如 Move Method)和特定的設計模式,以逐步改進可維護性。
* * *
## 重構……要盡早且要經常進行
持續集成(Continuous Integration,CI)就是經常集成變更。正如其典型的實現方式一樣,每當對項目的版本控制儲存庫做出一個更改時,運行于獨立機器上的自動 CI 服務器就會觸發一個自動構建。為了確保 [清單 3](#listing3)和 [清單 8](#listing8)中的腳本可以在對數據庫做出更改時一致地運行,您需要配置一個諸如 Hudsona 這樣的 CI 服務器(參見 [參考資料](#resources))。Hudson 是以 WAR 文件的形式發布的,您可以將它放入任何 Java Web 容器中。
由于 [清單 3](#listing3)和 [清單 8](#listing8)中的例子使用了 Gant,下面我就簡要介紹一下配置 Hudson CI 服務器以運行 Gant 腳本的步驟:
1. 在 Hudson 的儀表板上為 Hudson 安裝 Gant 插件:首先選擇 **Manage Hudson**,再選擇 **Manage Plugins**,然后選擇 **Available**選項卡。在這個選項卡上選中 Gant 插件復選框,然后單擊 **Install**按鈕。
2. 重新啟動 Web 容器(例如,Tomcat)。
3. 選擇 **Manage Hudson**,然后選擇 **Configure System**。在 **Gant installation**部分,鍵入一個惟一的名稱以及 Groovy 安裝到運行 Hudson 的機器上的位置。保存所作更改。
4. 返回到儀表板(選擇 **Hudson**鏈接),選擇一個 **existing Hudson Job**,再選擇 **Configure**,然后單擊 **Add build step**按鈕,選擇 **Invoke Gant script**選項。
配置 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 |
本文提供了一種使代碼味道與一種度量方法相關的模式,這種度量方法可以配置為通過自動靜態分析工具標記。您可以使用或不使用特定的設計模式來進行重構。這為您提供了一個以可重復的方式一致地查找和修復代碼味道的框架。我堅信本文的例子也有助于您使用靜態分析工具來查找本文未涉及到代碼味道。
- 讓開發自動化
- 讓開發自動化: 部署自動化模式,第 2 部分
- 讓開發自動化: 部署自動化模式,第 1 部分
- 讓開發自動化: 使用基于向導的安裝程序
- 讓開發自動化: 針對廣大開發人員的并行開發
- 讓開發自動化: 實現自動化數據庫遷移
- 讓開發自動化: 持續重構
- 讓開發自動化: 文檔化一鍵通
- 讓開發自動化: 利用 Ivy 管理依賴項
- 讓開發自動化: 自動負載測試
- 讓開發自動化: 使用自動化加速部署
- 讓開發自動化: 持續集成反模式
- 讓開發自動化: 斷言架構可靠性
- 讓開發自動化: 持續測試
- 讓開發自動化: 用 Eclipse 插件提高代碼質量
- 讓開發自動化: 除掉構建腳本中的氣味
- 讓開發自動化: 選擇持續集成服務器
- 讓開發自動化: 持續檢查