Code Complete — 建立高質量的程式碼

weixin_34208283發表於2016-02-27

本文將從變數,語句,程式碼塊,子程式,到類以及框架設計,詳細描述瞭如何編寫高質量的程式。儘管大部分原則你可能都知道了,但還是有些點會帶給你驚喜。

變數

變數初始化原則

  • 宣告的時候初始化
  • 在靠近變數第一次使用的位置初始化,就近原則。
  • 理想情況下,在靠近第一次使用變數的位置宣告和定義該變數,但是在JS裡面卻習慣將變數宣告提前。
  • 注意計數器和累加器的修改。
  • 在類的建構函式中初始化資料成員
  • 確定是否需要重新初始化
  • 把每個變數用於唯一用途

變數作用域優化

作用域指變數在程式內的可見和可引用範圍。介於同一變數多個引用點之間的程式碼可稱為”攻擊視窗(window of vulnerability)”,應把變數的引用點儘可能集中在一起,減小”攻擊視窗“的範圍。

  • 儘量縮短變數的引用範圍
  • 儘量縮短變數的存活時間
  • 把相關語句提取成單獨的子程式
  • 儘量少使用全域性變數。使用全域性變數可以讓程式寫起來很方便,因為全域性變數可以隨時訪問和使用,但是這樣很難維護和管理,如果換人來維護這些程式碼他很難知道這些變數在哪裡在什麼時候會被修改。

變數命名原則

  • 規範命名的目的是提高程式的可讀性同時易於除錯
  • 變數名需要準確描述其代表的事物
  • 變數名的平均長度在10到16個字元時更易於除錯。這並不是說你要把所有的變數都控制在這個範圍,命名的最終目的提高可讀性和可維護性,當你檢查程式碼時發現大部分變數名都很短或者含義不清時,那你的命名肯定有問題
  • 長名變數適合全域性變數,短名的適合區域性變數
  • 將計算值限定詞作為字尾。Total,Sum,Average,Max,Min,Str,Pointer等表示計算的限定詞一般放在後面。
  • 使用業界約定俗稱的變數。比如i,j,temp,flag這些,不用解釋都知道。
  • 使用團隊命名規範,不同團隊,不同語言的命名原則會有不同,優先服從規範。
    程式碼閱讀的次數遠大於編寫的次數,確保你的名字更易於閱讀,而不是易於編寫。

變數縮寫原則

  • 使用標準的縮寫,如min,sub,str等
  • 去掉所有非前置母音,如computer->cmptr,screen->scrn,apple->appl
  • 去掉虛詞and,or,the等
  • 使用單詞的前幾個字母,統一在每個單詞的第N個字母后截斷
  • 去除無用字尾,如ing,ed等
  • 保留每個音節中最引人注意的發音
  • 確保不要因為縮寫而改變了變數的含義,或者縮寫後的變數名有歧義或者很難理解

語句

直線型程式碼

組織直線型程式碼最主要的原則就是按照依賴關係進行排列。所謂依賴關係就是下一行程式碼是否會依賴上一行程式碼的執行,是則為順序相關依賴,否則為順序無關依賴。可以用好的子程式名,引數列表,註釋來讓順序相關依賴變得更明顯。如果程式碼之間沒有順序依賴關係,那就設法使相關的語句儘可能地接近。

條件語句

if語句使用原則
  • 先處理正常路徑,再處理不常見情況
  • 考慮else語句。雖然5到8成的程式碼都會有else語句,但有些情況是在程式一開始就做一個if判斷,是則返回,不執行後面的程式碼,這樣可以避免將後面的程式碼全都巢狀在else子語句中。但無論是否有else,請都將子句用大括號括起來。
  • 簡化複雜的條件檢測。在if/elseif語句中,經常會有很複雜的邏輯判斷,為了提高可讀性,可將這些邏輯判斷封裝成布林函式。
  • 考慮將if/elseif 替換成case.
case 語句

case語句適合處理簡單易分類的資料,如果你的資料並不簡單,請使用if/elseif語句。

  • 按字母/數字順序排列各種情況
  • 優先處理正常情況
  • 按執行效率排列case語句
  • 如果在某個case後面沒有break,請註釋說明。
  • 利用default子句來檢測錯誤

表驅動法

直接訪問表

在前端開發,針對後臺返回的錯誤碼,通常不會直接用if/else判斷錯誤碼來顯示相應地錯誤資訊,而是將錯誤碼-錯誤提示存放在”表“物件中,通過傳入錯誤碼來返回錯誤提示,這就是最簡單的表驅動法——直接訪問表。

當然我們可能會遇到更加複雜的情況,比如某活動要給1到100歲的人提供優惠,不同年齡的人群優惠可能相同也可能不同。如果將年齡作為key,優惠作為value,那麼最笨得方法是儲存100個鍵值對,當然這裡面的值會有重複的。

解決方法就是做鍵值轉換,將年齡轉化成另外一個鍵,然後讓該鍵對應到具體優惠。

索引訪問表

鍵值轉換提供了一個很好地思路,那就是將表的”查詢條件“和”查詢記錄"分開管理,建立索引。索引訪問表適合處理表記錄佔用空間比較大得情況,操作索引中的記錄往往比操作主表本身的記錄更方便廉價,並且由於索引和主表是分開的,同一個主表可以根據不同查詢條件建立不同索引,靈活性更強,後期可維護性也更好。

階梯訪問表

索引訪問的一個問題就是如果鍵的取值範圍很大的話,那建立的索引就會很長很佔空間,階梯訪問表則是對某些情況下的一種優化。
階梯訪問的基本思想是:表中的記錄對於不同的資料範圍有效,而不是不同的資料點。相對於索引訪問,通常將輸入資料對映到指定資料範圍,飯後取得對於值的過程是比較耗時的,這其實是一種用時間換空間的方式。具體採用哪種表驅動方法,就看時間和空間哪個對你更重要了。

高質量的子程式

建立子程式最主要的目的是提高程式的可管理性,當然也有其他一些好的理由。其中,節省程式碼空間只是一個次要原因,更重要的是能提高可讀性、可靠性和可修改性。
高質量的子程式可以:

  • 降低和隔離複雜度
  • 引入中間層,易懂的程式碼
  • 提高可移植性
  • 改善效能
  • 隱藏實現細節,隱藏全域性資料
  • 限制變化帶來的影響
  • 形成中央控制點
  • 達到特定的重構目的

高質量的子程式應該是功能上高內聚的,有著良好的命名。說到命名,一直很矛盾,怎樣才能算是一個好的命名?按什麼標準?書中給了參考:

  • 描述子程式所做的所有事情。要完整的描述一個子程式,名字可能會很長,這個時候除了使用縮寫,還應該思考一下這樣的子程式本身是不是有問題。
  • 避免使用無意義或模糊的詞。計算機是明確的,doSomething這樣的函式名只是用來教學。
  • 不要通過數字來標識。看到handle1,handle2這樣的命名是不是很憤怒,哈哈。
  • 根據需要確定子程式名字的長度。研究表明,變數名的最佳長度是9到15個字元。我不知道這個調查是針對特定程式語言還是所有程式語言,按理說應該是語言無關,但我怎麼有種感覺,Java或者C++程式碼的命名普遍比JS中的要長?
    -給函式命名時要對返回值有所描述。就是說看到函式名就知道它會返回什麼。比如xxx.isReady()看名字就知道返回布林型,xxx.next()返回下一個與xxx相關的物件。
  • 給過程起名時使用語氣強烈的動賓形式。比如printDocument,checkOrderInfo。但是在面嚮物件語言中,比如JS,通常不用加賓語,因為賓語就是物件本身,比如document.print(),orderInfo.check()。
  • 準確使用對仗詞。比如add/remove,open/close。fileOpen對fileClose,fileOpen對fClose就會很奇怪。
  • 為常用操作確定命名規則。

書中還說了一個比較有趣的問題,子程式可以寫多長?理論上認為的子程式最佳長度是一屏程式碼或列印出來一到兩頁紙的長度,約20200行(原書是50150行)。人們已經在子程式長度的問題上做了大量統計和研究,但並非所有的這些統計都適合現代程式設計。不過有一點,如果你的子程式超過了200行,那你就要小心了。

子程式通常會有引數,如何組織這些引數也是門學問。下面是一些指導原則:

  • 按照輸入-可修改-輸出的順序排列引數,也可以考慮按照該排列規則對引數進行規範命名。
  • 讓所有子程式引數排列順序保持一致。
  • 使用所有引數。很遺憾,這是JS的先天缺陷,你需要更加小心。
  • 把狀態或者出錯變數放到最後。
  • 不要把子程式的引數用作工作變數,應該在子程式中使用區域性變數。
calcDemo(inputVal){
  inputVal = inputVal + currentAdder(inputVal)
  // do something with inputVal
  ...
  return inputVal
}

這樣的程式碼雖然沒有任何錯誤,但是容易造成誤解,因為最後返回的inputVal已經不是最初傳入的inputVal了,正確的做法是在函式內部使用區域性變數指向inputVal然後返回該區域性變數。這裡是工程程式碼,不是在競賽網站上,不能為了簡潔而簡潔,少寫一行程式碼並不會給你加分。

  • 在介面中對引數的假定加以說明。
  • 限制子程式的引數個數。7是個很神奇的數字,讓你的引數保持在七個以內。
  • 為子程式傳遞用以維持其介面抽象的變數或物件。我在很多程式碼中發現,函式引數並不是一個個變數,而是一個物件,通過該物件來傳遞引數。

這是一個富有爭議的問題。假如一個物件有10個屬性,但是處理方法只用到了3個屬性,那麼直接傳遞物件就暴露了其他屬性,這破壞了封裝原則,增加了程式碼耦合。另一種觀點則認為傳遞整個物件能使子程式更加靈活,使介面更加穩定易於擴充套件。

那到底何時傳變數,何時傳物件呢?作者認為關鍵在於子程式的介面想要表達何種抽象。如果要表達的抽象是子程式期望的特定資料,那麼應該直接傳資料,如果要表達的抽象是想擁有某個特定物件,就應該傳物件。

比如,你發現在呼叫子程式之前都要先建立一個物件,呼叫完後又從物件中取出這些資料,那說明你需要的是資料而非物件。如果你發現自己經常需要修改子程式的參數列,而每次修改的引數都來自同一個物件,那說明你需要的是整個物件。

說完引數,最後來說說返回值。如果把函式按語義劃分,可以分為“函式”和“過程”,”函式”有返回值,而“過程”返回void或者沒有返回值。什麼時候使用”函式“,什麼時候使用”過程”,其實通過函式名就應該能確定下來。比如xxx.next()和xxx.fire(),前者一看就是”函式“,而後者是”過程“。
如果你使用”函式“,肯定會存在返回錯誤返回值的風險,尤其是當函式內有多條分支時。為減小這一風險,請確保:

  • 檢查所有可能的返回路徑
  • 不要返回指向區域性資料的引用或者指標

防禦式程式設計

防禦式程式設計的核心其實就是容錯。當子程式遭遇到各種非法輸入資料時也能工作。對於這些非法資料,通常有三種方式來處理:

  1. 檢查所有來源於外部的資料。檔案,使用者,網路等介面的資料都屬於外部資料,這些都是不安全的。
  2. 檢查子程式所有的輸入引數。子程式的輸入資料來源於其它子程式,這裡做檢查是為了防止程式內部產生了非預期的資料。
  3. 決定如何處理錯誤的輸入資料。根據專案需求,你可以返回錯誤碼,記錄日誌,返回一個預設的合法值或返回與前次相同的資料,具體方案視需求而定。

第一點和第二點都是資料校驗,第三點是對校驗結果的處理方式。一切錯誤都來自於輸入輸出。理論上對於所有外部資料都要進行校驗,因為這些資料都是不可靠不確定的,需要通過一個”過濾系統”將其過濾成確定型別的資料。這個”過濾系統”就是隔欄(barricade)。在隔欄的外面應該使用錯誤處理技術,在內部應該使用斷言。因為隔欄內部的資料都是被清理過的,如果在內部出錯那應該是程式的錯誤而非資料的錯誤。

還有一種容錯方式叫異常。異常是把程式碼中得錯誤或異常事件傳遞給呼叫方程式碼的一種特殊手段。異常跟斷言的使用情景相似,都是用來處理那些罕見或者永遠不應該發生得情況。書中給出了使用異常的一些建議:

  • 用異常通知程式的其他部分,進行錯誤訊息傳遞。
  • 只有在其他編碼方式無法解決的情況下才使用異常。
  • 不要把本可在區域性處理的錯誤當成一個未捕獲的異常丟擲去。
  • 避免使用空得catch語句,這是一種不負責任的寫法。
  • 瞭解所有函式庫可能丟擲的異常。
  • 建立一套幾種的異常處理機制。
  • 考慮異常的替換方案,確保你的程式是真的需要處理異常。

過度的防禦式程式設計會使程式變得臃腫緩慢,增加軟體的複雜度,變得難以維護。所以在進行編碼時呀考慮好什麼地方需要防禦,然後調整優先順序,因地制宜。

系列文章

相關文章