1. 程式人生 > >淺談契約式程式設計

淺談契約式程式設計

  契約式程式設計是程式設計的一種方法。那麼什麼是契約式程式設計呢?我想這個概念是從“合同”演變過來的。

  在人類的社會活動中,契約一般是用於兩方,一方(供應者)為另一方(客戶)完成一些任務。每一方都期待從契約中獲得利益,同時也要接受一些義務。通常,一方視為義務的對另一方來說是權利。契約文件要清楚地寫明雙方的權利與義務。契約合同能保障雙方的利益,對客戶來說,合同規定了供應者要做的工作;對供應者來說,合同說明了如果約定的條件不滿足,供應者沒有義務一定要完成規定的任務。

  同樣的道理也適合於軟體。設想一個軟體單元E。它要達到它的目的(履行契約), E使用的策略可能會包括一系列的子任務,t1, ... tn。如果子任務ti 不是那麼簡單的,它得呼叫另一個功能例程(routine)R。換句話說,E把子任務轉包給R。這樣的情形應該被一個很好定義的“登記表”(roster)來管理雙方的義務與權利--契約。假如ti是一項任務,要求把一個給定的元素插入到一個有限容量的字典中。此處字典是一個(名-值)表,每一個元素(值)通過一個作為關鍵字的字串(名)存取。(譯者注:這裡“元素”可以理解成一個任意的資料項)。簡而言之,就是函式呼叫者應該保證傳入函式的引數是符合函式的要求,如果不符合函式要求,函式將拒絕繼續執行。如果按照契約式程式設計的思想編寫程式碼,就要求我們寫函式時檢查函式引數。有時候是簡單的判斷某個引數不能為空,或者數值不能小於0。如果在專案中全面應用契約式程式設計,則應該有一個“契約框架”幫我們來做這些事情。

契約與我們通常所說的商業契約很相似,有以下幾個特點:

  1. 契約關係的雙方是平等的,對整個bussiness的順利進行負有共同責任,沒有哪一方可以只享有權利而不承擔義務。
  2. 契約關係經常是相互的,權利和義務之間往往是互相捆綁在一起的;
  3. 執行契約的義務在我,而核查契約的權力在對方;
  4. 我的義務保障的是你的利益,而你的義務保障的是我的利益;

  將契約關係引入到軟體開發領域,尤其是面向物件領域之後,在觀念上給我們帶來了幾大衝擊:

  一般的觀點,在軟體體系中,程式庫和元件庫被類比為server,而使用程式庫、元件庫的程式被視為client。根據這種C/S關係,我們往往對庫程式和元件的質量提出很嚴苛的要求,強迫它們承擔本不應該由它們來承擔的責任,而過分縱容client一方,甚至要求庫程式去處理明顯由於client錯誤造成的困境。客觀上導致程式庫和元件庫的設計和編寫異常困難,而且質量隱患反而更多;同時client一方程式碼大多鬆散隨意,質量低劣。這種情形,就好像在一個權責不清的企業裡,必然會養一批屍位素餐的混混,苦一批任勞任怨,不計得失的老黃牛。引入契約觀念之後,這種C/S關係被打破,大家都是平等的,你需要我正確提供服務,那麼你必須滿足我提出的條件,否則我沒有義務“排除萬難”地保證完成任務。

  一般認為在模組中檢查錯誤狀況並且上報,是模組本身的義務。而在契約體制下,對於契約的檢查並非義務,實際上是在履行權利。一個義務,一個權利,差別極大。例如下面的程式碼:

if (dest == NULL) { ... }

  這就是義務,其要點在於,一旦條件不滿足,我方(義務方)必須負責以合適手法處理這尷尬局面,或者返回錯誤值,或者丟擲異常。而:

assert(dest != NULL);

  這是檢查契約,履行權利。如果條件不滿足,那麼錯誤在對方而不在我,我可以立刻“撕毀合同”,罷工了事,無需做任何多餘動作。這無疑可以大大簡化程式庫和元件庫的開發。

  契約所核查的,是“為保證正確性所必須滿足的條件”,因此,當契約被破壞時,只表明一件事:軟體系統中有bug。其意義是說,某些條件在到達我這裡時,必須已經確保為“真”。誰來確保?應該是系統中的其他模組在先期確保。如果在我這裡發現契約沒有被遵守,那麼表明系統中其他模組沒有正確履行自己的義務。就拿上面提到的“開啟檔案”的例子來說,如果有一個模組需要一個FILE*,而在契約檢查中發現該指標為NULL,則意味著有一個模組沒有履行其義務,即“檢查檔案是否存在,確保檔案以正確模式開啟,並且保證指標的正確性”。因此,當契約檢查失敗時,我們首先要知道這意味著程式錯誤,而且要做的不是糾正契約核查方,而是糾正契約提供方。換句話說,當你發現:

assert(dest != NULL);

  報錯時,你要做的不是去修改你的string_copy函式,而是要讓任何程式碼在呼叫string_copy時確保dest指標不為空。

  我們以往對待“過程”或“函式”的理解是:完成某個計算任務的過程,這一看法只強調了其目標,沒有強調其條件。在這種理解下,我們對於exception的理解非常模糊和寬泛:只要是無法完成這個計算過程,均可被視為異常,也不管是我自己的原因,還是其他人的原因(典型的權責不清)。正是因為這種模糊和寬泛,“究竟什麼時候應該丟擲異常”成為沒有人能回答的問題。而引入契約之後,“過程”和“函式”被定義為:完成契約的過程。基於契約的相互性,如果這個契約的失敗是因為其他模組未能履行契約,本過程只需報告,無需以任何其他方式做出反應。而真正的異常狀況是“對方完全滿足了契約,而我依然未能如約完成任務”的情形。這樣以來,我們就給“異常”下了一個清晰、可行的定義。

  一般來說,在面向物件技術中,我們認為“介面”是唯一重要的東西,介面定義了元件,介面確定了系統,介面是面向物件中我們唯一需要關心的東西,介面不僅是必要的,而且是充分的。然而,契約觀念提醒我們,僅僅有介面還不充分,僅僅通過介面還不足以傳達足夠的資訊,為了正確使用介面,必須考慮契約。只有考慮契約,才可能實現面向物件的目標:可靠性、可擴充套件性和可複用性。反過來,“沒有契約的複用根本就是瞎胡鬧。(Bertrand Meyer語)”。

  由上述觀點可以看出,雖然Eiffel所倡導的Design By Contract在表象上不過是系統化的斷言(assertion)機制,然而在背後,確實是完全的思想革新。正如Ivar Jacoboson訪華時對《程式設計師》雜誌所說:“我認為Bertrand Meyer的方向——Design by Contract——是正確的方向,我們都會沿著他的足跡前進。我相信,大型廠商(微軟、IBM,當然還有Rational)都不會對Bertrand Meyer的成就坐視不理。所有這些廠商都會在這個方向上有所行動。”