證據要從佈丁裡吃出來#

設計再漂亮,也要在實際程式碼中被驗證。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) 應該被拆成三個方法,但因為時間壓力被合併。事後可在回顧時補上。
  • 詞彙混用lateoverdue 在程式中互換使用,雖然字典寫成同義詞,但在系統中其實有兩個語意(OverdueRental 是逾期未還、LateReturnOfRental 是回收後超期)。需要回頭把所有地方統一更名。

違反指南本身不是罪,遺忘記錄與不再回頭審視才是。

操作介面:對外與對測試的雙重門面#

com.samscdrental.controller 包含三個 Operations 類別作為 Façade:

  • RentalOperationscheckinCDDisc()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 錯誤、屬性層級處理欄位格式錯誤。

發現了名詞模糊就分裂#

第一次釋出後的回顧發現:

  • lateoverdue 雖然在英文字典是同義詞,但系統意義不同。
  • 解法:拆成 OverdueRentalLateReturnOfRental,讓兩者意涵互不重疊。

A Rose by Any Other Name Is Not a Rose——意義不同就拆名稱;發現混用就馬上更名。

避免過早一般化#

Sam 系統最終可被泛化為「租借任意有 ID 的物件」,但作者警告:

Avoid Premature Generalization——先解決具體問題,再讓解決方案變通用。

理由:

  • 一開始就追求「通用租賃系統」會被定價策略、租期計算、商品種類等無數變化拖住。
  • 在 Sam 的具體場景中先把問題解透,累積經驗,後面再泛化(例如把 CDDisc 改名 RentableItemCDRelease 改名 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先解決具體問題再通用化