「重構是一種對軟體內部結構的改善,目的是在不改變軟體的可見行為的情況下,使其更易理解,修改成本更低。」—— Martin Fowler

為什麼要重構?#

對專案的價值#

  1. 保持程式碼品質 - 防止程式碼腐化到無法維護
  2. 持續演進 - 無法一開始就設計完美,需求在變化
  3. 避免過度設計 - 遇到問題再重構,有的放矢

對個人的價值#

  1. 鍛煉設計能力 - 應用設計原則與模式的最佳練習場
  2. 成就感 - 將爛程式碼變好的滿足感
  3. 能力證明 - 重構能力是衡量程式設計師水平的有效指標

「初級工程師在維護程式碼,高級工程師在設計程式碼,資深工程師在重構程式碼」

重構什麼?#

大型重構#

對頂層設計的重構:

  • 系統架構
  • 模組劃分
  • 類別之間的關係
  • 程式碼分層

手段:分層、模組化、解耦、抽象可複用組件

特點

  • 改動範圍大
  • 影響面廣
  • 需要有計劃、有組織地進行
  • 需要資深工程師主導

小型重構#

對程式碼細節的重構:

  • 規範命名
  • 消除重複程式碼
  • 拆分過長的函式
  • 消除過大的類別

手段:應用編碼規範

特點

  • 改動集中
  • 風險較低
  • 隨時可做

什麼時候重構?#

不要等到程式碼爛到無法維護才重構!

持續重構#

將重構作為開發的一部分,而不是獨立的大規模活動。

flowchart TD
    A[日常開發流程] --> B[修改功能時]
    A --> C[Code Review 時]
    A --> D[閒暇時]

    B --> E[順手重構相關程式碼]
    C --> F[發現問題立即修正]
    D --> G[主動改善可最佳化的程式碼]

    E --> H[保持程式碼品質]
    F --> H
    G --> H

持續重構意識#

技術在更新、需求在變化、人員在流動,程式碼品質總會下降。時刻保持持續重構意識比任何技巧都重要。

如何重構?#

大型重構策略#

  1. 制定計劃 - 明確目標和範圍
  2. 分階段進行 - 每階段完成一小部分
  3. 保持可執行 - 每次提交程式碼都能正常執行
  4. 控制影響範圍 - 考慮兼容性,必要時寫過渡程式碼

每個階段最好控制在一天內完成,避免與新功能開發衝突。

小型重構策略#

  • 隨時進行
  • 可借助靜態分析工具(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;
    }
}

可測試性設計要點

  1. 使用依賴注入 - 便於替換為 mock 物件
  2. 避免靜態方法 - 或封裝成可注入的物件
  3. 避免全局變數 - 不可預測的狀態影響測試
  4. 避免在建構函式中執行複雜邏輯

解耦合技巧#

為什麼要解耦?#

高耦合程式碼的問題:

  • 改一處影響多處
  • 難以理解
  • 難以測試
  • 難以複用

解耦方法#

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 <- B

3. 依賴注入

// 耦合:內部創建依賴
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

  1. 只解決當前問題 - 預留擴展點,但不提前實作
  2. 簡單方案優先 - 能用簡單方式解決就不要複雜化
  3. 持續重構 - 需要時再演化設計

簡單與直觀是永恆的解決方案#

// 過度設計:為了可能的擴展引入工廠 + 策略
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 層
  • 難以為類別取名
  • 修改一處需要改多處
重構步驟
  1. 確保有測試覆蓋
  2. 小步修改
  3. 每步都能編譯執行
  4. 頻繁提交
  5. 保持行為不變
常見重構手法
手法適用情況
提取方法函式過長
內聯方法方法體比方法名更清晰
提取類別類別職責過多
內聯類別類別幾乎沒做什麼事
移動方法方法在錯誤的類別中
重命名名稱不能表達意圖
參數物件參數過多
使用多型取代條件大量 if-else 或 switch

破窗效應:一旦有人往專案裡堆砌爛程式碼,就會有更多人跟進。資深工程師要負起責任,保持程式碼品質處於良好狀態。