本文將從變量,語句,代碼塊,子程序,到類以及框架設(shè)計,詳細(xì)描述了如何編寫高質(zhì)量的程序。盡管大部分原則你可能都知道了,但還是有些點會帶給你驚喜。
變量
變量初始化原則
- 聲明的時候初始化
- 在靠近變量第一次使用的位置初始化,就近原則。
- 理想情況下,在靠近第一次使用變量的位置聲明和定義該變量,但是在JS里面卻習(xí)慣將變量聲明提前。
- 注意計數(shù)器和累加器的修改。
- 在類的構(gòu)造函數(shù)中初始化數(shù)據(jù)成員
- 確定是否需要重新初始化
- 把每個變量用于唯一用途
變量作用域優(yōu)化
作用域指變量在程序內(nèi)的可見和可引用范圍。介于同一變量多個引用點之間的代碼可稱為”攻擊窗口(window of vulnerability)”,應(yīng)把變量的引用點盡可能集中在一起,減小”攻擊窗口“的范圍。
- 盡量縮短變量的引用范圍
- 盡量縮短變量的存活時間
- 把相關(guān)語句提取成單獨的子程序
- 盡量少使用全局變量。使用全局變量可以讓程序?qū)懫饋砗芊奖悖驗槿肿兞靠梢噪S時訪問和使用,但是這樣很難維護(hù)和管理,如果換人來維護(hù)這些代碼他很難知道這些變量在哪里在什么時候會被修改。
變量命名原則
- 規(guī)范命名的目的是提高程序的可讀性同時易于調(diào)試
- 變量名需要準(zhǔn)確描述其代表的事物
- 變量名的平均長度在10到16個字符時更易于調(diào)試。這并不是說你要把所有的變量都控制在這個范圍,命名的最終目的提高可讀性和可維護(hù)性,當(dāng)你檢查代碼時發(fā)現(xiàn)大部分變量名都很短或者含義不清時,那你的命名肯定有問題
- 長名變量適合全局變量,短名的適合局部變量
- 將計算值限定詞作為后綴。Total,Sum,Average,Max,Min,Str,Pointer等表示計算的限定詞一般放在后面。
- 使用業(yè)界約定俗稱的變量。比如i,j,temp,flag這些,不用解釋都知道。
- 使用團(tuán)隊命名規(guī)范,不同團(tuán)隊,不同語言的命名原則會有不同,優(yōu)先服從規(guī)范。
代碼閱讀的次數(shù)遠(yuǎn)大于編寫的次數(shù),確保你的名字更易于閱讀,而不是易于編寫。
變量縮寫原則
- 使用標(biāo)準(zhǔn)的縮寫,如min,sub,str等
- 去掉所有非前置元音,如computer->cmptr,screen->scrn,apple->appl
- 去掉虛詞and,or,the等
- 使用單詞的前幾個字母,統(tǒng)一在每個單詞的第N個字母后截斷
- 去除無用后綴,如ing,ed等
- 保留每個音節(jié)中最引人注意的發(fā)音
- 確保不要因為縮寫而改變了變量的含義,或者縮寫后的變量名有歧義或者很難理解
語句
直線型代碼
組織直線型代碼最主要的原則就是按照依賴關(guān)系進(jìn)行排列。所謂依賴關(guān)系就是下一行代碼是否會依賴上一行代碼的執(zhí)行,是則為順序相關(guān)依賴,否則為順序無關(guān)依賴。可以用好的子程序名,參數(shù)列表,注釋來讓順序相關(guān)依賴變得更明顯。如果代碼之間沒有順序依賴關(guān)系,那就設(shè)法使相關(guān)的語句盡可能地接近。
條件語句
if語句使用原則
- 先處理正常路徑,再處理不常見情況
- 考慮else語句。雖然5到8成的代碼都會有else語句,但有些情況是在程序一開始就做一個if判斷,是則返回,不執(zhí)行后面的代碼,這樣可以避免將后面的代碼全都嵌套在else子語句中。但無論是否有else,請都將子句用大括號括起來。
- 簡化復(fù)雜的條件檢測。在if/elseif語句中,經(jīng)常會有很復(fù)雜的邏輯判斷,為了提高可讀性,可將這些邏輯判斷封裝成布爾函數(shù)。
- 考慮將if/elseif 替換成case.
case 語句
case語句適合處理簡單易分類的數(shù)據(jù),如果你的數(shù)據(jù)并不簡單,請使用if/elseif語句。
- 按字母/數(shù)字順序排列各種情況
- 優(yōu)先處理正常情況
- 按執(zhí)行效率排列case語句
- 如果在某個case后面沒有break,請注釋說明。
- 利用default子句來檢測錯誤
表驅(qū)動法
直接訪問表
在前端開發(fā),針對后臺返回的錯誤碼,通常不會直接用if/else判斷錯誤碼來顯示相應(yīng)地錯誤信息,而是將錯誤碼-錯誤提示存放在”表“對象中,通過傳入錯誤碼來返回錯誤提示,這就是最簡單的表驅(qū)動法——直接訪問表。
當(dāng)然我們可能會遇到更加復(fù)雜的情況,比如某活動要給1到100歲的人提供優(yōu)惠,不同年齡的人群優(yōu)惠可能相同也可能不同。如果將年齡作為key,優(yōu)惠作為value,那么最笨得方法是存儲100個鍵值對,當(dāng)然這里面的值會有重復(fù)的。
解決方法就是做鍵值轉(zhuǎn)換,將年齡轉(zhuǎn)化成另外一個鍵,然后讓該鍵對應(yīng)到具體優(yōu)惠。
索引訪問表
鍵值轉(zhuǎn)換提供了一個很好地思路,那就是將表的”查詢條件“和”查詢記錄"分開管理,建立索引。索引訪問表適合處理表記錄占用空間比較大得情況,操作索引中的記錄往往比操作主表本身的記錄更方便廉價,并且由于索引和主表是分開的,同一個主表可以根據(jù)不同查詢條件建立不同索引,靈活性更強(qiáng),后期可維護(hù)性也更好。
階梯訪問表
索引訪問的一個問題就是如果鍵的取值范圍很大的話,那建立的索引就會很長很占空間,階梯訪問表則是對某些情況下的一種優(yōu)化。
階梯訪問的基本思想是:表中的記錄對于不同的數(shù)據(jù)范圍有效,而不是不同的數(shù)據(jù)點。相對于索引訪問,通常將輸入數(shù)據(jù)映射到指定數(shù)據(jù)范圍,飯后取得對于值的過程是比較耗時的,這其實是一種用時間換空間的方式。具體采用哪種表驅(qū)動方法,就看時間和空間哪個對你更重要了。
高質(zhì)量的子程序
創(chuàng)建子程序最主要的目的是提高程序的可管理性,當(dāng)然也有其他一些好的理由。其中,節(jié)省代碼空間只是一個次要原因,更重要的是能提高可讀性、可靠性和可修改性。
高質(zhì)量的子程序可以:
- 降低和隔離復(fù)雜度
- 引入中間層,易懂的代碼
- 提高可移植性
- 改善性能
- 隱藏實現(xiàn)細(xì)節(jié),隱藏全局?jǐn)?shù)據(jù)
- 限制變化帶來的影響
- 形成中央控制點
- 達(dá)到特定的重構(gòu)目的
高質(zhì)量的子程序應(yīng)該是功能上高內(nèi)聚的,有著良好的命名。說到命名,一直很矛盾,怎樣才能算是一個好的命名?按什么標(biāo)準(zhǔn)?書中給了參考:
- 描述子程序所做的所有事情。要完整的描述一個子程序,名字可能會很長,這個時候除了使用縮寫,還應(yīng)該思考一下這樣的子程序本身是不是有問題。
- 避免使用無意義或模糊的詞。計算機(jī)是明確的,doSomething這樣的函數(shù)名只是用來教學(xué)。
- 不要通過數(shù)字來標(biāo)識。看到handle1,handle2這樣的命名是不是很憤怒,哈哈。
- 根據(jù)需要確定子程序名字的長度。研究表明,變量名的最佳長度是9到15個字符。我不知道這個調(diào)查是針對特定編程語言還是所有編程語言,按理說應(yīng)該是語言無關(guān),但我怎么有種感覺,Java或者C++代碼的命名普遍比JS中的要長?
-給函數(shù)命名時要對返回值有所描述。就是說看到函數(shù)名就知道它會返回什么。比如xxx.isReady()看名字就知道返回布爾型,xxx.next()返回下一個與xxx相關(guān)的對象。 - 給過程起名時使用語氣強(qiáng)烈的動賓形式。比如printDocument,checkOrderInfo。但是在面向?qū)ο笳Z言中,比如JS,通常不用加賓語,因為賓語就是對象本身,比如document.print(),orderInfo.check()。
- 準(zhǔn)確使用對仗詞。比如add/remove,open/close。fileOpen對fileClose,fileOpen對fClose就會很奇怪。
- 為常用操作確定命名規(guī)則。
書中還說了一個比較有趣的問題,子程序可以寫多長?理論上認(rèn)為的子程序最佳長度是一屏代碼或打印出來一到兩頁紙的長度,約20200行(原書是50150行)。人們已經(jīng)在子程序長度的問題上做了大量統(tǒng)計和研究,但并非所有的這些統(tǒng)計都適合現(xiàn)代編程。不過有一點,如果你的子程序超過了200行,那你就要小心了。
子程序通常會有參數(shù),如何組織這些參數(shù)也是門學(xué)問。下面是一些指導(dǎo)原則:
- 按照輸入-可修改-輸出的順序排列參數(shù),也可以考慮按照該排列規(guī)則對參數(shù)進(jìn)行規(guī)范命名。
- 讓所有子程序參數(shù)排列順序保持一致。
- 使用所有參數(shù)。很遺憾,這是JS的先天缺陷,你需要更加小心。
- 把狀態(tài)或者出錯變量放到最后。
- 不要把子程序的參數(shù)用作工作變量,應(yīng)該在子程序中使用局部變量。
calcDemo(inputVal){
inputVal = inputVal + currentAdder(inputVal)
// do something with inputVal
...
return inputVal
}
這樣的代碼雖然沒有任何錯誤,但是容易造成誤解,因為最后返回的inputVal已經(jīng)不是最初傳入的inputVal了,正確的做法是在函數(shù)內(nèi)部使用局部變量指向inputVal然后返回該局部變量。這里是工程代碼,不是在競賽網(wǎng)站上,不能為了簡潔而簡潔,少寫一行代碼并不會給你加分。
- 在接口中對參數(shù)的假定加以說明。
- 限制子程序的參數(shù)個數(shù)。7是個很神奇的數(shù)字,讓你的參數(shù)保持在七個以內(nèi)。
- 為子程序傳遞用以維持其接口抽象的變量或?qū)ο蟆N以诤芏啻a中發(fā)現(xiàn),函數(shù)參數(shù)并不是一個個變量,而是一個對象,通過該對象來傳遞參數(shù)。
這是一個富有爭議的問題。假如一個對象有10個屬性,但是處理方法只用到了3個屬性,那么直接傳遞對象就暴露了其他屬性,這破壞了封裝原則,增加了代碼耦合。另一種觀點則認(rèn)為傳遞整個對象能使子程序更加靈活,使接口更加穩(wěn)定易于擴(kuò)展。
那到底何時傳變量,何時傳對象呢?作者認(rèn)為關(guān)鍵在于子程序的接口想要表達(dá)何種抽象。如果要表達(dá)的抽象是子程序期望的特定數(shù)據(jù),那么應(yīng)該直接傳數(shù)據(jù),如果要表達(dá)的抽象是想擁有某個特定對象,就應(yīng)該傳對象。
比如,你發(fā)現(xiàn)在調(diào)用子程序之前都要先創(chuàng)建一個對象,調(diào)用完后又從對象中取出這些數(shù)據(jù),那說明你需要的是數(shù)據(jù)而非對象。如果你發(fā)現(xiàn)自己經(jīng)常需要修改子程序的參數(shù)表,而每次修改的參數(shù)都來自同一個對象,那說明你需要的是整個對象。
說完參數(shù),最后來說說返回值。如果把函數(shù)按語義劃分,可以分為“函數(shù)”和“過程”,”函數(shù)”有返回值,而“過程”返回void或者沒有返回值。什么時候使用”函數(shù)“,什么時候使用”過程”,其實通過函數(shù)名就應(yīng)該能確定下來。比如xxx.next()和xxx.fire(),前者一看就是”函數(shù)“,而后者是”過程“。
如果你使用”函數(shù)“,肯定會存在返回錯誤返回值的風(fēng)險,尤其是當(dāng)函數(shù)內(nèi)有多條分支時。為減小這一風(fēng)險,請確保:
- 檢查所有可能的返回路徑
- 不要返回指向局部數(shù)據(jù)的引用或者指針
防御式編程
防御式編程的核心其實就是容錯。當(dāng)子程序遭遇到各種非法輸入數(shù)據(jù)時也能工作。對于這些非法數(shù)據(jù),通常有三種方式來處理:
- 檢查所有來源于外部的數(shù)據(jù)。文件,用戶,網(wǎng)絡(luò)等接口的數(shù)據(jù)都屬于外部數(shù)據(jù),這些都是不安全的。
- 檢查子程序所有的輸入?yún)?shù)。子程序的輸入數(shù)據(jù)來源于其它子程序,這里做檢查是為了防止程序內(nèi)部產(chǎn)生了非預(yù)期的數(shù)據(jù)。
- 決定如何處理錯誤的輸入數(shù)據(jù)。根據(jù)項目需求,你可以返回錯誤碼,記錄日志,返回一個默認(rèn)的合法值或返回與前次相同的數(shù)據(jù),具體方案視需求而定。
第一點和第二點都是數(shù)據(jù)校驗,第三點是對校驗結(jié)果的處理方式。一切錯誤都來自于輸入輸出。理論上對于所有外部數(shù)據(jù)都要進(jìn)行校驗,因為這些數(shù)據(jù)都是不可靠不確定的,需要通過一個”過濾系統(tǒng)”將其過濾成確定類型的數(shù)據(jù)。這個”過濾系統(tǒng)”就是隔欄(barricade)。在隔欄的外面應(yīng)該使用錯誤處理技術(shù),在內(nèi)部應(yīng)該使用斷言。因為隔欄內(nèi)部的數(shù)據(jù)都是被清理過的,如果在內(nèi)部出錯那應(yīng)該是程序的錯誤而非數(shù)據(jù)的錯誤。
還有一種容錯方式叫異常。異常是把代碼中得錯誤或異常事件傳遞給調(diào)用方代碼的一種特殊手段。異常跟斷言的使用情景相似,都是用來處理那些罕見或者永遠(yuǎn)不應(yīng)該發(fā)生得情況。書中給出了使用異常的一些建議:
- 用異常通知程序的其他部分,進(jìn)行錯誤消息傳遞。
- 只有在其他編碼方式無法解決的情況下才使用異常。
- 不要把本可在局部處理的錯誤當(dāng)成一個未捕獲的異常拋出去。
- 避免使用空得catch語句,這是一種不負(fù)責(zé)任的寫法。
- 了解所有函數(shù)庫可能拋出的異常。
- 建立一套幾種的異常處理機(jī)制。
- 考慮異常的替換方案,確保你的程序是真的需要處理異常。
過度的防御式編程會使程序變得臃腫緩慢,增加軟件的復(fù)雜度,變得難以維護(hù)。所以在進(jìn)行編碼時呀考慮好什么地方需要防御,然后調(diào)整優(yōu)先級,因地制宜。