「重構是一種對軟體內部結構的改善,目的是在不改變軟體的可見行為的情況下,使其更易理解,修改成本更低。」—— Martin Fowler
為什麼要重構?#
對專案的價值#
- 保持程式碼品質 - 防止程式碼腐化到無法維護
- 持續演進 - 無法一開始就設計完美,需求在變化
- 避免過度設計 - 遇到問題再重構,有的放矢
對個人的價值#
- 鍛煉設計能力 - 應用設計原則與模式的最佳練習場
- 成就感 - 將爛程式碼變好的滿足感
- 能力證明 - 重構能力是衡量程式設計師水平的有效指標
「初級工程師在維護程式碼,高級工程師在設計程式碼,資深工程師在重構程式碼」
重構什麼?#
大型重構#
對頂層設計的重構:
- 系統架構
- 模組劃分
- 類別之間的關係
- 程式碼分層
手段:分層、模組化、解耦、抽象可複用組件
特點:
- 改動範圍大
- 影響面廣
- 需要有計劃、有組織地進行
- 需要資深工程師主導
小型重構#
對程式碼細節的重構:
- 規範命名
- 消除重複程式碼
- 拆分過長的函式
- 消除過大的類別
手段:應用編碼規範
特點:
- 改動集中
- 風險較低
- 隨時可做
什麼時候重構?#
不要等到程式碼爛到無法維護才重構!
持續重構#
將重構作為開發的一部分,而不是獨立的大規模活動。
flowchart TD
A[日常開發流程] --> B[修改功能時]
A --> C[Code Review 時]
A --> D[閒暇時]
B --> E[順手重構相關程式碼]
C --> F[發現問題立即修正]
D --> G[主動改善可最佳化的程式碼]
E --> H[保持程式碼品質]
F --> H
G --> H持續重構意識#
技術在更新、需求在變化、人員在流動,程式碼品質總會下降。時刻保持持續重構意識比任何技巧都重要。
如何重構?#
大型重構策略#
- 制定計劃 - 明確目標和範圍
- 分階段進行 - 每階段完成一小部分
- 保持可執行 - 每次提交程式碼都能正常執行
- 控制影響範圍 - 考慮兼容性,必要時寫過渡程式碼
每個階段最好控制在一天內完成,避免與新功能開發衝突。
小型重構策略#
- 隨時進行
- 可借助靜態分析工具(CheckStyle、FindBugs、PMD)
- Code Review 時發現即修正
安全重構的保障:單元測試#
為什麼需要單元測試?#
重構改變的是程式碼結構,不改變行為。如何確保行為不變?
答案:充分的單元測試覆蓋
// 重構前:驗證 IP 地址
public boolean isValidIp(String ip) {
// 使用正則表達式
return ip.matches("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$");
}
// 重構後:使用更清晰的邏輯
public boolean isValidIp(String ip) {
String[] parts = ip.split("\\.");
if (parts.length != 4) return false;
for (String part : parts) {
int value = Integer.parseInt(part);
if (value < 0 || value > 255) return false;
}
return true;
}
// 單元測試確保行為一致
@Test
void testValidIp() {
assertTrue(isValidIp("192.168.1.1"));
assertTrue(isValidIp("0.0.0.0"));
assertTrue(isValidIp("255.255.255.255"));
assertFalse(isValidIp("256.1.1.1"));
assertFalse(isValidIp("1.2.3"));
}什麼是好的單元測試?#
FIRST 原則:
| 原則 | 說明 |
|---|---|
| Fast | 執行速度快 |
| Independent | 測試之間獨立 |
| Repeatable | 可重複執行 |
| Self-validating | 自動判斷成敗 |
| Timely | 及時編寫 |
提高程式碼可測試性#
flowchart LR
subgraph 難以測試
direction TB
T1[Transaction] -->|new| U1[UserRepo]
T1 -->|靜態呼叫| I1[IdGenerator]
T1 -->|全局變數| R1[Runtime]
end
subgraph 可測試
direction TB
T2[Transaction] -.->|注入| U2[UserRepo]
T2 -.->|注入| I2[IdValidator]
T2 -.->|注入| R2[MemoryChecker]
end
難以測試 -->|依賴注入重構| 可測試難以測試的程式碼:
public class Transaction {
public boolean execute(String id, Long buyerId) {
// 直接 new 物件,無法替換為 mock
User buyer = new UserRepo().getUser(buyerId);
// 直接呼叫靜態方法
if (!IdGenerator.isValid(id)) {
return false;
}
// 使用全局變數
if (Runtime.getRuntime().freeMemory() < 1024) {
return false;
}
return true;
}
}可測試的程式碼:
public class Transaction {
private UserRepo userRepo;
private IdValidator idValidator;
private MemoryChecker memoryChecker;
// 依賴注入
public Transaction(UserRepo userRepo,
IdValidator idValidator,
MemoryChecker memoryChecker) {
this.userRepo = userRepo;
this.idValidator = idValidator;
this.memoryChecker = memoryChecker;
}
public boolean execute(String id, Long buyerId) {
User buyer = userRepo.getUser(buyerId);
if (!idValidator.isValid(id)) {
return false;
}
if (!memoryChecker.hasEnoughMemory()) {
return false;
}
return true;
}
}可測試性設計要點:
- 使用依賴注入 - 便於替換為 mock 物件
- 避免靜態方法 - 或封裝成可注入的物件
- 避免全局變數 - 不可預測的狀態影響測試
- 避免在建構函式中執行複雜邏輯
解耦合技巧#
為什麼要解耦?#
高耦合程式碼的問題:
- 改一處影響多處
- 難以理解
- 難以測試
- 難以複用
解耦方法#
flowchart LR
subgraph 耦合
A1[模組 A] -->|直接依賴| B1[模組 B]
end
subgraph 解耦
A2[模組 A] --> I[介面層]
I --> B2[模組 B]
end
耦合 -->|重構| 解耦1. 封裝與抽象
// 耦合:直接使用實作類別
RedisClient client = new RedisClient();
// 解耦:使用介面
Cache cache = new RedisCache();2. 中間層
// 耦合:模組 A 直接依賴模組 B
A -> B
// 解耦:通過介面層
A -> Interface <- B3. 依賴注入
// 耦合:內部創建依賴
class OrderService {
private UserService userService = new UserService();
}
// 解耦:外部注入依賴
class OrderService {
private UserService userService;
public OrderService(UserService userService) {
this.userService = userService;
}
}4. 事件驅動
// 耦合:直接呼叫
orderService.createOrder();
paymentService.processPayment();
inventoryService.reduceStock();
// 解耦:發布事件
eventBus.publish(new OrderCreatedEvent());
// 各服務訂閱事件自行處理避免過度設計#
什麼是過度設計?#
- 為不存在的需求預先設計
- 為極小概率的場景做複雜處理
- 盲目應用設計模式
- 過度抽象和封裝
如何避免?#
YAGNI 原則:You Ain’t Gonna Need It
- 只解決當前問題 - 預留擴展點,但不提前實作
- 簡單方案優先 - 能用簡單方式解決就不要複雜化
- 持續重構 - 需要時再演化設計
簡單與直觀是永恆的解決方案#
// 過度設計:為了可能的擴展引入工廠 + 策略
interface PaymentStrategy { }
class PaymentStrategyFactory { }
class WechatPaymentStrategy implements PaymentStrategy { }
class AlipayPaymentStrategy implements PaymentStrategy { }
// 簡單直觀:目前只有一種支付方式
class PaymentService {
void pay(Order order) {
wechatApi.pay(order);
}
}
// 等需要多種支付方式時再重構重構實戰清單#
重構時機檢查
需要重構的信號:
- 函式超過 50 行
- 類別超過 500 行
- 參數超過 5 個
- 重複程式碼出現三次以上
- 深層嵌套超過 3 層
- 難以為類別取名
- 修改一處需要改多處
重構步驟
- 確保有測試覆蓋
- 小步修改
- 每步都能編譯執行
- 頻繁提交
- 保持行為不變
常見重構手法
| 手法 | 適用情況 |
|---|---|
| 提取方法 | 函式過長 |
| 內聯方法 | 方法體比方法名更清晰 |
| 提取類別 | 類別職責過多 |
| 內聯類別 | 類別幾乎沒做什麼事 |
| 移動方法 | 方法在錯誤的類別中 |
| 重命名 | 名稱不能表達意圖 |
| 參數物件 | 參數過多 |
| 使用多型取代條件 | 大量 if-else 或 switch |
破窗效應:一旦有人往專案裡堆砌爛程式碼,就會有更多人跟進。資深工程師要負起責任,保持程式碼品質處於良好狀態。