開發人員編寫代碼。不幸的是,開發人員也編寫缺陷,其中大多數缺陷是在最初的編碼階段加入的。修複這些缺陷成本最低的地方同樣也是在開發的初始階段。如果等到功能測試或者系統測試來捕獲並修複缺陷,那麼您的軟體開發成本就會高得多。在本文中,作者 Scott Will、Ted Rivera 和 Adam Tate 討論了一些基本的“防禦性”編碼和單元測試實踐,讓開發人員更容易找到缺陷 —— 更重要的是,從一開始預防缺陷產生。
防禦性駕駛和防禦性開發
大多數司機接受過防禦性駕駛技術的教育 —— 這有很好的理由 —— 但是並不是所有開發人員都接受過防禦性開發的教育,特別是那些沒有用組合語言進行過多少開發(如果不是完全沒用過的話)、也沒有因記憶體約束和處理器限制而關心過編寫極其緊湊的代碼的年輕開發人員。本文討論防禦性編碼和單元測試概念,它們可以協助開發人員更快產生更好的代碼並且缺陷更少。
為什麼防禦性開發是重要的?
捕捉錯誤、問題和缺陷的最佳位置是在開發週期的早期。圖 1 展示了最容易出現缺陷的地方,以及最容易發現它們的地方,並包括了修複這些缺陷的成本(這些成本是針對 1996 年的 —— 今天的成本顯然更高)。
圖 1. 缺陷:引入階段及發現階段(包括成本)
當然,比在編碼階段找到缺陷更好的是在一開始就防止它們。防止缺陷應該是開發人員最優先考慮的。我們將分析幾個讓開發人員可以在編碼和單元測試時防止並檢測缺陷的簡單的、經過證明的方法。
在編譯前(防禦性設計考慮)
防止缺陷(特別是系統性缺陷)的最有效方式是仔細檢查編碼所依據的設計。由設計缺陷導致的缺陷 —— 雖然一般不是很多 —— 通常修補成本是最高的。事前花很少的時間針對以下幾點對設計進行檢查可以得到顯著的長期回報。
設計考慮
設計是否有任何不清楚或者混亂的部分?如果是的話,在編寫任何代碼 之前 澄清這些問題。否則,您可能以一種方式解釋一個設計需求,而同事則以另一種方式解釋它,從而得到不相容的實現。
如果您的代碼要訪問同時被其他組件訪問的資料,那麼保證您的設計可以處理這種情況。同時,檢查設計的安全問題。
如果您的代碼嚴重依賴於其他應用程式的代碼,那麼您對那個應用程式是否熟悉到可以對設計進行檢查?考慮在您的設計檢查小組中加入熟悉該產品的一個開發人員。在 設計階段 發現的整合問題可以得到最有效處理。
安裝和使用考慮
如果您的代碼是以前版本的一個升級,那麼是否有會使升級失敗的參數或者其他選項改變?有哪些其他產品與新代碼互動或者整合 —— 如果這些產品本身也改變了呢?還有,您的代碼是否容易安裝?
作業系統和資料庫考慮
您的代碼是否會在新版本的作業系統或者資料庫上運行?您是否知道這些新版本中加入了哪些改變以及它們是否(及如何)影響您的代碼?
這隻是測試
設計是否結合了可測試性?雖然您可能認為可測試性問題不是您需要關心的,但是事實上單元測試 是 開發人員的責任之一 —— 幾乎所有使執行功能測試和/或系統測試更容易的任何事情也會使單元測試更容易執行。
下面是 可測試性 領域內容的幾個例子。
設計是否允許運行時外部工具訪問“狀態”變數(例如,目前狀態),特別是那些測試程式需要用來驗證代碼是否正確工作以協助確定問題的變數?
是否對跟蹤和日誌給予了足夠的重視?您讓其他人分析缺陷越容易,您在發現缺陷後修正它們就越容易(而且在單元測試中發現自己的問題也會更容易)。
您是否考慮了所有可能調用您的代碼的上下文?如果您可以將錯誤訊息與調用它的使用者函數上下文相關聯,那麼使用者就更有可能理解這個錯誤。
設計是否結合了您的測試自動化工具所需要的特定的“鉤子(hook)”?再多想一想您肯定可以在這個清單中加入更多的內容,特別是那些對您的產品或者組織特定的內容。
防禦性編碼技術:編譯器是您的朋友
當您完成對設計的檢查後,就輪到編碼了。就讓我們面對它,除了設計錯誤外,編碼是惟一引入缺陷的地方。無論如何,您的測試程式和客戶是不會加入缺陷的 —— 只有 您 會。我們都知道時間很緊張,但是如果您沒有時間在第一次就把它編寫正確,那麼您怎麼能找到時間去修正它呢?花上一些時間,這會使您在以後的編碼工作中更輕鬆。
防止缺陷的最好方法之一是使用編譯器。令人恐懼的是,開發人員在編譯時間通常選擇使用最低程度的警告輸出,所以請啟用編譯的全部警告 —— 把即使將編譯器配置為檢查 所有方面 編譯時間也不產生一個警告當成編寫代碼的一個挑戰。此外,對代碼使用多種編譯器使很多程式員獲益 —— 這種方法有時會捕獲不同的語法錯誤。
編碼習慣
下面我們將拋磚引玉介紹幾個好的編碼習慣。我們不是要為您定義“最佳編碼習慣” —— 我們只是要您形成自己遵守的代碼編寫習慣。下面是幾個供參考的最佳習慣的例子。
在使用前初始化所有變數
您是否有一組可接受的預設值,特別是對於可能被使用者、其他組件或者其他程式有選擇地修改的資料?同時,我們強烈要求您列出在最外圍常式中要使用的所有本地變數,然後再專門初始化它們。這樣不會對您編寫代碼時的想法留下任何疑問。雖然這可能要多花一些時間並且像是沒有理由地增加了幾行代碼,但是與只是在“運行中(on the fly)”聲明本地變數相比,大多數最佳化編譯器不會對此產生任何額外的運行時代碼。清單 1 顯示了在一個常式中最初幾行代碼的一個例子:
清單 1. 初始化本地變數
public unsigned short TransMogrify( UFEventLink IncomingLink ) { // // local variables // unsigned short usRc; String sOurEventType; String sTheirEventType; // // beginning of code // usRc = 0; sOurEventType = null; sTheirEventType = null; // // a miracle occurs... // return( usRc ); } // end "TransMogrify" |
使用一個“編碼通訊協定”文檔
如果您有一個編碼通訊協定文檔,就使用它。您可以在 Internet 上找到許多種編碼通訊協定。找到一種簡單的、切中要害、並為您留下一定的活動空間的標準。Sun 的網站有一個關於 Java 編程的編碼規範的文章(請參閱 參考資料),它給出了擁有標準的下列幾點理由:
一個軟體生存期百分之八十的成本都用在維護上。
幾乎沒有軟體在整個使用期間都是由原作者維護的。
編碼規範改進了軟體的可讀性,使工程師可以更快和更充分地理解新代碼。
如果您將原始碼作為產品交付,那麼需要保證它有像您建立的所有其他產品一樣的封裝和整潔性。
即使不贊成“標準”的想法,至少採用這個簡單的建議:對變數名使用“匈牙利命名法”,這會使您的代碼更容易閱讀和維護(。
保證傳回碼的一致性
在調試時有一種會製造麻煩的情況是:調用程式屏蔽(或者覆蓋)一個表示錯誤的傳回碼。一定要想好您要向調用您的代碼的常式返回什麼值,並保證從您所調用的常式返回的所有錯誤碼都得到恰當處理。如果傳回碼 n 在一個地方意味著一件事,就不要在其他的地方用傳回碼 n 表示另一件事。
對每個常式使用“單點退出”
這一點怎麼強調也不過分:對每個常式使用單點退出 —— 就是說,沒有多重返回!這是最容易忽視的、也是您可以採用的最好的習慣。如果常式只從一個地方返回,那麼就可以用一種非常容易的方法保證在返回前完成所有必要的清理工作,這也使調試更容易。清單 2 顯示了一個包含多重返回的程式碼範例。注意重複代碼、甚至忘記“清理”項目是多麼容易。
清單 2. 單點退出樣本
1 public String getName( )2 {3 //4 // local variables5 //6 String returnString;789 //10 // beginning of code11 //12 returnString = textField.getText( );13 if ( null == returnstring )14 {15 badCount++;16 totalCount++;17 return( null )18 }1920 returnString = returnString.trim( );21 if ( returnString.equals( "" ) )22 {23 badCount++;24 totalCount++;25 return( null );26 }2728 totalCount++;29 return( returnString );3031 } // end getName |
在第 15 行,badCount 增加了,因為 getText( ) 返回 null。在第 23 行,badCount 代碼又重複了。現在想像一下如果這個例子需要完成更複雜的“清理”時會有多混亂。
清單 3 顯示了一種更好的方法:
清單 3. 單點退出樣本 —— 修正後
1 public String getName( )2 {3 //4 // local variables5 //6 String returnString;789 //10 // beginning of code11 //12 returnString = textField.getText( );13 if ( null != returnstring )14 {15 returnString = returnString.trim( );16 if ( returnString.equals( "" ) )17 returnString = null;18 }1920 //21 // "cleanup"22 //23 if ( null == returnString )24 badCount++;25 totalCount++;2627 return( returnString );2829 } // end getName |
這是一個簡化的例子,但是請注意遵照這種習慣有多麼容易,以及這樣做的好處。