Martin Fowler 等:重构——改善既有代码的设计@2003 (第1版 侯捷译)

重構的核心思想,如本書作者 Martin Fowler 與其他貢獻者以及譯者侯捷、熊節所闡述,是在不改變既有程式碼外在行為的前提下,有紀律地調整其內部結構,以提升軟體的品質、可理解性、可維護性及可擴充性。這就像鐵道工人(道班工人)在不中斷交通的情況下,透過持續的「砸道」工作,讓路基上的碎石更緊密,從而加強鐵道的穩固性一樣。重構不是推倒重來,而是在現有基礎上逐步改善,為未來的修改和擴充做好準備。

重構的目的並非僅止於美學層面,儘管好的設計往往伴隨良好的可讀性。更重要的是,缺乏重構會導致軟體設計逐漸腐敗變質。當開發者為了快速達成短期目標,或是在對系統缺乏全面理解的情況下修改程式碼,就會引入重複、複雜且難以理解的程式碼,使得後續的修改變得越來越困難,引入臭蟲的風險也隨之增加。重構的價值在於透過消除重複程式碼、分解冗長的函式、改善類別之間的職責劃分等方式,使程式碼結構更清晰、邏輯更集中、依賴關係更簡潔,從根本上降低軟體的修改成本,並能更容易地發現和修復潛藏的臭蟲。

本書特別強調重構與軟體開發過程的整合,而非視其為一項獨立的任務。如同 Kent Beck 所提出的「兩頂帽子」比喻,開發者應在「添加新功能」和「重構」兩種模式之間切換。添加新功能時專注於實現需求,通過測試驗證功能;重構時則專注於改善內部結構,確保不改變外在行為。這種頻繁的切換(有時僅需十分鐘)能夠確保在功能開發的同時,持續改善程式碼的品質。

至於何時進行重構,作者群提出了幾種常見的時機:
1. 三次法則 (The Rule of Three): 當第三次重複做某件類似的事情時,就應該考慮重構。這是一種經驗法則,提醒開發者重複往往是潛在設計問題的徵兆。
2. 添加功能時: 如果現有程式碼結構不利於方便地添加新功能,應先重構程式碼使其易於修改,再添加功能。這是最常見的重構動機之一,因為重構能幫助開發者更好地理解既有程式碼,並規劃新功能的最佳整合方式。
3. 修補錯誤時: 在偵錯過程中,重構可以幫助開發者更深入地理解程式碼的運作方式和潛藏的假設,往往能在理清結構的過程中找出臭蟲。將重構視為理解程式碼和偵錯的手段。
4. 程式碼複審時: 程式碼複審不僅是傳播知識和提升品質的活動,也是找出需要重構之處的好時機。複審者和原作者可以共同討論並即時進行重構,使複審建議得以迅速落地。

進行重構最關鍵的基礎是擁有可靠的測試體系。作者們認為,良好的測試(特別是具備自我檢查能力的自動化測試)是重構的根本保障。測試能夠在每次微小的修改後迅速驗證程式碼行為是否被破壞,從而大大降低引入臭蟲的風險並縮短偵錯時間。這種「小步修改,頻繁測試」的節奏,正是重構得以安全有效進行的關鍵。許多開發者抗拒寫測試,認為這是額外工作,但本書作者強調,良好的測試實際上能極大地提升開發速度,因為它減少了花費在偵錯上的時間。作者們推薦使用 JUnit 這樣的測試框架來支援單元測試,並強調應優先測試那些複雜或容易出錯的部分(如邊界條件和異常情況)。

關於重構與軟體設計的關係,本書提出了一種新的觀點:重構改變了「預先設計」(upfront design)的角色。傳統觀念認為必須先有一個完善的設計才能開始編碼,但有了重構,開發者可以在對問題理解尚不完全透徹時先採用簡單合理的設計方案,然後在開發過程中隨著對問題的理解加深,逐步透過重構來演進和完善設計。這降低了「預先設計」的壓力,也避免了過早考慮所有潛在變化而導致過度工程(over-engineering)和不必要的複雜性。重構使得設計不再是一切動作的前提,而是在整個開發過程中逐漸湧現出來。

重構也可能引發一些實際挑戰。其中之一是對性能的擔憂。有些重構(如將臨時變數替換為查詢函數)可能會導致程式碼運行變慢,因為增加了函數呼叫或重複計算。然而,作者認為不應過早擔心性能問題。大多數情況下,這些微小的性能損失可以忽略不計。如果性能確實受到影響,應在軟體開發後期,使用性能評測工具找出真正的性能熱點(hot spot),然後針對這些熱點進行優化。良好組織和結構化的程式碼,反而更容易被有效地優化。

另一個挑戰是與資料庫的交互。資料庫結構(schema)往往與程式碼緊密耦合,且修改資料庫通常涉及複雜耗時的資料遷移。重構程式碼時,可能需要同時修改資料庫結構,這增加了重構的難度。一種可能的解決方案是在物件模型和資料庫模型之間引入一個分隔層(separate layer),以隔離兩者的變化。

修改接口是重構的另一個潛在難題。如果重構改變了被外部客戶使用的已發布接口(published interface),可能需要同時維護新舊接口一段時間,直到客戶完成遷移。作者建議應盡量避免過早發布過多接口,並調整程式碼所有權政策,鼓勵跨團隊協作修改程式碼以應對接口變動。

當然,重構並非萬靈丹。有時候,既有程式碼的混亂程度可能使得重寫(而非重構)成為更有效率的選擇。此外,如果項目即將迎來最後期限,可能不適合進行大規模重構,因為重構帶來的生產力提升往往需要時間才能顯現。作者將未能完成的重構工作比喻為「技術債務」,適度的債務可以接受,但過高的債務會壓垮項目。重構是償還技術債務的方式。

本書還探討了自動化重構工具的潛力。雖然手動重構是學習和實踐的基礎,但自動化工具可以極大地降低重構的成本和風險,使重構變得像調整程式碼格式一樣容易。這樣的工具能夠快速分析程式結構、找出重構機會、並安全地執行許多瑣碎的重構步驟,甚至能提供撤銷功能,鼓勵開發者以更探索性的方式進行設計。作者認為,一旦自動化工具普及,重構將更深度地融入日常開發流程,編程與重構之間的界線將更加模糊。

總結來說,本書的核心論點在於:重構是一項持續的、有紀律的實踐,是改善軟體設計、提升程式碼可讀性、更快地開發軟體以及更容易地找到和修復臭蟲的關鍵。它需要穩固的測試基礎、小步前進的方法論,並應被視為日常開發的一部分,而非獨立的任務。儘管可能面臨性能、資料庫、接口等挑戰,且某些核心設計難以完全透過重構改變,但在大多數情況下,重構所帶來的好處遠遠超過其成本,並能幫助開發者更靈活地面對軟體需求的變化。這本書為開發者提供了一套具體的技術詞彙(重構手法名錄)和實踐指南,使其能夠系統性地應用重構,從而成為更優秀的軟體開發者。