核心觀點#

Martin Fowler 定義 refactoring 為「在不改變外部行為的前提下,改善程式碼內部結構的過程」。本章透過一個具體的範例——質數產生器(Generating Primes)– 示範了完整的重構流程。

重點: 每個軟體模組都有三個功能:(1) 執行時的功能、(2) 容許變更的能力、(3) 與讀者溝通的能力。難以變更的模組是壞的,無法讓人理解的模組也是壞的,即使它能正確運作。

為何需要 Refactoring#

「如果沒壞,就不要修」這種想法忽略了程式碼的其他責任。僅靠原則和模式還不夠,讓模組容易閱讀和變更還需要:

  • 注意力(attention)
  • 紀律(discipline)
  • 對創造美的熱情(a passion for creating beauty)

重構範例:質數產生器#

起點:Version 1(GeneratePrimes.cs)#

原始程式碼是一個使用 Sieve of Eratosthenes 演算法的大型單一函式,充斥著:

  • 單字母變數名稱(sfij
  • 大量註解來彌補程式碼的不可讀性
  • 所有邏輯塞在一個方法裡
public class GeneratePrimes
{
  public static int[] GeneratePrimeNumbers(int maxValue)
  {
    if (maxValue >= 2)
    {
      int s = maxValue + 1;
      bool[] f = new bool[s];
      // ... 所有邏輯在一個巨大函式中
    }
    else
      return new int[0];
  }
}

第一步:確保有測試保護#

重構前先確認有充分的測試。測試採用統計方式,分別檢查產生 0、2、3、100 以內的質數:

[TestFixture]
public class GeneratePrimesTest
{
  [Test]
  public void TestPrimes()
  {
    int[] nullArray = GeneratePrimes.GeneratePrimeNumbers(0);
    Assert.AreEqual(nullArray.Length, 0);

    int[] centArray = GeneratePrimes.GeneratePrimeNumbers(100);
    Assert.AreEqual(centArray.Length, 25);
    Assert.AreEqual(centArray[24], 97);
  }
}

重構步驟#

Step 1:提取方法(Extract Method)

將單一大函式拆為三個職責清楚的方法,並更改類別名稱為 PrimeGenerator

  • InitializeSieve(maxValue)——初始化
  • Sieve()——執行篩法
  • LoadPrimes()——載入結果

提取函式自然迫使某些區域變數升級為靜態欄位,使變數的作用範圍更加清晰。

Step 2:改善命名(Rename)

  • 移除多餘的 s 變數,改用 f.Length
  • InitializeSieve -> InitializeArrayOfIntegers -> UncrossIntegersUpTo
  • isCrossed -> crossedOut(避免雙重否定的困擾)
  • LoadPrimes -> PutUncrossedIntegersIntoResult

技巧: 有了 refactoring 工具(如 ReSharper),這類命名修改幾乎零成本。即使沒有工具,簡單的搜尋取代加上測試也能確保安全。

Step 3:提取內層迴圈

  • CrossOutMultiples 中的內層迴圈提取為 CrossOutputMultiplesOf(int i)
  • if (isCrossed[i] == false) 改為 if (NotCrossed(i)),提升可讀性

Step 4:修正錯誤的命名與概念

作者發現 maxPrimeFactor 這個名稱有誤——陣列大小的平方根並不一定是質數。重新命名為 DetermineIterationLimit(),並撰寫正確的註解解釋為何只需迭代到平方根。

同時移除了一個不必要的 +1,並因為對正確性感到不安而增加了一個更嚴格的測試:

[Test]
public void TestExhaustive()
{
  for (int i = 2; i<500; i++)
    VerifyPrimeList(PrimeGenerator.GeneratePrimeNumbers(i));
}

Step 5:最終審閱(The Final Reread)

從頭到尾像閱讀幾何證明一樣通讀整個程式。這是重要的步驟——之前都在重構片段,現在要確認整體是否作為一個可讀的整體連貫一致。

flowchart TD
    A([確保有測試保護]) --> B["Step 1: 提取方法<br/>Extract Method"]
    B --> C["Step 2: 改善命名<br/>Rename"]
    C --> D["Step 3: 提取內層迴圈<br/>Extract Inner Loop"]
    D --> E["Step 4: 修正錯誤的<br/>命名與概念"]
    E --> F["Step 5: 最終審閱<br/>Final Reread"]
    F --> G([完成重構])

最終版本#

public class PrimeGenerator
{
  private static bool[] crossedOut;
  private static int[] result;

  public static int[] GeneratePrimeNumbers(int maxValue)
  {
    if (maxValue < 2)
      return new int[0];
    else
    {
      UncrossIntegersUpTo(maxValue);
      CrossOutMultiples();
      PutUncrossedIntegersIntoResult();
      return result;
    }
  }
  // ... 每個 private 方法都職責單一、命名清晰
}

補充: 提取只被呼叫一次的函式可能稍微影響效能,但在絕大多數情況下,增加的可讀性值得那幾奈秒的代價。如果真的在效能瓶頸的深層迴圈中,再考慮效能即可。

結論#

重構後的程式不僅更好讀,也更好改,而且結構上各部分彼此隔離,更容易變更。

重點: Refactoring 就像飯後洗碗。第一次跳過清理,確實能更快完成晚餐。但碗盤堆積會讓下次準備更花時間。最終你會花大量時間清理頑固的污垢。不清理其實並不會讓事情更快。

作者強調,應該對你寫和維護的每一個模組都進行 refactoring。時間投入很小,但為自己和他人節省的未來成本巨大。

注意: 書中所有原則和模式,如果應用在混亂的程式碼上,都是徒勞的。在投資原則和模式之前,先投資在乾淨的程式碼上。