林星 (iamlinx@21cn.com)
2003 年 12 月
http://www-900.ibm.com/developerWorks/cn/linux/software_engineering/l-oo/index6.shtml
[Copy to clipboard] [ - ]
CODE:
針對契約設計是一種嚴謹的軟體設計思路,它有助於提高軟體的品質。軟體設計中經常出現的bug往往是由於需要的前提條件或資料不能夠得到滿足而導致的。針對契約設計通過一種約束性的方法,解決了這個問題。
1.針對契約設計
我們知道,現代的社會是一種生人社會,這和我們幾千年的熟人社會已經不一樣了,人和人的關係變得很複雜,如何保證每個人的利益,如何保證這種複雜的關係不會對社會的穩定性造成影響。現代社會的解決方案是採用契約,或說是合約。我們和用人單位需要簽訂勞動合約,購買房產需要商品房買賣合約,甚至我們上車買票,車票本身也是一種合約。為什麼合約如此的重要呢?因此它規定了人和人之間的一種關係,並為這種關係定義了嚴謹的責任和權利。軟體設計也是類似的。一個大型的系統,類之間的關係非常的複雜,方法間相互調用,因此,我們也需要一種類似於契約一樣的嚴謹規範,來約束每個類、每個方法,以保證軟體整體的穩定性。這就是針對契約設計的實質。
Eiffel語言天生就是一種嚴謹、甚至可以說是保守的語言。例如,對於Eiffel中的公有欄位,預設的情況下就是唯讀,以此保證公有屬性被不當修改。而Eiffel值得稱道之處,在於在使用Eiffel語言的時候,你的大部分的精力都花在如何設計類的前置條件(precondition)、後置條件(postcondition)和不變式(invariant)上。不要小看這三者的作用,雖然看起來簡單,在嘗試著設計它們的時候,你就會發現,你的類將會變得非常的強壯。為什麼呢?我們都知道,要保證資訊傳遞的正確性,就需要有反饋機制。而在軟體設計的時候如何引入這種反饋機制呢?Eiffel就用它自己的方式實現了這一點。對前置條件的檢查,保證了方法開始之前狀態的正確性,後置條件和不變式保證了方法執行完畢後我們得到了我們所需要的狀態。雖然我們還可以找到很多其它的反饋機制,但是Eiffel的這種機制,無疑是很有效。
Eiffel的特性粗看起來並沒有什麼特別之處,舉一個最小的例子:
對於一個將內部count值加一的Inc的方法來說,它的後置條件是
[Copy to clipboard] [ - ]
CODE:
count=old count+1
這裡的old count指的是未被改變的值,即舊值。你可能會說,這不是吃飽了撐著嗎,代碼本身做的事情就是另count值加一,最後還要檢驗一次加一的結果,純屬浪費機器資源。這裡例子非常的小,因此我們無法看出更為具體的思路。但是我們知道,在物件導向設計中,各個類、各個方法之間構成了細密的協作網。在這種情況下,即容易犯錯誤。這時候,後置條件的根本作用,就是強迫你找到另外一種方法,來驗證你剛才的工作是正確的。這就好像我們在驗算數學題的時候,如果方法相同,那麼這個結果還未必是對的,因為方法一樣,你可能漏掉了一些資訊,但是如果我們能夠從另一個渠道來驗證結果,例如和同桌對對答案,這個結果正確的可能性就大了許多,你的把握也會大很多。所以,後置條件可以看作是另一個渠道的驗算。
Eiffel的機制還導致了另一個結果,那就是優秀的物件導向軟體設計。物件導向的最基本的功底,在於設計微小的、完成一個簡單任務的類和方法。可惜從非物件導向轉型來的程式員,仍然喜歡編寫一些很長的代碼塊。使用Eiffel完全不會出現這種情況,使用這些冗長的方法,你根本無法實現前置條件和後置條件。學習使用它們,你可以很自然的學習重用的思路。基於這種考慮,建議向物件導向轉型的程式員們都學學Eiffel,你的物件導向設計功力會大有長進的。
Eiffel並不是本文討論的重點,因此本文並不打算花費太多的精力來介紹它,遺憾的是,國內關於Eiffel的資料比較少,我瞭解到的中文資料是人民郵電出版社即將出版的面向契約設計一書。
另外仍需要提及的一點是,Eiffel並不是一種國內流行的開發語言,但是這並不影響我們在其它語言中吸收這種優秀的思路。Java語言在其新的JDK版本中引入了斷言機制,就可以用於實現前置條件和後置條件。即使是一些不支援斷言的語言,也很容易編寫自己的斷言機制。
前置條件和後置條件的應用還擴充到了其它的方面。例如,在設計中和用例中,都引入了前置條件和後置條件的用法。不管是應用在哪一個地方,思路都是一樣的。前置條件最大的好處就是排除非法的輸入值,而後置條件的最大好處就是對結果進行驗證,以保證過程的正確性。不同應用的前置條件和後置條件都有兩個共同的收益;
更好的結構性
更優的重用性
和Eiffel的思路一樣,我們把現實中的業務對象看作是由基本查詢、派生查詢、操作這三者組成的。同時利用這三種機制,才能夠有效運用前置條件和後置條件。將問題域中的問題劃分為這三種類型,無疑提高了問題域組織的有效性,也就是獲得了更好的結構性。另外,前置條件和後置條件使用的是半形式化的表述方式,因此問題域的表述是清晰的,嚴謹的。
在獲得結構性的同時,我們得到了更優的重用性,怎麼說呢?派生查詢是由基本查詢構成的,而操作中又運用了兩種查詢。由於分類清晰,我們可以很方便的進行重用。
這樣的說法可能過於抽象了。我們說一個現實中的故事。我接觸過一段的路由器,在學習路由器的第一堂課上,我學會了一個最重要的操作,就是在對任何的配置進行修改時候,你必須查看設定檔,以保證配置正確。這是一個很基本的操作,但是我卻是在實踐中吃過虧之後才發現它的重要性的。查看設定檔其實就是配置這個操作的後置條件,由它來保證配置操作過程的正確性,只要後置條件為真,出錯的機率將會降低。當然,我們還可以加入更多的後置條件來進一步降低機率。而這裡的前置條件是你已經正確登入到路由器,這是一個基本的條件,可能還有作業系統支援該配置條件等。
前置條件和後置條件確實是個好東西,不過,它並不是沒有成本的。(作為精益編程的擁護者,我們做任何事情都需要考慮成本和收益的)。最大的成本是時間,在同一個問題域上花費的時間大大增多了,影響到了整體的軟體過程,這對於很多的項目是要命的。如何看待這一成本呢?應該說,一開始應用前置條件和後置條件的時候,確實是需要付出額外的成本的。隨著對應用的熟悉,這個成本會慢慢降低。而後續因為軟體品質改進帶來的其它方面的收益,將會超過這個成本。它們的曲線大致會是這樣的:(遺憾的是,我們暫時做不到定量的分析)
當然,我們一方面注重效益,另一方面仍然要考慮儘可能降低成本。在成本方面,度是最重要的。前置條件和後置條件的應用的最精妙之處也在於此。要多少的前置條件和後置條件才能夠滿足需要?條件編寫的細緻程度如何?對不同的應用是否採用同樣的度?這些資料都只能夠來源於實踐經驗。度的不足難以表現前置條件和後置條件的威力,度的過剩又增加了投入成本。
前置條件和後置條件的思路在生活中到處可見,在數學中的應用就更多了。對我們軟體開發人員來說,重要的是形成這樣的操作思路,設計出穩定的軟體。在Eiffel語言中,另一個重要的特性是不變式。由於篇幅所限,我們這裡不可能進行大量的介紹,大家可以參考相關的資料。對於我們來說,最重要的是理解針對契約設計的優勢,並運用到項目當中。
一開始我們就說過,按契約設計是一種嚴謹的設計思路,開發的成本隨之提高,因此很多的軟體Team Dev並不願意採用Eiffel語言,Eiffel語言本身也是一個陽春白雪的存在。但是隨著軟體規模日益擴大,品質要求不斷提高,按契約設計的思路已經慢慢進入了很多的語言中了。
對於項目的開發人員來說,品質的要求最難的就是如何實際操作。加強測試的力量和強度固然是一種辦法,但是成本也不菲。審核也是一種有效辦法,但是對審核者的要求和壓力都不小,一旦流於形式,也起不到什麼效果。將軟體工程的思路徹底貫徹到代碼中一直是本文的主題,這裡也不例外。按契約設計提供了一種方法,要求程式員按照嚴謹的方法進行方法調用和方法設計。雖然調用方和被呼叫者同時採用嚴謹的設計模式存在浪費的可能,但這個成本是很低的,而從軟體工程文化的角度上來看,卻能夠逐漸形成高效的編碼習慣,這還是非常划算的。
2. 規範
要定義一個好的規範,首先我們需要清楚的知道我們制定規範的目的。首先可以肯定的,我們的目的不是使用Eiffel語言,而是提高軟體的品質,那麼,在這個目的下,我們如何制定規範呢?如果你希望在組織內引入面向契約設計。那麼,可以嘗試著使用下文介紹的iContract工具,並根據iContract的方式定義你的按契約設計規範。如果你不希望改變現有的開發方式,只是想從按契約設計中學習一些知識,那麼,你更重要的是從基本查詢、派生查詢、操作方面來考慮如何設計規範,來約束類的設計。這樣,你同樣可以從按契約設計的思路中獲益。
3. 技能
類設計的學習。學習按契約設計,最關鍵的就是學習如何通過基本查詢、派生查詢、操作這三個方面來設計類。
對象約束語言。對象約束語言(OCL)是一種描述物件導向設計的語言,它由OMG組織管理和維護。前置條件、後置條件和不變式的描述採用了OCL的一個子集。學習OCL語言,關鍵並不是在文法本身,而是在於對象的設計上。如果一個對象的設計不夠規範,你會發現,你無法使用OCL語言來描述它。如果你能夠熟練的使用OCL來描述類和類之間的關係,那麼,你會發現,軟體的品質會得到大幅度的提高。
4. 過程
按契約設計屬於設計範疇,所以,它可以很方便的和介面設計、測試等活動結合起來。例如,iContract中就提供了這方面的例子:
[Copy to clipboard] [ - ]
CODE:
/**
*/
public interface IEmployee {
/**
* @pre hasOffice()
*
* @return iContract.examples.office_management_system.API.IRoom
*/
public IRoom getOffice();
/**
* @post return == (office != null) // implementation, exposes null
*
* @return boolean
*/
public boolean hasOffice();
/**
* @pre office != null
*
* @post hasOffice()
* @post getOffice() == office
*
* @param office IRoom
*/
public void setOffice(IRoom office);
}
同樣的,測試活動中也可以針對以上的介面描述進行重點測試。測試前置條件違反的情況,測試後置條件和不變式是否滿足。
5. 工具
Eiffel是一門非常優秀的語言,但是要在項目中完全採用Eiffel並不是一件容易的事情。除了Eiffel之外,其它的語言都沒有對針對契約設計的明顯支援,不過確實有人為非Eiffel語言設計了針對契約式設計支援工具。而Java中的iContract就是其中的一種。iContract其實是一種先行編譯器,它把注釋中的特別標記翻譯為標準的Java代碼,插入到最終的代碼中:
[Copy to clipboard] [ - ]
CODE:
/**
* @pre f >;= 0.0
*/
public float sqrt(float f) { ... }
@pre是前置條件的標誌符號,它表示了函數sqrt的輸入參數f需要滿足的條件。同樣的,還有@post、@inv等標識符,它們分別表示了前面討論的後置條件和不變式。此外,iContract還支援forall、exists、implies等一些OCL文法。
關於作者
林星,辰訊軟體工作室專案管理組資深專案經理,有多年項目實施經驗。辰訊軟體工作室致力於先進軟體思想、軟體技術的應用,主要的研究方向在於軟體過程思想、Linux叢集技術、OO技術和軟體工廠模式。您可以通過電子郵件 iamlinx@21cn.com 和他聯絡。