證據要從佈丁裡吃出來#
設計再漂亮,也要在實際程式碼中被驗證。Tim 與作者在 Sam 系統的第一次釋出時,並沒有把所有細節分析得鉅細靡遺:
- 部分未定的細節對整體類別結構不會有影響,可以邊寫邊補。
- 設計刻意保持通用,可在多數物件導向語言中實作,甚至支援沒有繼承的「物件式」語言。
- 範例採 Java(因為自帶 GUI 庫),但程式風格能直接翻譯成 C++ 或 C#。
The Proof Is in the Pudding——一定要把設計餵進真實程式碼,才能驗證設計是否成立。
每次釋出都做回顧#
Perform a Retrospective After Each Release——檢視設計與設計過程,能讓下一輪釋出更好。
回顧的價值:
- 翻看設計日誌,找出當初被迫修改的決策;分析背後成因(資訊不足、誤解、為了搶速度)。
- 承認自己「為了趕進度走過捷徑」的地方,再判斷是否值得補回來。
- 不必等到釋出末才回顧;每個內部里程碑都可以做小型回顧。
「砍捷徑」會在短期帶來假性的速度感。長期來看,技術債就像不吃飯換來的工時——遲早要還。
沒有完美:Nothing Is Perfect#
Nothing Is Perfect——通常仍有更好的解,但你可以在「夠好」處收手。
實例:作者承認在處理 import 程式時違反了 prefactoring 態度——明知會被多個集合複製貼上,但因為 Java 當時不支援泛型,重寫前處理腳本反而會增加跑程式的門檻,於是接受了複製貼上。
收手的判斷:
- 程式仍然可運作。
- 重構成本目前明顯高於收益。
- 文件中註記,未來若擴增可優先處理。
練習與理論不總是吻合#
實作中會有與指南衝突的地方,但要知道:
- 意外耦合:作者用 Java 序列化做持久化,導致所有資料類別必須 implement
Serializable。改用資料庫就不必,但建立「資料對應持久化類別」的成本不一定值得。 - 懶人合併:
collectionsInitialize(customerFilename, cdDiscFilename, cdReleaseFilename)應該被拆成三個方法,但因為時間壓力被合併。事後可在回顧時補上。 - 詞彙混用:
late與overdue在程式中互換使用,雖然字典寫成同義詞,但在系統中其實有兩個語意(OverdueRental是逾期未還、LateReturnOfRental是回收後超期)。需要回頭把所有地方統一更名。
違反指南本身不是罪,遺忘記錄與不再回頭審視才是。
操作介面:對外與對測試的雙重門面#
com.samscdrental.controller 包含三個 Operations 類別作為 Façade:
RentalOperations:checkinCDDisc()、checkoutCDDisc()。MaintenanceOperations:負責資料載入。ReportOperations:報表產生。
這些 Façade:
- 切開顯示層與模型層,符合 Separate Concerns to Make Smaller Concerns。
- 為不同類型使用者準備不同的入口(測試介面、GUI 介面)。
- 方法的參數一律是 ADT,由 GUI 負責把字串轉成 ADT 並回報格式錯誤。
測試與生產的分離#
Test or Production: That Is the Question——所有「只在測試需要」的方法應放進測試介面,不該存在於生產介面。
TestOnlyOperations 介面包含:
collectionsClear():清空所有集合(生產不允許,多次確認也不該開放)。setStartTimeForRentalBackSomeDays():把租借時間倒推幾天,便於測逾期邏輯。
判斷:
- 若一個方法在生產情境中也有合理用途(如
isCDDiscRented()),可放到生產介面。 - 若加方法只是為了讓測試能跑,這通常是設計異味——好的設計通常自然好測。
為了測試而保留彈性#
Build Flexibility for Testing——設計時保留彈性,讓測試更易進行。
實踐:
- 集合介面允許替換實作。生產用資料庫、測試用記憶體 Java collection,避免測試啟動 DB 程序的負擔。
- 工廠或依賴注入讓「該用哪個實作」由設定決定,測試與生產可以共用同一份呼叫端。
- 強迫自己分離出可替換的元件,自然會逼出乾淨的「資料持久化層」。
多少測試才夠?#
區分「測試」與「除錯」:
- 測試:判斷系統是否符合需求;外部介面測試完整且通過,系統就算功能正確。
- 除錯:失敗時找出原因;內部測試多 → 可在 debug 時跳過已驗證的元件。
完整的內部測試不能完全取代逐步除錯——類別之間的互動仍可能藏 bug。但內部測試愈完整,定位 bug 的範圍愈快縮小。
設定(Configuration)#
Configuration 類別作為 Service Locator 的變體:
- 在中心點集中提供其他物件需要的設定(例如
DataAccessConfiguration)。 - 取得來源可以是程式內預設值、設定檔、資料庫——對使用者透明。
- 使用 DTO 作為設定資料載體,未來改變來源不影響上層程式。
偏離與錯誤的工程實踐#
第一次釋出時針對偏離與錯誤訊號約定一致風格:
- 每個失敗都被分類(檔案 IO 錯、屬性值錯、業務規則錯)。
- 例外階層分開設計,從頂層 Façade 一路傳到 GUI 時,使用者得到的是「能採取行動的訊息」。
- 匯入檔案類錯誤特別容易發生 → 集合層級處理檔案 IO 錯誤、屬性層級處理欄位格式錯誤。
發現了名詞模糊就分裂#
第一次釋出後的回顧發現:
late與overdue雖然在英文字典是同義詞,但系統意義不同。- 解法:拆成
OverdueRental與LateReturnOfRental,讓兩者意涵互不重疊。
A Rose by Any Other Name Is Not a Rose——意義不同就拆名稱;發現混用就馬上更名。
避免過早一般化#
Sam 系統最終可被泛化為「租借任意有 ID 的物件」,但作者警告:
Avoid Premature Generalization——先解決具體問題,再讓解決方案變通用。
理由:
- 一開始就追求「通用租賃系統」會被定價策略、租期計算、商品種類等無數變化拖住。
- 在 Sam 的具體場景中先把問題解透,累積經驗,後面再泛化(例如把
CDDisc改名RentableItem、CDRelease改名CatalogItem)就有實踐基礎。
接受 Trade-off 的工程現實#
務實意味承認:
- 時間與資源是工程約束,不是設計者能無限拉長的尺。
- 「可運作」是第一道門檻,「優雅」是第二層追求。
- 走過的捷徑都該記錄,未來才能定向打掃,而不是在一片混亂中重新設計。
務實指南總結#
| 指南 | 訊息 |
|---|---|
| The Proof Is in the Pudding | 設計要落地驗證 |
| Perform a Retrospective After Each Release | 每次釋出都做回顧 |
| Nothing Is Perfect | 在「夠好」處停手 |
| Test or Production: That Is the Question | 把測試專用方法放進測試介面 |
| Build Flexibility for Testing | 為可測性保留替換點 |
| Avoid Premature Generalization | 先解決具體問題再通用化 |