介面契約與驗證#
系統由介面(interface)構成。Bertrand Meyer 在《Object-Oriented Software Construction》中提出 Design by Contract:每個方法在被呼叫時都有前置條件(precondition)與後置條件(postcondition),介面像是設計給呼叫者看的合約。
Create Interface Contracts——以定義良好的介面進行設計,並在程式碼中強制執行其契約。
關鍵抉擇:
- 誰負責檢查前置條件? 作者偏好由被呼叫方在自身程式中明確檢查;被呼叫方擁有最完整的條件知識,呼叫方擁有最完整的「為何違反」的脈絡。
- 不變條件(invariant condition):必須在物件存活期間恆真,常涉及屬性的合理範圍。
- 檢查方式:傳統
if、assert、aspect-oriented programming 或語言原生機制皆可,但合約應在程式中與文件中可見。
Validate, Validate, Validate——在每個介面驗證輸入是否符合契約。
驗證的層次:
- 輸入應在進入系統時轉成對應的 ADT,例如把字串轉為
Dollar。失敗在轉換時就被攔下。 - 識別碼應自帶檢核(check digit),早期攔截最常見的輸入錯誤。
- 多層系統(layered system)在每層邊界依層間契約驗證。
- Web 系統雙端皆應驗證:伺服器端必做,瀏覽器端避免來回傳輸。
抓住大局#
Think About the Big Picture——系統內的決策應與大局保持一致。
「大局」涵蓋:
- 系統的整體架構與商業目的。
- 系統運作所在的更大環境(公司、部門、既有框架),它們可能已存在
Customer類別、共用元件或標準框架。
建立元件時不要把它們當成真空中的存在。元件就是要被嵌入大局裡使用,過度通用化(為了「未來可重用」)反而會讓基本需求複雜化。
一致即簡單#
Consistency Is Simplicity——一致的風格與解法可降低維護負擔。
實踐重點:
- 例外與錯誤的分類規則(哪些算契約失敗、哪些算意外狀況)需團隊共識。
- 呼叫者與被呼叫者的契約檢查責任要明確;十二個呼叫點一起做檢查,不如把檢查集中在被呼叫方。
- 開發環境(IDE、框架)的預設樣式可以成為一致性的基礎;除非有強烈理由,不要硬要對抗它。
預重構態度與 DRY#
Adapt a Prefactoring Attitude——重複發生之前先消除。Don’t Repeat Yourself(DRY)——每份知識在系統中只有單一、明確、權威的表述。
具體做法:
- 想複製貼上:先抽方法。
- 想寫長註解解釋如何運作:那段該成為獨立方法。
- 用模板或腳本維持類別/方法骨架一致。
- 用單一資料源衍生多種形式(例如以 XML 描述資料表,產生 SQL 與各語言的存取類別)。
紀錄假設與決策#
Document Your Assumptions and Your Decisions——保留日誌供回顧(retrospectives)使用。
理由:
- 程式碼說明 how,需求說明 what,但都不解釋 why。
- 當前的決策來自當前的假設;未來假設變了,沒有日誌就難以重新評估。
- 範例:Sam 表示不買多碟版專輯,因此
CDDisc採單一 ID。日後若需求變動,可回頭檢視假設是否仍成立。
真正的決策需要至少兩個選項。若你只想到一條路,那不是決策,是直覺。
偏離與錯誤的策略#
Decide on a Strategy to Deal with Deviations and Errors——明確區分「正常處理中可預期的偏離」與「環境失敗的錯誤」。
分類:
- 偏離(deviation):正常情境下可發生,例如客戶 ID 輸入錯誤;可建議使用者修正並繼續流程。
- 錯誤(error):不應發生,例如網路或伺服器崩潰;分為致命與非致命:
- 致命錯誤(out of memory、out of disk space)→ 上層介入或退出。
- 非致命錯誤(主網路斷)→ 在當下層處理(例如改走備援網路)。
實作要點:
- 偏離與錯誤的訊號可使用回傳碼或例外,但兩種應分屬不同例外階層,便於分辨預期與非預期條件。
- checked vs. unchecked exceptions:對「應終止」型的條件(DB 連不上)多用 unchecked。
- 抽象等級越往上,例外應越摘要:低層拋具體例外,高層只拋如
UnableToDeliver等抽象例外,細節寫在 explanation 中。
Report Meaningful User Messages——錯誤訊息要在「使用者能採取什麼行動」的脈絡下呈現,而不是把底層錯誤照搬出來。
Never Be Silent——遇到錯誤不要沉默,呼叫端有權知道方法做不到。
失敗距離(Failure Distance)#
bug 的設定點與爆發點距離越大,越難 debug:
- 同方法內的 null 設定,編譯器或靜態分析多半能警告。
- 跨多個方法後才使用,得逐一檢查中間程式碼。
- 用 ADT 與靜態型別在編譯期攔下「字串可亂塞」這類錯誤,是縮短失敗距離的好方法。
效能:先對再快#
Don’t Speed Until You Know Where You Are Going——讓系統先正確,再讓它快。
實作哲學:
- 用 profiler 而非直覺。Jim Batterson 觀察:「90% 的時間花在 10 行程式上。」
- 性能瓶頸通常源於演算法選擇(quicksort vs. bubble sort),改演算法的回報遠超過微優化。
- 抽象層次過多會帶來呼叫成本——OSF Unix 的虛擬記憶體層曾因抽象造成嚴重瓶頸,最後直接使用處理器硬體取代。
- 「避免過早優化」不等於可以選擇平方時間複雜度的演算法。
試算表困境(The Spreadsheet Conundrum)#
許多設計抉擇本質都是「以列為主還是以欄為主」的取捨。
The Spreadsheet Conundrum——意識到自己正在做 row/column 的取捨。
範例:
- 形狀繪圖:
shape.draw(context)vs.context.draw(shape)。前者新增形狀容易,後者新增裝置容易。 - 多語言字串資源:依語言儲存利於新增語言,依資源儲存利於新增資源。
- 電話撥號:
phone.dial(number)vs.number.dial(phone),後續加上AccountNumber時影響的修改範圍不同。

Figure 3-1. Spreadsheet of CDDiscs and days

Figure 3-2. Spreadsheet of resources and languages

Figure 3-3. Graphics spreadsheet
沒有絕對答案。把選擇與理由寫進設計日誌,未來情境改變時可回頭重新評估。
工具用得明智#
工具能自動化、確保一致性與完整性,但選擇仍要基於成本:
- 與 IDE 合作而非對抗:IDE 預設的事件處理樣式雖未必最美,但讓全隊一致是最大價值。
- 多工具使用:用對工具能省力(Perl 處理字串、XSLT 轉 XML、Crystal Reports 跑報表);但若該功能在系統中只佔極小份額,沿用主語言反而能減少維護負擔。
- 學習新工具的時機:當需求量到達可攤提學習成本時。
一般開發指南總結#
| 指南 | 訊息 |
|---|---|
| Create Interface Contracts | 介面有契約,並在程式碼中強制 |
| Validate, Validate, Validate | 每個介面都驗證輸入 |
| Think About the Big Picture | 決策要與大局一致 |
| Consistency Is Simplicity | 一致的解法易維護 |
| Adapt a Prefactoring Attitude / DRY | 預先消除重複 |
| Document Your Assumptions and Your Decisions | 寫下 why |
| Decide on a Strategy to Deal with Deviations and Errors | 區分偏離與錯誤 |
| Report Meaningful User Messages | 訊息要說使用者能做什麼 |
| Never Be Silent | 不要沉默 |
| Don’t Speed Until You Know Where You Are Going | 先對再快 |
| The Spreadsheet Conundrum | 認得 row/column 的取捨 |