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,保持安全