「寫作就是重寫」,代碼亦然。重構不是重寫,而是在保持外部行為不變的前提下,通過結構優化提升可維護性。本文基於圈複雜度等客觀度量指標,實戰演示「提取方法」模式如何將圈複雜度 23 的高風險方法拆解為可測試、可維護的小單元,並解決私有方法測試難題,構建「度量→識別→重構→驗證」閉環。
PMD 等靜態分析工具報告揭示高風險方法:
// 報告示例
Method 'updateContent' has cyclomatic complexity of 23 (threshold 10)
Class: AccountAction
Risk: High defect density, difficult to test (needs 23+ test cases)
圈複雜度本質:測試覆蓋所有路徑所需的最小測試用例數。複雜度 23 意味著需要 23+ 個獨立測試用例才能達到分支覆蓋,實際維護成本極高。
public void updateContent(Account account) {
// ... 大量其他代碼 ...
// 風險片段:狀態文本生成邏輯 (複雜度貢獻 +5)
String retstatus = null;
if (lastChangedStatus != null && lastChangedStatus.size() > 0) {
if (status.getId() == ((IStatus)lastChangedStatus.get(0)).getId()) {
retstatus = "Change in Current status";
} else {
retstatus = "Account Previously Changed in: " +
((IStatus)lastChangedStatus.get(0)).getStatusIdentification();
}
} else {
retstatus = "No Changes Since Creation";
}
// ... 後續使用 retstatus ...
}
// 原方法調用點簡化
String statusText = getStatusText(status, lastChangedStatus);
// 新增獨立方法:單一職責、易測試、可復用
private String getStatusText(IStatus status, List lastChangedStatus) {
if (lastChangedStatus != null && !lastChangedStatus.isEmpty()) {
IStatus lastStatus = lastChangedStatus.get(0);
if (status.getId() == lastStatus.getId()) {
return "Change in Current status";
} else {
return "Account Previously Changed in: " + lastStatus.getStatusIdentification();
}
}
return "No Changes Since Creation";
}
| 指標 | 重構前 | 重構後 | 改善 |
|---|---|---|---|
| updateContent 圈複雜度 | 23 | 18 | -5 (22% 降低) |
| getStatusText 圈複雜度 | N/A | 3 | 低風險 |
| 測試用例數需求 | 23+ | 18+3=21 | 結構更清晰,單元測試更易編寫 |
| 方法行數 | ~80 | ~60 + 15 | 單屏可讀 |
提取出的 `getStatusText` 為 `private`,JUnit 無法直接調用。四種解決方案對比:
| 方案 | 做法 | 優點 | 缺點 | 推薦場景 |
|---|---|---|---|---|
| 改為 public | 修改訪問修飾符 | 簡單直接 | 破壞封裝,污染 API | 極少數工具方法 |
| 改為 protected | 測試類同包繼承 | 保持封裝性 | 測試代碼侵入主代碼包 | 傳統 JUnit 3 風格 |
| 內部類測試 | 主類內部定義 Test 類 | 可訪問私有成員 | 污染生產代碼結構 | 不推薦 |
| PrivateAccessor (推薦) | 反射調用 + JUnit 插件 | 零侵入、框架無關、支持參數化 | 反射性能損耗、安全管理器限制 | 標準選擇 |
import junitx.util.PrivateAccessor; // JUnit-addons 模塊
public class AccountActionTest {
@Test
public void testGetStatusText_NoChanges() throws Throwable {
AccountAction action = new AccountAction();
// 反射調用私有方法
String result = (String) PrivateAccessor.invoke(action,
"getStatusText",
new Class[]{IStatus.class, List.class}, // 參數類型
new Object[]{null, null} // 參數值
);
assertEquals("No Changes Since Creation", result);
}
@Test
public void testGetStatusText_SameStatus() throws Throwable {
AccountAction action = new AccountAction();
IStatus status = mock(IStatus.class);
when(status.getId()).thenReturn(1L);
IStatus lastStatus = mock(IStatus.class);
when(lastStatus.getId()).thenReturn(1L);
List list = Arrays.asList(lastStatus);
String result = (String) PrivateAccessor.invoke(action,
"getStatusText",
new Class[]{IStatus.class, List.class},
new Object[]{status, list}
);
assertEquals("Change in Current status", result);
}
}
// 靜態調用 (私有靜態方法)
Object result = PrivateAccessor.invoke(AccountAction.class, "staticMethod", paramTypes, paramValues);
// 實例調用 (私有實例方法) - 最常用
Object result = PrivateAccessor.invoke(instance, "methodName", paramTypes, paramValues);
// 設置私有字段
PrivateAccessor.setField(instance, "fieldName", value);
// 獲取私有字段
Object value = PrivateAccessor.getField(instance, "fieldName");
| 指標 | 工具 | 閾值建議 | 觸發動作 |
|---|---|---|---|
| 圈複雜度 | PMD, SonarQube, CodeClimate | > 10 | 提取方法/策略模式 |
| 方法行數 | Checkstyle, SonarQube | > 30 行 | 提取方法/拆分類 |
| 參數數量 | PMD | > 4 個 | 引入 Parameter Object |
| 類行數 | Checkstyle | > 500 行 | 拆分職責/提取類 |
| 重複代碼 | CPD, SonarQube | > 6 行重複 | 提取公共方法/模板方法 |
# Maven PMD 插件配置片段
org.apache.maven.plugins
maven-pmd-plugin
/rulesets/java/design.xml
1.8
true
5 // 閾值 10
重構是對抗代碼熵增的唯一武器。通過圈複雜度等客觀指標識別風險熱點,應用「提取方法」等經典模式進行微創手術,配合 PrivateAccessor 解決私有方法測試難題,納入 CI 門禁形成「度量→重構→驗證」閉環,才能在交付壓力下保持代碼庫健康。記住:小就是美,測試是盾,度量是眼。