核心觀點#
Martin Fowler 定義 refactoring 為「在不改變外部行為的前提下,改善程式碼內部結構的過程」。本章透過一個具體的範例——質數產生器(Generating Primes)– 示範了完整的重構流程。
重點: 每個軟體模組都有三個功能:(1) 執行時的功能、(2) 容許變更的能力、(3) 與讀者溝通的能力。難以變更的模組是壞的,無法讓人理解的模組也是壞的,即使它能正確運作。
為何需要 Refactoring#
「如果沒壞,就不要修」這種想法忽略了程式碼的其他責任。僅靠原則和模式還不夠,讓模組容易閱讀和變更還需要:
- 注意力(attention)
- 紀律(discipline)
- 對創造美的熱情(a passion for creating beauty)
重構範例:質數產生器#
起點:Version 1(GeneratePrimes.cs)#
原始程式碼是一個使用 Sieve of Eratosthenes 演算法的大型單一函式,充斥著:
- 單字母變數名稱(
s、f、i、j) - 大量註解來彌補程式碼的不可讀性
- 所有邏輯塞在一個方法裡
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->UncrossIntegersUpToisCrossed->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。時間投入很小,但為自己和他人節省的未來成本巨大。
注意: 書中所有原則和模式,如果應用在混亂的程式碼上,都是徒勞的。在投資原則和模式之前,先投資在乾淨的程式碼上。