我們三個人的家中有四個青少年駕駛員,很快還會增加一個。不用說,我們非常熟悉在駕駛員培訓班中灌輸給我們這些新駕駛員的防御性駕駛技術。因為在我們的青少年駕駛員短短的開車期間,他們看到了打手機的男人闖紅燈、女人在拐彎的時候化妝,以及業務主管在早晨上班的路上讀報。所以孩子們很快學會如何注意可能會發生事故的區域以及如何避免這些事故。
防御性駕駛和防御性開發
大多數司機接受過防御性駕駛技術的教育 —— 這有很好的理由 —— 但是并不是所有開發人員都接受過防御性開發的教育,特別是那些沒有用匯編語言進行過多少開發(如果不是完全沒用過的話)、也沒有因內存約束和處理器限制而關心過編寫極其緊湊的代碼的年輕開發人員。本文討論防御性編碼和單元測試概念,它們可以幫助開發人員更快生成更好的代碼并且缺陷更少。
為什么防御性開發是重要的?
捕捉錯誤、問題和缺陷的最佳位置是在開發周期的早期。圖 1 展示了最容易出現缺陷的地方,以及最容易發現它們的地方,并包括了修復這些缺陷的成本(這些成本是針對 1996 年的 —— 今天的成本顯然更高)。
當然,比在編碼階段找到缺陷更好的是在一開始就防止它們。防止缺陷應該是開發人員最優先考慮的。我們將分析幾個讓開發人員可以在編碼和單元測試時防止并檢測缺陷的簡單的、經過證明的方法。
在編譯前(防御性設計考慮)
防止缺陷(特別是系統性缺陷)的最有效方式是仔細檢查編碼所依據的設計。由設計缺陷導致的缺陷 —— 雖然一般不是很多 —— 通常修補成本是最高的。事前花很少的時間針對以下幾點對設計進行檢查可以得到顯著的長期回報。
設計考慮
設計是否有任何不清楚或者混亂的部分?如果是的話,在編寫任何代碼 之前 澄清這些問題。否則,您可能以一種方式解釋一個設計需求,而同事則以另一種方式解釋它,從而得到不兼容的實現。
如果您的代碼要訪問同時被其他組件訪問的數據,那么保證您的設計可以處理這種情況。同時,檢查設計的安全問題(請參閱 參考資料)。
如果您的代碼嚴重依賴于其他應用程序的代碼,那么您對那個應用程序是否熟悉到可以對設計進行檢查?考慮在您的設計檢查小組中加入熟悉該產品的一個開發人員。在 設計階段 發現的集成問題可以得到最有效的處理。
安裝和使用考慮
如果您的代碼是以前版本的一個升級,那么是否有會使升級失敗的參數或者其他選項改變?有哪些其他產品與新代碼交互或者集成 —— 如果這些產品本身也改變了呢?還有,您的代碼是否容易安裝?
操作系統和數據庫考慮
您的代碼是否會在新版本的操作系統或者數據庫上運行?您是否知道這些新版本中加入了哪些改變以及它們是否(及如何)影響您的代碼?
這只是測試
設計是否結合了可測試性?雖然您可能認為可測試性問題不是您需要關心的,但是事實上單元測試 是 開發人員的責任之一 —— 幾乎所有使執行功能測試和/或系統測試更容易的任何事情也會使單元測試更容易執行。
下面是 可測試性 領域內容的幾個例子(更多內容請參閱 參考資料 部分)。
再多想一想您肯定可以在這個清單中加入更多的內容,特別是那些對您的產品或者組織特定的內容。
防御性編碼技術:編譯器是您的朋友
當您完成對設計的檢查后,就輪到編碼了。就讓我們面對它,除了設計錯誤外,編碼是惟一引入缺陷的地方。無論如何,您的測試程序和客戶是不會加入缺陷的 —— 只有 您 會。我們都知道時間很緊張,但是如果您沒有時間在第一次就把它編寫正確,那么您怎么能找到時間去修正它呢?花上一些時間,這會使您在以后的編碼工作中更輕松。
防止缺陷的最好方法之一是使用編譯器。令人恐懼的是,開發人員在編譯時通常選擇使用最低程度的警告輸出,所以請啟用編譯的全部警告 —— 把即使將編譯器配置為檢查 所有方面 編譯時也不產生一個警告當成編寫代碼的一個挑戰。此外,對代碼使用多種編譯器使很多程序員獲益 —— 這種方法有時會捕獲不同的語法錯誤。
編碼習慣
下面我們將拋磚引玉介紹幾個好的編碼習慣。我們不是要為您定義“最佳編碼習慣” —— 我們只是要您形成自己遵守的代碼編寫習慣。下面是幾個供參考的最佳習慣的例子。
在使用前初始化所有變量
您是否有一組可接受的默認值,特別是對于可能被用戶、其他組件或者其他程序有選擇地修改的數據?同時,我們強烈要求您列出在最外圍例程中要使用的所有本地變量,然后再專門初始化它們。這樣不會對您編寫代碼時的想法留下任何疑問。雖然這可能要多花一些時間并且像是沒有理由地增加了幾行代碼,但是與只是在“運行中(on the fly)”聲明本地變量相比,大多數優化編譯器不會對此生成任何額外的運行時代碼。清單 1 顯示了在一個例程中最初幾行代碼的一個例子:
|
使用一個“編碼標準”文檔
如果您有一個編碼標準文檔,就使用它。您可以在 Internet 上找到許多種編碼標準。找到一種簡單的、切中要害、并為您留下一定的活動空間的標準。Sun 的網站有一個關于 Java 編程的編碼規范的文章(請參閱 參考資料),它給出了擁有標準的下列幾點理由:
即使不贊成“標準”的想法,至少采用這個簡單的建議:對變量名使用“匈牙利命名法”,這會使您的代碼更容易閱讀和維護(有關匈牙利命名法的說明請參閱 參考資料)。
保證返回代碼的一致性
在調試時有一種會制造麻煩的情況是:調用程序屏蔽(或者覆蓋)一個表示錯誤的返回代碼。一定要想好您要向調用您的代碼的例程返回什么值,并保證從您所調用的例程返回的所有錯誤代碼都得到恰當處理。如果返回代碼 n
在一個地方意味著一件事,就不要在其他的地方用返回代碼 n
表示另一件事。
對每個例程使用“單點退出”
這一點怎么強調也不過分:對每個例程使用單點退出 —— 就是說,沒有多重返回!這是最容易忽視的、也是您可以采用的最好的習慣。如果例程只從一個地方返回,那么就可以用一種非常容易的方法保證在返回前完成所有必要的清理工作,這也使調試更容易。清單 2 顯示了一個包含多重返回的代碼示例。注意重復代碼、甚至忘記“清理”項目(如紅色突出顯示的文本所示)是多么容易。
|
在第 15 行,badCount
增加了,因為 getText( )
返回 null
。在第 23 行,badCount
代碼又重復了?,F在想像一下如果這個例子需要完成更復雜的“清理”時會有多混亂。
清單 3 顯示了一種更好的方法:
VBD>清單 3. 單點退出示例 —— 修正后
|
這是一個簡化的例子,但是請注意遵照這種習慣有多么容易,以及這樣做的好處。
加強警戒(En garde)!
要記住,您的客戶對您的產品有與您不一樣的想法。他們會在一個您的小組很可能從來也沒想到的 —— 或者至少是沒有可能測試的 —— 環境中安裝它。他們會以您從來沒有想到過的方法使用它,并以您意想不到的方法配置它。下面的列表有助于幫助您保證他們不會發怒:
null
,但是您在索引數組之前沒有檢查這種可能性時會發生什么情況)。
日志是必不可少的,而現有的最好工具是 alphaWorks 的 Logging Toolkit for Java (更多信息請參閱 參考資料)。 |
單元測試(防御性測試技術)
在本文中,我們所說的 單元測試 是開發人員在自己的代碼正確編譯后、在交給功能測試小組之前進行的所有測試和分析。正如我們在這只是一個測試 中提到的,主動進行單元測試并 在測試時像一位測試者那樣思考(即,必須往壞處想、熱衷于破壞并喜歡惡作?。┦呛苤匾?。下面是在單元測試時要記住的幾件事。
靜態代碼分析工具
第一種,也是最容易的分析代碼的方法是讓別人替您做 —— 或者像在這里一樣,讓其他 工具 替您做。有一些不同的靜態代碼分析工具可用,從綜合性的工具 —— 一些開發機構實際上在他們的“編譯”環境(這可是需要購買的)中加入了這樣的工具 —— 到其他可以免費從 Internet 上下載的工具。有關可用的靜態代碼分析工具的信息請參閱 參考資料。
發現缺陷
當您準備運行代碼并檢查缺陷時,要記住往壞處想。這些缺陷是您所創建的或者由您忽略的代碼產生。下面是一些幫助您找到代碼中缺陷的提示:
n
個數據項,然后試圖刪除 n+1
個項。 關于時間的考慮?
如果操作“b”在操作“a”之前發生會怎么樣?就算您 認為 它不會發生 —— 您能 使 它發生嗎?如果是的話,可以打賭您的客戶會使它發生的。最好現在找出來,而不是在修復成本更高、并聽到客戶報怨您的軟件質量糟糕之后再去做。
腳手架代碼
我們在前面發現缺陷 中討論了腳手架代碼。如果是為自己的使用需要而創建的,一定要將它交給驗證工程師??赡苣峁┑哪_手架代碼使他們可以很快地開始測試您的代碼,或者至少使他們更好地理解當其他組件存在時可以預期什么。
如果您的產品有保護性的安全功能,那么您必須測試它們。提供可以創建您想要防止的情況的腳手架代碼是很重要的:您必須能夠創建系統試圖防止的那種情況。
腳手架代碼的另一個簡單例子是提供操縱隊列的代碼。如果您的產品使用隊列,那么想像如果有一個可以在運行時從隊列中增加或者刪除項、破壞隊列中的數據(以保證適當的錯誤處理)等等的工具會有多方便。
源代碼級調試程序
使用源代碼級的調試程序是進行徹底和成功的單元測試的關鍵方法。開發人員應該與他們的調試程序共生死。不幸的是,對源代碼級的調試程序的充分了解和使用是一種正在消亡的做法,盡管這些調試程序的好處遠遠超過任何學習曲線。簡而言之,我們強烈鼓勵您全面學習一種調試程序。下面是用源代碼級調試程序對代碼進行單元測試的幾種方法。您可以:
認識您的驗證工程師
驗證工程師是測試知識的很好來源。他們可以給您指出要測試什么并幫助您了解可以在代碼中加入什么以幫助他們測試(如代碼鉤子)。此外,您可以向他們展示如何使用您的腳手架代碼。他們還會很有興趣了解您認為在測試中哪些應該是自動化的 —— 如果您某些事情做了不止一遍,那么他們也會。
開始測驗!
現在是進行小測試的時候了。讓我們看看您是否用心了。
問題
您希望檢查一個整數的值是否為 5
。通常,要這樣編寫代碼:
|
不過,如果您對代碼進行“手指檢查”,并且把代碼寫成了下面這樣會出現什么情況呢?
|
這個失誤是一個缺陷,但是只有在運行時才能捕獲它 —— 可能需要相當的調試努力才能找到它。編譯器會輕易放過您的代碼,那么如何防止這種錯誤發生?
答案
實際上有兩個答案:您可以使用一種上面描述的靜態代碼分析工具,并希望它有足夠的健壯性可以捕獲這種錯誤,也可以交換操作數以使常量位于左邊:
|
因為這種方法保證您可以在編譯代碼時立即捕捉到問題,所以它是首選的技術。雖然它看上去有些笨,但是代碼可以編譯并運行得很好。然而,當您“手指檢查”代碼時就可以立即看到好處了:
|
可是編譯器不喜歡這樣,因為 5
不能被賦值為另一個值。這就是我們在前面 說您應當將編譯器看成是您的朋友時所表達的意思。
您還可以在檢查 null 指針時使用這個技巧??聪旅娴拇a:
|
如果您偶然將它“誤寫”成下面這樣會發生什么呢?
|
您可能不會得到想要的結果。而改用下面的方法您會得到與我們剛描述過的同樣的“編譯器保護”:
|
結束語
您可能預計會有一個長的小結,把我們所討論的所有內容重新概述一遍,然后是請您盡可能多地采用我們的建議。冒著讓您失望的風險,為保持簡明扼要我們做了一個相當簡潔的歸納:要么現在去做,要么以后花 多得多 的代價去做。換句話說,您在開發周期的早期在測試和預防代碼缺陷上花的時間越多,您在以后節省的時間和金錢就越多。這就是防御性編碼的意義。它就是這么簡單。
原文轉自:http://www.anti-gravitydesign.com