什麼是 Prefactoring?#
重構(refactoring)是在不改變外部行為的前提下,調整程式內部結構以提升品質。Prefactoring 則把這份「事後修正」前移:在開發新系統時,把自己過去的經驗、他人的經驗、以及重構過程中淬鍊出的洞察,提前納入設計判斷。
預重構同時強調「介面」的重要:思考元件能為你做什麼,而不是它如何運作。這也呼應重構——重構保護的正是介面:可以改變內部實作,卻維持外部行為不變。
本書並不主張預重構能讓你免於重構。新需求終究會浮現、決策也並非永遠正確。預重構的目標是利用既有經驗,盡量減少日後的修改成本。
三個「極致」核心概念#
書中的指南圍繞三個被推到極致的觀念,與極限編程(Extreme Programming)的精神相通——既然某件事是好事,就把它做到底,再從中找回適合自己的位置。
抽象(Abstraction)#
抽象描述「做什麼」(what)而非「怎麼做」(how)。
- 系統可以被描述得夠抽象,使其能透過手動、半自動或全自動流程實作。
- 過度抽象則會難以想像實際運作,需要原型(prototype)佐證。
- 範例指南:「不要用原始型別(int、double)描述概念」。
關注點分離(Separation of Concerns)#
把責任拆分到不同的類別、方法與變數中。以 Customer 為例,可以拆成:
CustomerData:只持有資料。CustomerPersistence:負責持久化。CustomerGUIDisplay:負責 GUI 顯示與輸入。CustomerImportExport:負責讀寫文字格式。CustomerModel:包含行為。CustomerBusinessRule:包含可變的業務規則。
不一定每個類別都要拆得這麼細,但要意識到「可以這樣拆」這個選項,才能在需要時做出正確抉擇。
可讀性(Readability)#
程式碼同時與電腦及讀者溝通。
- 程式碼應採用所有開發者都能理解的風格。
- 程式碼越貼近需求陳述,越容易驗證是否符合需求。
- 極致範例:「客戶或業主應該能讀懂你的程式碼」。
指南是什麼#
書中所有建議都以「指南」形式呈現,並有一條「例外指南」:每條指南都有例外,除了這條本身。
指南不是「最佳實踐」(best practices)。「最佳」只能在當下開發脈絡中判斷。指南只是適合該脈絡的「好實踐」建議。
許多指南是同一基本原則的不同呈現,原則之間也有取捨:
- 套用關注點分離通常會增加類別與方法數量。
- 一致性可能讓程式碼變多,卻能降低不同模組的學習成本。
- 強調介面與委派會增加大量「轉手用」(delegating)的方法。
脈絡決定一切(Context Is Everything)#
只有一條鐵律:沒有任何做法到處都適用,必須由你判斷某項實踐是否適合手上的應用。
- 一次性的轉換程式不需要厚重文件,但心律調節器的程式如果文件不全可能致命。
- 有些系統需要繁瑣的錯誤處理(航空電子、醫療裝置),有些則只需在伺服器無回應時顯示訊息即可。
- 自用的小腳本可以不用太認真預重構;多人使用時就應該注重輸入驗證與有意義的錯誤訊息。
- 接觸全新架構或技術時,先做端到端模型探索,能更快了解指南如何適配新環境。
把書中的指南視為起點,依循自己的問題空間調整出專屬的預重構準則,而不是照單全收。
透過回顧累積經驗#
指南源自經驗。每個專案結束後做技術回顧(retrospective),可以把這次的設計成敗轉化為下一輪的指南:
- 架構是否契合問題空間?哪裡可以做得不一樣?
- 一開始該問哪些問題?哪些需求變更導致設計大幅改動?
- 下次有哪些既有元件可以直接採用?
- 遇到 bug 時,除了修掉,還要分析「為什麼會發生」,例如「未驗證輸入」就成為新的指南項目。
回顧也不必等到專案結束。持續性的小型回顧,可以讓開發流程逐步精進。
開發脈絡:Sam 的 CD 出租店#
書中以 Sam 的 CD 出租店作為主線範例,採用敏捷流程(agile process)——迭代式(iterative)與漸進式(incremental)的綜合體:
- 從以使用案例(use cases)表達的需求開始。
- 每個使用案例的細節,等到要實作時才補齊。
- 先進行整體初步分析,產出大局架構(big picture),但不填上每個類別的細節。
- 完成第一組需求後再深入設計與實作。
對瀑布式工程師來說,這像是太早寫程式;對極限編程的擁護者來說,又像是過度分析。本書作者刻意採取折衷:早些把可運作的系統交到使用者手上,但仍要讓系統契合整體解決方案。
建立共通詞彙:A Rose by Any Other Name#
需求討論的第一個任務是讓客戶與開發者使用同一套詞彙。
- 範例:Sam 用「CD」一詞同時指實體光碟與發行版本。作者把它拆成
CDRelease(發行)與CDDisc(實體光碟)兩個概念。 - 命名是主觀的——只要客戶與開發者一致即可,與外部世界是否認同無關。
- 屬性名稱應與客戶習慣一致:客戶用全名就用全名,客戶用縮寫就用縮寫;如果開發者記不住縮寫,請客戶提供完整名稱。
A Rose by Any Other Name Is Not a Rose——為系統中的每個概念建立清晰、定義明確的名稱。
Splitters 比 Lumpers 更容易合併#
- 同名指兩個概念:容易造成混淆。
- 同概念兩個名字:較不致命,但仍嫌囉嗦(例如 Sam 把租借同時稱為「rent」與「checkout」)。
- 寫不出單句定義時,先用兩個名字;之後若發現多餘可宣告為同義詞。
將兩個名稱合併為一是簡單的全域取代;但把一個名稱拆成兩個,需要逐一檢視每處用途——所以「先拆後併」比「先併再拆」安全得多。
群聚資料以降低概念數(Clumping)#
當 Sam 描述客戶有家用地址與帳單地址,且兩者結構相同時,作者建議把它們抽象成 Address 類別:
class Address
{
String line1;
String line2;
String city;
String state;
String zip;
}- Clumping(群聚):把屬於同一概念的多個屬性合成一個有名字的概念,是抽象化技術。
- Lumping(混為一談):用同一名字指涉兩個不同概念,可能掩蓋重要區別。
- 群聚應同時考慮職責,否則會產生「資料污染」的類別——光有資料、沒有行為。
抽象資料型別(Abstract Data Types, ADT)#
在描述使用案例或建模時,避免使用原始型別。每種數值幾乎都能描述為一個 ADT:
- 價格:
Dollar、CurrencyUnits。 - 庫存數量:
Count。 - 折扣比例:
Percentage。 - 時間長度:
TimePeriod。 - 長度:
Length、LengthInMeters、LengthInInches。
ADT 的優點:
- 把焦點放在「能做什麼」而非「怎麼表示」。
- 可內建驗證(如
Count不可為負、Age範圍 0–150)。 - 可定義字串顯示格式(例如
Dollar自帶幣別符號與千分位)。 - 在 GUI 中可自動依型別產生對應控件(例如
FileName旁加上瀏覽按鈕)。
When You’re Abstract, Be Abstract All the Way——抽象就抽到底,不要用原始型別描述資料項目。先全部明確型別化,需要時再退回 implicit typing;反向走會非常痛苦。
字串不是萬能字串#
要避免「萬物皆 String」症候群,可以宣告 CommonString 代表沒有驗證、格式或語意的純字元集合,再進一步把:
state限制為合法縮寫的State。zipcode用ZipCode而非NumericString,把表現方式抽象掉。phoneNumber、socialSecurityNumber即使格式相似,仍是兩個語意不同的型別,不可混用。fileName、encodedWebString等也可建立各自的 ADT,附帶相應驗證。
避免常數魔法數字#
- 任何具語意的數值都應該命名(例:
Dollar RENTAL_LATE_FEE = 3.00;)。 - 例外:純粹的初始化值(如索引 0)通常不需要常數名。
- 若值會變動,放進設定機制(XML、資料庫、設定檔),程式只透過符號名稱查詢實際值。
Never Let a Constant Slip into Code——所有有意義的值都該有符號名稱。
Jerry Weinberg 觀察本章指南有一條共同主軸:不要丟棄資訊。把價格寫成
double而不是Dollar、把多概念硬塞同一類別,都是丟資訊的行為。
原型勝過千言萬語#
文字使用案例擅長描述邏輯,但圖像化原型能讓客戶更快看出介面是否合用:
- 原型可以發現遺漏的需求,引發客戶的回饋。
- 不要太早做出「完美」的 GUI 原型——使用者會誤以為系統幾乎完成了。
- 介面專家建議用白板、便利貼、或刻意「手繪感」工具(例如 Java 的 Napkin Look and Feel)來產出原型。

Figure 2-1. Rental screens
Prototypes Are Worth a Thousand Words——畫一張介面圖,比千字描述更有說服力。