天天看點

Code Complete — 建立高品質的代碼

本文将從變量,語句,代碼塊,子程式,到類以及架構設計,較長的描述了如何編寫高品質的程式。盡管大部分原則你可能都知道了,但還是有些點會帶給你驚喜。

聲明的時候初始化

在靠近變量第一次使用的位置初始化,就近原則。

理想情況下,在靠近第一次使用變量的位置聲明和定義該變量,但是在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等

保留每個音節中最引人注意的發音

確定不要因為縮寫而改變了變量的含義,或者縮寫後的變量名有歧義或者很難了解

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

先處理正常路徑,再處理不常見情況

考慮else語句。雖然5到8成的代碼都會有else語句,但有些情況是在程式一開始就做一個if判斷,是則傳回,不執行後面的代碼,這樣可以避免将後面的代碼全都嵌套在else子語句中。但無論是否有else,請都将子句用大括号括起來。

簡化複雜的條件檢測。在if/elseif語句中,經常會有很複雜的邏輯判斷,為了提高可讀性,可将這些邏輯判斷封裝成布爾函數。

考慮将if/elseif 替換成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就會很奇怪。

為常用操作确定命名規則。

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

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

按照輸入-可修改-輸出的順序排列參數,也可以考慮按照該排列規則對參數進行規範命名。

讓所有子程式參數排列順序保持一緻。

使用所有參數。很遺憾,這是js的先天缺陷,你需要更加小心。

把狀态或者出錯變量放到最後。

不要把子程式的參數用作工作變量,應該在子程式中使用局部變量。

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

在接口中對參數的假定加以說明。

限制子程式的參數個數。7是個很神奇的數字,讓你的參數保持在七個以内。

為子程式傳遞用以維持其接口抽象的變量或對象。我在很多代碼中發現,函數參數并不是一個個變量,而是一個對象,通過該對象來傳遞參數。

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

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

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

說完參數,最後來說說傳回值。如果把函數按語義劃分,可以分為“函數”和“過程”,”函數”有傳回值,而“過程”傳回void或者沒有傳回值。什麼時候使用”函數“,什麼時候使用”過程”,其實通過函數名就應該能确定下來。比如xxx.next()和xxx.fire(),前者一看就是”函數“,而後者是”過程“。

如果你使用”函數“,肯定會存在傳回錯誤傳回值的風險,尤其是當函數内有多條分支時。為減小這一風險,請確定:

檢查所有可能的傳回路徑

不要傳回指向局部資料的引用或者指針

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

檢查所有來源于外部的資料。檔案,使用者,網絡等接口的資料都屬于外部資料,這些都是不安全的。

檢查子程式所有的輸入參數。子程式的輸入資料來源于其它子程式,這裡做檢查是為了防止程式内部産生了非預期的資料。

決定如何處理錯誤的輸入資料。根據項目需求,你可以傳回錯誤碼,記錄日志,傳回一個預設的合法值或傳回與前次相同的資料,具體方案視需求而定。

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

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

用異常通知程式的其他部分,進行錯誤消息傳遞。

隻有在其他編碼方式無法解決的情況下才使用異常。

不要把本可在局部處理的錯誤當成一個未捕獲的異常抛出去。

避免使用空得catch語句,這是一種不負責任的寫法。

了解所有函數庫可能抛出的異常。

建立一套幾種的異常處理機制。

考慮異常的替換方案,確定你的程式是真的需要處理異常。

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

繼續閱讀