Nothing astonishes men so much as common sense and plain dealing.

— Ralph Waldo Emerson, Essays

核心概念#

與電腦系統打交道很難,與人打交道更難。但作為一個物種,我們已經花了數千年時間來解決人際互動的問題。其中一個確保「公平交易」的最佳解決方案就是合約(contract)

合約定義了你的權利和責任,以及對方的權利和責任。此外,如果任何一方未能遵守合約,還有事先約定的補救措施。我們可以將同樣的概念應用於軟體模組之間的互動。

Design by Contract (DBC)#

Bertrand Meyer 為 Eiffel 語言開發了 Design by Contract 的概念。它是一種簡單而強大的技術,專注於記錄(並同意)軟體模組的權利和責任,以確保程式的正確性。

什麼是正確的程式?就是不多不少、恰好做到它宣稱要做的事情。記錄並驗證這個宣稱,就是 DBC 的核心。

三大組成要素#

軟體系統中的每個函式都做某件事。在開始執行之前,函式可能對世界的狀態有某些期待,完成後也能對世界的狀態做出某些聲明:

要素說明
前置條件(Preconditions)呼叫該常式之前必須為真的條件。這是呼叫者的責任——傳入合法的資料。前置條件被違反時,常式不應該被呼叫
後置條件(Postconditions)常式保證完成後的狀態。後置條件的存在也暗示常式一定會結束——不允許無限迴圈
類別不變量(Class Invariants)從呼叫者的角度來看,類別確保某個條件永遠為真。在內部處理期間不變量可能暫時不成立,但當常式結束、控制權回到呼叫者時,不變量必須恢復

合約的運作方式#

常式與任何潛在呼叫者之間的合約可以這樣理解:

如果呼叫者滿足了所有前置條件,常式就保證所有後置條件和不變量在完成時為真。

如果任何一方未能履行合約條款,則會觸發事先約定的補救措施——可能是拋出例外,或程式終止。無論如何,未能履行合約就是一個 bug。

Tip 37 - Design with Contracts(用合約來設計)

「懶惰」的程式碼#

在 Topic 10 Orthogonality 中,書中建議撰寫「害羞」的程式碼。在合約式設計中,重點在於「懶惰」的程式碼:在開始之前對接受的條件要嚴格,承諾的回傳要盡可能少。 如果你的合約表示你什麼都接受、什麼都承諾,那你就有非常多的程式碼要寫了!

語言支援#

原生支援的語言#

  • Clojure:支援 pre- 和 post-conditions,以及 specs 提供的更全面的檢測工具
  • Elixir:使用 guard clauses 來分派函式呼叫到不同的函式體,如果參數不符合任何 guard clause,直接得到 FunctionClauseError

不支援 DBC 的語言#

在不支援 DBC 的語言中,DBC 是一種設計技術。即使沒有自動檢查,你也可以將合約放在程式碼的註解中或單元測試中,仍然能獲得很大的好處。

Assertions 的限制#

用 assertions 可以部分模擬 DBC,但有幾個限制:

  • 在物件導向語言中,可能無法將 assertions 沿繼承層級傳播
  • 沒有內建的「舊值」概念(即方法進入時的值)
  • 傳統的執行時期系統和函式庫並非設計來支援合約

DBC 與 TDD 的比較#

DBC 和測試是解決程式正確性這個更大議題的不同方法。DBC 相較於測試方法有幾個優勢:

  • DBC 不需要任何設置或 mocking
  • DBC 定義了所有情況下的成功或失敗參數,而測試一次只能針對一個特定案例
  • TDD 只在 build cycle 的「測試時期」發生,但 DBC 和 assertions 是永久的:設計、開發、部署、維護時都存在
  • DBC 比防禦性程式設計更高效(也更 DRY),因為不是每個人都需要驗證資料

語意不變量(Semantic Invariants)#

你可以用語意不變量來表達不可違反的需求——一種「哲學合約」。例如一個金融卡交易系統的核心不變量是:使用者絕不能被重複扣款。寧可錯在不處理交易,也不要處理重複的交易。

當你發現一個符合語意不變量資格的需求,確保它成為文件中眾所周知的一部分。用清楚、不含糊的方式表述它。

動態合約與代理#

在自主代理(autonomous agents)的領域中,合約不一定是固定不變的規格。代理可以自由拒絕不想承擔的請求,也可以重新協商合約。想像一下:如果有足夠多的組件和代理能自行協商彼此之間的合約以達成目標,我們也許能讓軟體來解決軟體生產力危機。

相關章節#

  • Topic 10,正交性
  • Topic 24,死程式不說謊
  • Topic 25,Assertion 式程式設計
  • Topic 38,巧合式程式設計
  • Topic 42,基於屬性的測試
  • Topic 43,保持安全