膾炙人口的詩"春有百花秋有月,夏有涼風冬有雪",意境唯美,簡明易懂。好的代碼也是讓人陶醉的,那么如何寫出好的代碼?
高質量代碼有三要素:可讀性、可維護性、可變更性。我們的代碼要一個都不能少地達到了這三要素的要求才能算高質量的代碼。
代碼可讀性的重要性
一提到可讀性似乎有一些老生常談的味道,雖然大家一而再,再而三地強調可讀性,但我們的代碼在可讀性方面依然做得非常糟糕。由于工作的需要,常常需要去閱讀他人的代碼,維護他人設計的模塊。
很多同行在編寫代碼的時候往往只關注一些宏觀上的主題:架構,設計模式,數據結構等等,卻忽視了一些更細節上的點:比如變量如何命名與使用,控制流的設計,以及注釋的寫法等等。以上這些細節上的東西可以用代碼的可讀性來概括。每當看到大段大段、密密麻麻的代碼,而且還沒有任何的注釋時常常感慨不已,深深體會到了這項工作的重要。
由于分工的需要,我們寫的代碼難免需要別人去閱讀和維護的。而對于許多程序員來說,閱讀和維護別人的代碼常有的事,而往往在平常很少關注代碼的可讀性,也對如何提高代碼的可讀性缺乏切身體會。有時即使為代碼編寫了注釋,也常常是注釋語言晦澀難懂形同天書,令閱讀者反復斟酌依然不明其意。
對于一個整體的軟件系統而言,既需要宏觀上的架構決策,設計與指導原則,也必須重視微觀上的代碼細節。在過往中,有許多影響深遠的重大失敗,其根源往往是編碼細節出現了疏漏。
不同于宏觀上的架構,設計模式等需要好幾個類,好幾個模塊才能看出來:代碼的可讀性是能夠立刻從微觀上的,一個變量的命名,函數的邏輯劃分,注釋的信息質量里面看出來的。
宏觀層面上的東西固然重要,但是代碼的可讀性也屬于評價代碼質量的一個無法讓人忽視的指標:它影響了閱讀代碼的成本(畢竟代碼是給人看的),甚至會影響代碼出錯的概率!
對于一個整體的軟件系統而言,既需要宏觀上的架構決策,設計與指導原則,也必須重視微觀上的代碼細節。在軟件歷史中,有許多影響深遠的重大失敗,其根源往往是編碼細節出現了疏漏。所以代碼的可讀性可以作為考量一名程序員專業程度的指標。
怎么理解代碼可讀性
或許已經有很多同行也正在努力提高自己代碼的可讀性。然而這里有一個很典型的錯覺是:越少的代碼越容易讓人理解。
但是事實上,并不是代碼越精簡就越容易讓人理解。相對于追求最小化代碼行數,一個更好的提高可讀性方法是最小化人們理解代碼所需要的時間。
這就引出了這本中的一個核心定理:
可讀性基本定理:代碼的寫法應當使別人理解它所需要的時間最小化。
相對于追求最小化代碼行數,更好的提高可讀性方法是:最小化人們理解代碼所需要的時間。具體如何讓代碼易于理解?主要體現在下面三個層次:
表層上的改進:在命名方法(變量名,方法名),變量聲明,代碼格式,注釋等方面的改進。控制流和邏輯的改進:在控制流,邏輯表達式上讓代碼變得更容易理解。結構上的改進:善于抽取邏輯,借助自然語言的描述來改善代碼。
表層的改進
首先來講最簡單的一層如何改進,涉及到以下幾點:
1.如何命名
我們在命名變量、函數、屬性、類以及包的時候,應當仔細想想,使名稱更加符合相應的功能。我們常常在說,設計一個系統時應當有一個或多個系統分析師對整個系統的包、類以及相關的函數和屬性進行規劃,但在通常的項目中這都非常難于做到。對它們的命名更多的還是程序員來完成。但是,在一個項目開始的時候,應當對項目的命名出臺一個規范。譬如,在我的項目中規定,新增記錄用new或add開頭,更新記錄用edit或mod開頭,刪除用del開頭,查詢用find或query開頭。使用最亂的就是get,因此get開頭的函數僅僅用于獲取類屬性。
關鍵思想:把盡可能多的信息裝入名字中。
1.選擇專業的詞匯,避免泛泛的名字
2.給名字附帶更多信息
3.決定名字最適合的長度
4.名字不能引起歧義
選擇專業的詞匯,避免泛泛的名字
比如get、query等詞最好是用來做輕量級地取方法的開頭,嚴禁使用拼音和英文混合的方式,更不允許直接使用中文的方式。
舉個例子:
getPage(url)
通過這個方法名很難判斷出這個方法是從緩存中獲取頁面數據還是從網頁中獲取。如果是從網頁中獲取,更專業的詞應該是fetchPage(url)或者downloadPage(url)。
還有一個比較常見的反例:returnValue和retval。這兩者都是“返回值”的意思,他們被濫用在各個有返回值的函數里面。其實這兩個次除了攜帶他們本來的意思返回值以外并不具備任何其他的信息,是典型的泛泛的名字。
那么如何選擇一個專業的詞匯呢?答案是在非常貼近你自己的意圖的基礎上,選擇一個富有表現力的詞匯。
舉幾個例子:
相對于make,選擇create,generate,build等詞匯會更有表現力,更加專業。
相對于find,選擇search,extract,recover等詞匯會更有表現力,更加專業。
相對于retval,選擇一個能充分描述這個返回值的性質的名字,例如:
vareuclidean_norm=function(v){ varretval=0.0; for(vari=0;i
這里的retval表示的是“平方的和”,因此sum_squares這個詞更加貼切你的意圖,更加專業。
但是,有些情況下,泛泛的名字也是有意義的,例如一個交換變量的情景:
if(right
像上面這種tmp只是作為一個臨時存儲的情況下,tmp表達的意思就比較貼切了。因此,像tmp這個名字,只適用于短期存在而且特性為臨時性的變量。
給名字附帶更多信息
除了選擇一個專業,貼切意圖的詞匯,我們也可以通過添加一些前后綴來給這個詞附帶更多的信息。這里所指的更多的信息有三種:變量的單位、變量的屬性、變量的格式。
為變量添加單位
有些變量是有單位的,在變量名的后面添加其單位可以讓這個變量名攜帶更多信息:
一個表達時間間隔的變量,它的單位是秒:相對于duraction,ducation_secs攜帶了更多的信息 一個表達內存大小的變量,它的單位是mb:相對于size,cache_mb攜帶了更多的信息。
為變量添加重要屬性
有些變量是具有一些非常重要的屬性,其重要程度是不允許使用者忽略的。例如:
一個UTF-8格式的html字節,相對于html,html_utf8更加清楚地描述了這個變量的格式。一個純文本,需要加密的密碼字符串:相對于password,plaintext_password更清楚地描述了這個變量的特點。
為變量選擇適當的格式
對于命名,有些既定的格式需要注意:
使用大駝峰命名來表示類名:HomeViewController。
使用小駝峰命名來表示屬性名:userNameLabel。
使用下劃線連接詞來表示變量名:product_id。
使用kConstantName來表示常量:kCacheDuraction。
使用MACRO_NAME來表示宏:SCREEN_WIDTH。
決定名字最適合的長度
名字越長越難記住,名字越短所持有的信息就越少,如何決定名字的長度呢?總結有幾個原則:
1.如果變量的作用域很小,可以取很短的名字
2.駝峰命名中的單元不能超過3個
3.不能使用大家不熟悉的縮寫
4.丟掉不必要的單元
如果變量的作用域很小,可以取很短的名字
如果一個變量作用域很小:則可以給它取一個很短的名字也無妨。
看下面這個例子:
if(debug){ mapm; LookUpNamesNumbers(&m); Print(m); }
在這里,變量的類型和使用范圍一眼可見,讀者可以了解這段代碼的所有信息,所以即使是取m這個非常簡短的名字,也不影響讀者來理解作者的意圖。
相反的,如果m是一個全局變量,當你看到下面這段代碼就會很頭疼,因為你不知道它的類型并不明確:
LookUpNamesNumbers(&m); Print(m);
駝峰命名中的單元不能超過3個
我們知道駝峰命名可以很清晰地體現變量的含義,但是當駝峰命名中的單元超過了3個之后,就會很影響閱讀體驗:
userFriendsInfoModelmemoryCacheCalculateTool
是不是看上去很吃力?因為我們大腦同時可以記住的信息非常有限,尤其是在看代碼的時候,這種短期記憶的局限性是無法讓我們同時記住或者瞬間理解幾個具有3~4個單元的變量名的。所以我們需要在變量名里面去除一些不必要的單元:
丟掉不必要的單元
有些單元在變量里面是可以去掉的,例如:
convertToString可以省略成toString。
不能使用大家不熟悉的縮寫
有些縮寫是大家熟知的:
doc 可以代替document
str 可以代替string
但是如果你想用BEManager來代替BackEndManager就比較不合適了。因為不了解的人幾乎是無法猜到這個名稱的意義的。
所以類似這種情況不能偷懶,該是什么就是什么,否則會起到相反的效果。因為它看起來非常陌生,跟我們熟知的一些縮寫規則相去甚遠。
名字不能引起歧義
名字要表達意思明確,不能讓人看不懂。
例如:
filter:過濾這個詞,可以是過濾出符合標準的,也可以是減少不符合標準的:是兩種完全相反的結果,所以不推薦使用。
clip:類似的,到底是在原來的基礎上截掉某一段還是另外截出來某一段呢?同樣也不推薦使用。
布爾值:read_password:是表達需要讀取密碼,還是已經讀了密碼呢?所以最好使用need_password或者is_authenticated來代替比較好。通常來說,給布爾值的變量加上is,has,can,should這樣的詞可以使布爾值表達的意思更加明確
2.如何聲明與使用變量
開發過程中我們會聲明很多變量(成員變量,臨時變量),而我們要知道變量的聲明與使用策略是會對代碼的可讀性造成影響的:
1.變量越多,越難跟蹤它們的動向。
2.變量的作用域越大,就需要跟蹤它們的動向越久。
3.變量改變的越頻繁,就越難跟蹤它的當前值。
相對的,對于變量的聲明與使用,我們可以從這四個角度來提高代碼的可讀性:
1.減少變量的個數
2.縮小變量的作用域
縮短變量聲明與使用其代碼的距離
變量最好只寫一次
沒有價值的臨時變量
有些變量的聲明完全是多此一舉,它們的存在反而加大了閱讀代碼的成本:
letnow=datetime.datatime.now() root_message.last_view_time=now
上面這個now變量的存在是毫無意義的,因為:
沒有拆分任何復雜的表達式
datetime.datatime.now已經很清楚地表達了意思
只使用了一次,因此而沒有壓縮任何冗余的代碼
所以完全不用這個變量也是完全可以的:
root_message.last_view_time=datetime.datatime.now()
表示中間結果的變量
有的時候為了達成一個目標,把一件事情分成了兩件事情來做,這兩件事情中間需要一個變量來傳遞結果。但往往這件事情不需要分成兩件事情來做,這個“中間結果”也就不需要了:
看一個比較常見的需求,一個把數組中的某個值移除的例子:
varremove_value=function(array,value_to_remove){ varindex_to_remove=null; for(vari=0;i
這里面把這個事情分成了兩件事情來做:
找出要刪除的元素的序號,保存在變量index_to_remove里面。
拿到index_to_remove以后使用splice方法刪除它。(這段代碼是JavaScript代碼)
這個例子對于變量的命名還是比較合格的,但實際上這里所使用的中間結果變量是完全不需要的,整個過程也不需要分兩個步驟進行。來看一下如何一步實現這個需求:
varremove_value=function(array,value_to_remove){ for(vari=0;i
上面的方法里面,當知道應該刪除的元素的序號i的時候,就直接用它來刪除了應該刪除的元素并立即返回。
除了減輕了內存和處理器的負擔(因為不需要開辟新的內容來存儲結果變量以及可能不用完全走遍整個的for語句),閱讀代碼的人也會很快領會代碼的意圖。
所以在寫代碼的時候,如果可以“速戰速決”,就盡量使用最快,最簡潔的方式來實現目的。
縮小變量的作用域
變量的作用域越廣,就越難追蹤它,值也越難控制,所以我們應該讓你的變量對盡量少的代碼可見。
比如類的成員變量就相當于一個“小型局部變量”。如果這個類比較龐大,我們就會很難追蹤它,因為所有方法都可以“隱式”調用它。所以相反地,如果我們可以把它“降格”為局部變量,就會很容易追蹤它的行蹤:
//成員變量,比較難追蹤 classLargeCass{ stringstr_; voidMethod1(){ str_=...; Method2(); } voidMethod2(){ //usingstr_ } }
降格:
//局部變量,容易追蹤 classLargeCass{ voidMethod1(){ stringstr=...; Method2(str); } voidMethod2(stringstr){ //usingstr } }
所以在設計類的時候如果這個數據(變量)可以通過方法參數來傳遞,就不要以成員變量來保存它。
縮短變量聲明與使用其代碼的距離
在實現一個函數的時候,我們可能會聲明比較多的變量,但這些變量的使用位置卻不都是在函數開頭。
有一個比較不好的習慣就是無論變量在當前函數的哪個位置使用,都在一開始(函數的開頭)就聲明了它們。這樣可能導致的問題是:閱讀代碼的人讀到函數后半部分的時候就忘記了這個變量的類型和初始值;而且因為在函數的開頭就聲明了好幾個變量,也對閱讀代碼的人的大腦造成了負擔,因為人的短期記憶是有限的,特別是記一些暫時還不知道怎么用的東西。
因此,如果在函數內部需要在不同地方使用幾個不同的變量,建議在真正使用它們之前再聲明它。
變量最好只寫一次
操作一個變量的地方越多,就越難確定它的當前值。所以在很多語言里面有其各自的方式讓一些變量不可變(是個常量),比如C++里的const和Java中的final。
3.如何簡化表達式
有些表達式比較長,很難讓人馬上理解。這時候最好可以將其拆分成更容易的幾個小塊。可以嘗試下面的幾個方法:
1.使用解釋變量
2.使用總結變量
3.使用德摩根定理
使用解釋變量
有些變量會從一個比較長的算式得出,這個表達式可能很難讓人看懂。這時候就需要用一個簡短的“解釋”變量來詮釋算式的含義。使用一個例子:
其實上面左側的表達式其實得出的是用戶名,我們可以用userName來替換它:
使用總結變量
除了以“變量”替換“算式”,還可以用“變量”來替換含有更多變量更復雜的內容,比如條件語句,這時候該變量可以被稱為"總結變量"。使用一個例子:
上面這條判斷語句所判斷的是:“用戶id是否相等”。我們可以使用一個總結性的變量isEqual來替換它:
使用德摩根定理
當我們條件語句里面存在外部取反的情況,就可以使用德摩根定理來做個轉換。
使用例子:
4.如何讓代碼具有美感
在讀過一些好的源碼之后我有一個感受:好的源碼往往都看上去都很漂亮,很有美感。這里說的漂亮和美感不是指代碼的邏輯清晰有條理,而是指感官上的視覺感受讓人感覺很舒服。這是從一種純粹的審美的角度來評價代碼的:富有美感的代碼讓人賞心悅目,也容易讓人讀懂。
為了讓代碼更有美感,采取以下實踐會很有幫助:
1.選擇一個有意義的順序
2.把代碼分成"段落"
3.保持風格一致性
4.不要編寫大段的代碼
用換行和列對齊來讓代碼更加整齊
有些時候,我們可以利用換行和列對齊來讓代碼顯得更加整齊。
換行
換行比較常用在函數或方法的參數比較多的時候。
使用換行:
-(void)requestWithUrl:(NSString*)url method:(NSString*)method params:(NSDictionary*)params success:(SuccessBlock)success failure:(FailuireBlock)failure{ }
不使用換行:
-(void)requestWithUrl:(NSString*)urlmethod:(NSString*)methodparams:(NSDictionary*)paramssuccess:(SuccessBlock)successfailure:(FailuireBlock)failure{ }
通過比較可以看出,如果不使用換行,就很難一眼看清楚都是用了什么參數,而且代碼整體看上去整潔干凈了很多。
列對齊
在聲明一組變量的時候,由于每個變量名的長度不同,導致了在變量名左側對齊的情況下,等號以及右側的內容沒有對齊:
NSString*name=userInfo[@"name"]; NSString*sex=userInfo[@"sex"]; NSString*address=userInfo[@"address"];
而如果使用了列對齊的方法,讓等號以及右側的部分對齊的方式會使代碼看上去更加整潔:
NSString*name=userInfo[@"name"]; NSString*sex=userInfo[@"sex"]; NSString*address=userInfo[@"address"];
這二者的區別在條目數比較多以及變量名稱長度相差較大的時候會更加明顯。
選擇一個有意義的順序
當涉及到相同變量(屬性)組合的存取都存在的時候,最好以一個有意義的順序來排列它們:
讓變量的順序與對應的HTML表單中字段的順序相匹配 從最重要到最不重要排序 按照字母排序
舉個例子:相同集合里的元素同時出現的時候最好保證每個元素出現順序是一致的。除了便于閱讀這個好處以外,也有助于能發現漏掉的部分,尤其當元素很多的時候:
//給model賦值 model.name=dict["name"]; model.sex=dict["sex"]; model.address=dict["address"]; ... //拿到model來繪制UI nameLabel.text=model.name; sexLabel.text=model.sex; addressLabel.text=model.address;
把代碼分成"段落"
在寫文章的時候,為了能讓整個文章看起來結構清晰,我們通常會把大段文字分成一個個小的段落,讓表達相同主旨的語言湊到一起,與其他主旨的內容分隔開來。
而且除了讓讀者明確哪些內容是表達同一主旨之外,把文章分為一個個段落的好處還有便于找到你的閱讀”腳印“,便于段落之間的導航;也可以讓你的閱讀具有一定的節奏感。
其實這些道理同樣適用于寫代碼:如果你可以把一個擁有好幾個步驟的大段函數,以空行+注釋的方法將每一個步驟區分開來,那么則會對讀者理解該函數的功能有極大的幫助。這樣一來,代碼既能有一定的美感,也具備了可讀性。其實可讀性又何嘗不是來自于規則,富有美感的代碼呢?
BigFunction{ //step1:***** .... //step2:***** ... //step3:***** .... }
保持風格一致性
有些時候,你的某些代碼風格可能與大眾比較容易接受的風格不太一樣。但是如果你在你自己所寫的代碼各處能夠保持你這種獨有的風格,也是可以對代碼的可讀性有積極的幫助的。
比如一個比較經典的代碼風格問題:
if(condition){ }
or:
if(condition) { }
對于上面的兩種寫法,每個人對條件判斷右側的大括號的位置會有不同的看法。但是無論你堅持的是哪一個,請在你的代碼里做到始終如一。因為如果有某幾個特例的話,是非常影響代碼的閱讀體驗的。
我們要知道,一個邏輯清晰的代碼也可以因為留白的不規則,格式不對齊,順序混亂而讓人很難讀懂,這是十分讓人痛心的事情。所以既然你的代碼在命名上,邏輯上已經很優秀了,就不妨再費一點功夫把她打扮的漂漂亮亮的吧!
5.如何寫注釋
注釋是每個項目組都在不斷強調的,可是依然有許多的代碼沒有任何的注釋。為什么呢?因為每個項目在開發過程中往往時間都是非常緊的。在緊張的代碼開發過程中,注釋往往就漸漸地被忽略了。注釋的目的是盡量幫助讀者了解得和作者一樣多。
在你寫代碼的時候,在腦海中可能會留下一些代碼里面很難體現出來的部分:這些部分在別人讀你的代碼的時候可能很難體會到。而這些“不對稱”的信息就是需要通過以注釋的方式來告訴閱讀代碼的人。
控制流和邏輯的改進
控制流在編碼中占據著很重要的位置,它往往代表著一些核心邏輯和算法。因此,如果我們可以讓控制流變得看上去更加“自然”,那么就會對閱讀代碼的人理解這些邏輯甚至是整個系統提供很大的幫助。
那么都有哪相關實踐呢?
1.使用符合人類自然語言的表達習慣
2.if/else語句塊的順序
3.使用return提前返回
4.代碼不能寫死
5.預測可能發生的變化
1.使用符合人類自然語言的表達習慣
寫代碼也是一個表達的過程,雖然表現形式不同,但是如果我們能夠采用符合人類自然語言習慣的表達習慣來寫代碼,對閱讀代碼的人理解我們的代碼是很有幫助的。條件語句中參數的順序:首先比較一下下面兩段代碼,哪一個更容易讀懂?
大家習慣上應該會覺得code1容易讀懂。還有條件語句中的正負邏輯:在判斷一些正負邏輯的時候,建議使用if(result)而不是if(!result)。
2.if/else語句塊的順序
在寫if/else語句的時候,可能會有很多不同的互斥情況(好多個else if)。那么這些互斥的情況可以遵循哪些順序呢?
先處理掉簡單的情況,后處理復雜的情況:這樣有助于閱讀代碼的人循序漸進地地理解你的邏輯,而不是一開始就吃掉一個胖子,耗費不少精力。
先處理特殊或者可疑的情況,后處理正常的情況:這樣有助于閱讀代碼的人會馬上看到當前邏輯的邊界條件以及需要注意的地方。
3.使用return提前返回
在一個函數或是方法里,可能有一些情況是比較特殊或者極端的,對結果的產生影響很大(甚至是終止繼續進行)。如果存在這些情況,我們應該把他們寫在前面,用return來提前返回(或者返回需要返回的返回值)。
這樣做的好處是可以減少if/else語句的嵌套,也可以明確體現出:“哪些情況是引起異常的”。
4.代碼不能寫死
目的就是能夠使用中提高代碼可維護性,便于日后的變更。
5.預測可能發生的變化
在開發過程中,如果將一些關鍵參數放到配置文件中,可以為軟件部署和使用帶來更多的靈活性。要做到這一點,要求我們在軟件設計時,應當有更多的意識,考慮到軟件應用中可能發生的變化。就可能方便部署人員在實際部署中進行靈活變化。然而這樣的配置,必要的注釋說明是非常必要的。軟件可維護性的另一層意思就是軟件的設計便于日后的變更。這一層意思與軟件的可變更性是重合的。所有的軟件設計理論的發展,都是從軟件的可變更性這一要求逐漸展開的,它成為了軟件設計理論的核心。
代碼組織的改進
關于代碼組織的改進,以下三種方法:
1.抽取出與程序主要目的“不相關的子邏輯”
2.重新組織代碼使它一次只做一件事情
3.借助自然語言描述來將想法變成代碼
一個函數里面往往包含了其主邏輯與子邏輯,我們應該積極地發現并抽取出與主邏輯不相關的子邏輯。類似于工具方法的函數其實是脫離于某個具體的需求的:它可以用在其他的主函數中,也可以放在其他的項目里面。
結語
遵循一些簡單的規定(規范化指導)能使代碼將更容易閱讀(從而進一步理解、維護和擴展)。個人認為這一點是最重要的,好的程序員都是有強迫癥的,他們會嚴格要求自己,通過不斷的學習來提升自己的技術最終成為大神級別的程序員。如果不能以高標準來要求自己,即使看再多的如何寫出高質量代碼,懂再多的代碼規范,也是沒有用,最終還是會寫出低質量代碼。但是,提高自我要求是一種改變,一般來說,改變都不是一蹴而就的,需要一步一步來。所以,改變最好從小事做起,慢慢積累,最終蛻變。先從代碼規范開始,熟悉代碼規范,遵循規范寫代碼,直到成為習慣,然后再學習其它方法,最終寫出高質量代碼,讓我們一起堅持,且一起行動。
審核編輯:湯梓紅
-
程序
+關注
關注
117文章
3785瀏覽量
81003 -
函數
+關注
關注
3文章
4327瀏覽量
62569 -
代碼
+關注
關注
30文章
4779瀏覽量
68521
原文標題:如何寫出高質量可讀性強的代碼?
文章出處:【微信號:ZYNQ,微信公眾號:ZYNQ】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論