介面契約與驗證#

系統由介面(interface)構成。Bertrand Meyer 在《Object-Oriented Software Construction》中提出 Design by Contract:每個方法在被呼叫時都有前置條件(precondition)與後置條件(postcondition),介面像是設計給呼叫者看的合約。

Create Interface Contracts——以定義良好的介面進行設計,並在程式碼中強制執行其契約。

關鍵抉擇:

  • 誰負責檢查前置條件? 作者偏好由被呼叫方在自身程式中明確檢查;被呼叫方擁有最完整的條件知識,呼叫方擁有最完整的「為何違反」的脈絡。
  • 不變條件(invariant condition):必須在物件存活期間恆真,常涉及屬性的合理範圍。
  • 檢查方式:傳統 ifassert、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 的取捨