• 1摘要
  • 2什麼是好代碼
    • 2.1好代碼的定義
    • 2.2可讀的代碼
      • 2.2.1逐字翻譯
      • 2.2.2遵循約定
      • 2.2.3文檔和註釋
      • 2.2.4推薦閱讀
    • 2.3可發佈的代碼
      • 2.3.1處理異常
      • 2.3.2處理併發
      • 2.3.3優化性能
      • 2.3.4日誌
      • 2.3.5擴展閱讀
    • 2.4可維護的代碼
      • 2.4.1避免重複
      • 2.4.2模塊劃分
      • 2.4.3簡潔與抽象
      • 2.4.4推薦閱讀
  • 3結語

1.摘要

這是爛代碼系列的第二篇,在文章中我會跟你們討論一下如何儘量高效和客觀的評價代碼的優劣。 
在發佈了關於爛代碼的那些事(上)以後,發現這篇文章居然意外的很受歡迎,不少人也描(tu)述(cao)了各自代碼中這樣或者那樣的問題。 
最近部門在組織bootcamp,正好我負責培訓代碼質量部分,在培訓課程中讓你們花了很多時間去討論、改進、完善本身的代碼。雖然剛畢業的同窗對於代碼質量都很用心,但最終呈現出來的質量仍然沒能達到「十分優秀」的程度。 究其緣由,主要是不瞭解好的代碼「應該」是什麼樣的。
html

2.什麼是好代碼

寫代碼的第一步是理解什麼是好代碼。在準備bootcamp的課程的時候,我就爲這個問題犯了難,我嘗試着用一些精確的定義區分出「優等品」、「良品」、「不良品」;可是在總結的過程當中,關於「什麼是好代碼」的描述卻大多沒有可操做性java

2.1.好代碼的定義

隨便從網上搜索了一下「優雅的代碼」,找到了下面這樣的定義:linux

Bjarne Stroustrup,C++之父:git

  • 邏輯應該是清晰的,bug難以隱藏;
  • 依賴最少,易於維護;
  • 錯誤處理徹底根據一個明確的策略;
  • 性能接近最佳化,避免代碼混亂和無原則的優化;
  • 整潔的代碼只作一件事。

Grady Booch,《面向對象分析與設計》做者:程序員

  • 整潔的代碼是簡單、直接的;
  • 整潔的代碼,讀起來像是一篇寫得很好的散文;
  • 整潔的代碼永遠不會掩蓋設計者的意圖,而是具備少許的抽象和清晰的控制行。

Michael Feathers,《修改代碼的藝術》做者:github

  • 整潔的代碼看起來老是像很在意代碼質量的人寫的;
  • 沒有明顯的須要改善的地方;
  • 代碼的做者彷佛考慮到了全部的事情。

看起來彷佛說的都頗有道理,但是實際評判的時候卻難以參考,尤爲是對於新人來講,如何理解「簡單的、直接的代碼」或者「沒有明顯的須要改善的地方」?web

而實踐過程當中,不少同窗也確實面對這種問題:對本身的代碼老是處在一種內心不踏實的狀態,或者是本身以爲很好了,可是卻被其餘人認爲很爛,甚至有幾回我和新同窗由於代碼質量的標準一連討論好幾天,卻誰也說服不了誰:咱們都堅持本身對於好代碼的標準纔是正確的。算法

在經歷了無數次code review以後,我以爲這張圖彷佛總結的更好一些:數據庫

code review

代碼質量的評價標準某種意義上有點相似於文學做品,好比對小說的質量的評價主要來自於它的讀者,由個體主觀評價造成一個相對客觀的評價。並非依靠字數,或者做者使用了哪些修辭手法之類的看似徹底客觀但實際沒有什麼意義的評價手段。編程

但代碼和小說還有些不同,它實際存在兩個讀者:計算機和程序員。就像上篇文章裏說的,即便全部程序員都看不懂這段代碼,它也是能夠被計算機理解並運行的。

因此對於代碼質量的定義我須要於從兩個維度分析:主觀的,被人類理解的部分;還有客觀的,在計算機裏運行的情況。

既然存在主觀部分,那麼就會存在個體差別,對於同一段代碼評價會由於看代碼的人的水平不一樣而得出不同的結論,這也是大多數新人面對的問題:他們沒有一個能夠執行的評價標準,因此寫出來的代碼質量也很難提升。

有些介紹代碼質量的文章講述的都是傾向或者原則,雖說的很對,可是實際指導做用不大。因此在這篇文章裏我但願儘量把評價代碼的標準用(我自認爲)與實際水平無關的評價方式表示出來。

2.2.可讀的代碼

在權衡好久以後,我決定把可讀性的優先級排在前面:一個程序員更但願接手一個有bug可是看的懂的工程,仍是一個沒bug可是看不懂的工程?若是是後者,能夠直接關掉這個網頁,去作些對你來講更有意義的事情。

2.2.1.逐字翻譯

在不少跟代碼質量有關的書裏都強調了一個觀點:程序首先是給人看的,其次纔是能被機器執行,我也比較認同這個觀點。在評價一段代碼能不能讓人看懂的時候,我習慣讓做者把這段代碼逐字翻譯成中文,試着組成句子,以後把中文句子讀給另外一我的沒有看過這段代碼的人聽,若是另外一我的能聽懂,那麼這段代碼的可讀性基本就合格了。

用這種判斷方式的緣由很簡單:其餘人在理解一段代碼的時候就是這麼作的。閱讀代碼的人會一個詞一個詞的閱讀,推斷這句話的意思,若是僅靠句子沒法理解,那麼就須要聯繫上下文理解這句代碼,若是簡單的聯繫上下文也理解不了,可能還要掌握更多其它部分的細節來幫助推斷。大部分狀況下,理解一句代碼在作什麼須要聯繫的上下文越多,意味着代碼的質量越差。

逐字翻譯的好處是能讓做者能輕易的發現那些只有本身知道的、沒有體如今代碼裏的假設和可讀性陷阱。沒法從字面意義上翻譯出本來意思的代碼大多都是爛代碼,好比「ms表明messageService「,或者「ms.proc()是發消息「,或者「tmp表明當前的文件」。

2.2.2.遵循約定

約定包括代碼和文檔如何組織,註釋如何編寫,編碼風格的約定等等,這對於代碼將來的維護很重要。對於遵循何種約定沒有一個強制的標準,不過我更傾向於遵照更多人的約定。

與開源項目保持風格一致通常來講比較靠譜,其次也能夠遵照公司內部的編碼風格。可是若是公司內部的編碼風格和當前開源項目的風格衝突比較嚴重,每每表明着這個公司的技術傾向於封閉,或者已經有些跟不上節奏了。

可是不管如何,遵照一個約定總比本身創造出一些規則要好不少,這下降了理解、溝通和維護的成本。若是一個項目本身創造出了一些奇怪的規則,可能意味着做者看過的代碼不夠多。

一個工程是否遵循了約定每每須要代碼閱讀者有必定經驗,或者須要藉助checkstyle這樣的靜態檢查工具。若是感受無處下手,那麼大部分狀況下跟着google作應該不會有什麼大問題:能夠參考google code style,其中一部分有對應的中文版

另外,沒有必要糾結於遵循了約定到底有什麼收益,就好像走路是靠左好仍是靠右好同樣,即便得出告終論也沒有什麼意義,大部分約定只要遵照就能夠了。

2.2.3.文檔和註釋

文檔和註釋是程序很重要的部分,他們是理解一個工程或項目的途徑之一。二者在某些場景下定位會有些重合或者交叉(好比javadoc實際能夠算是文檔)。

對於文檔的標準很簡單,能找到、能讀懂就能夠了,通常來講我比較關心這幾類文檔:

  1. 對於項目的介紹,包括項目功能、做者、目錄結構等,讀者應該能3分鐘內大體理解這個工程是作什麼的。
  2. 針對新人的QuickStart,讀者按照文檔說明應該能在1小時內完成代碼構建和簡單使用。
  3. 針對使用者的詳細說明文檔,好比接口定義、參數含義、設計等,讀者能經過文檔瞭解這些功能(或接口)的使用方法。

有一部分註釋實際是文檔,好比以前提到的javadoc。這樣能把源碼和註釋放在一塊兒,對於讀者更清晰,也能簡化很多文檔的維護的工做。

還有一類註釋並不做爲文檔的一部分,好比函數內部的註釋,這類註釋的職責是說明一些代碼自己沒法表達的做者在編碼時的思考,好比「爲何這裏沒有作XXX」,或者「這裏要注意XXX問題」。

通常來講我首先會關心註釋的數量:函數內部註釋的數量應該不會有不少,也不會徹底沒有,我的的經驗值是滾動幾屏幕看到一兩處左右比較正常。過多的話可能意味着代碼自己的可讀性有問題,而若是一點都沒有可能意味着有些隱藏的邏輯沒有說明,須要考慮適當的增長一點註釋了。

其次也須要考慮註釋的質量:在代碼可讀性合格的基礎上,註釋應該提供比代碼更多的信息。文檔和註釋並非越多越好,它們可能會致使維護成本增長。關於這部分的討論能夠參考簡潔部分的內容。

2.2.4.推薦閱讀

  • 《代碼整潔之道》

2.3.可發佈的代碼

新人的代碼有一個比較典型的特徵,因爲缺乏維護項目的經驗,寫的代碼總會有不少考慮不到的地方。好比說測試的時候彷佛沒什麼異常,項目發佈以後才發現有不少意料以外的情況;而出了問題以後不知道從哪下手排查,或者僅能讓系統處於一個並不穩定的狀態,依靠一些巧合勉強運行。

2.3.1.處理異常

新手程序員廣泛沒有處理異常的意識,但代碼的實際運行環境中充滿了異常:服務器會死機,網絡會超時,用戶會胡亂操做,不懷好意的人會惡意攻擊你的系統。

我對一段代碼異常處理能力的第一印象來自於單元測試的覆蓋率。大部分異常難以在開發或者測試環境裏復現,即便有專業的測試團隊也很難在集成測試環境中模擬全部的異常狀況。

而單元測試能夠比較簡單的模擬各類異常狀況,若是一個模塊的單元測試覆蓋率連50%都不到,很難想象這些代碼考慮了異常狀況下的處理,即便考慮了,這些異常處理的分支都沒有被驗證過,怎麼期望實際運行環境中出現問題時表現良好呢?

2.3.2.處理併發

我收到的不少簡歷裏都寫着:精通併發編程/熟悉多線程機制,諸如此類,跟他們聊的時候也說的頭頭是道,什麼鎖啊互斥啊線程池啊同步啊信號量啊一堆一堆的名詞口若懸河。而給應聘者一個實際場景,讓應聘者寫一段很簡單的併發編程的小程序,能寫好的卻很少。

實際上併發編程也確實很難,若是說寫好同步代碼的難度爲5,那麼併發編程的難度能夠達到100。這並非危言聳聽,不少看似穩定的程序,在面對併發場景的時候仍然可能出現問題:好比最近咱們就碰到了一個linux kernel在調用某個系統函數時因爲同步問題而出現crash的狀況。

而是否高質量的實現併發編程的關鍵並非是否應用了某種同步策略,而是看代碼中是否保護了共享資源:

  • 局部變量以外的內存訪問都有併發風險(好比訪問對象的屬性,訪問靜態變量等)
  • 訪問共享資源也會有併發風險(好比緩存、數據庫等)。
  • 被調用方若是不是聲明爲線程安全的,那麼頗有可能存在併發問題(好比java的hashmap)。
  • 全部依賴時序的操做,即便每一步操做都是線程安全的,仍是存在併發問題(好比先刪除一條記錄,而後把記錄數減一)。

前三種狀況可以比較簡單的經過代碼自己分辨出來,只要簡單培養一下本身對於共享資源調用的敏感度就能夠了。

可是對於最後一種狀況,每每很難簡單的經過看代碼的方式看出來,甚至出現併發問題的兩處調用並非在同一個程序裏(好比兩個系統同時讀寫一個數據庫,或者併發的調用了一個程序的不一樣模塊等)。可是,只要是代碼裏出現了不加鎖的,訪問共享資源的「先作A,再作B」之類的邏輯,可能就須要提升警戒了。

2.3.3.優化性能

性能是評價程序員能力的一個重要指標,不少程序員也對程序的性能津津樂道。但程序的性能很難直接經過代碼看出來,每每要藉助於一些性能測試工具,或者在實際環境中執行纔能有結果。

若是僅從代碼的角度考慮,有兩個評價執行效率的辦法:

  • 算法的時間複雜度,時間複雜度高的程序運行效率必然會低。
  • 單步操做耗時,單步耗時高的操做盡可能少作,好比訪問數據庫,訪問io等。

而實際工做中,也會見到一些程序員過於熱衷優化效率,相對的會帶來程序易讀性的下降、複雜度提升、或者增長工期等等。對於這類狀況,簡單的辦法是讓做者說出這段程序的瓶頸在哪裏,爲何會有這個瓶頸,以及優化帶來的收益。

固然,不管是優化不足仍是優化過分,判斷性能指標最好的辦法是用數聽說話,而不是單純看代碼,性能測試這部份內容有些超出這篇文章的範圍,就不詳細展開了。

2.3.4.日誌

日誌表明了程序在出現問題時排查的難易程度,經(jing)驗(chang)豐(cai)富(keng)的程序員大概都會遇到過這個場景:排查問題時就少一句日誌,查不到某個變量的值不知道是什麼,致使死活分析不出來問題到底出在哪。

對於日誌的評價標準有三個:

  • 日誌是否足夠,全部異常、外部調用都須要有日誌,而一條調用鏈路上的入口、出口和路徑關鍵點上也須要有日誌。
  • 日誌的表達是否清晰,包括是否能讀懂,風格是否統一等。這個的評價標準跟代碼的可讀性同樣,不重複了。
  • 日誌是否包含了足夠的信息,這裏包括了調用的上下文、外部的返回值,用於查詢的關鍵字等,便於分析信息。

對於線上系統來講,通常能夠經過調整日誌級別來控制日誌的數量,因此打印日誌的代碼只要不對閱讀形成障礙,基本上都是能夠接受的。

2.3.5.擴展閱讀

  • 《Release It!: Design and Deploy Production-Ready Software》(不要看中文版,翻譯的實在是太爛了)
  • Numbers Everyone Should Know

2.4.可維護的代碼

相對於前兩類代碼來講,可維護的代碼評價標準更模糊一些,由於它要對應的是將來的狀況,通常新人很難想象如今的一些作法會對將來形成什麼影響。不過根據個人經驗,通常來講,只要反覆的提問兩個問題就能夠了:

  • 他離職了怎麼辦?
  • 他沒這麼作怎麼辦?

2.4.1.避免重複

幾乎全部程序員都知道要避免拷代碼,可是拷代碼這個現象仍是不可避免的成爲了程序可維護性的殺手。

代碼重複分爲兩種:模塊內重複和模塊間重複。不管何種重複,都在必定程度上說明了程序員的水平有問題,模塊內重複的問題更大一些,若是在同一個文件裏都能出現大片重複的代碼,那表示他什麼難以想象的代碼都有可能寫出來。

對於重複的判斷並不須要反覆閱讀代碼,通常來講現代的IDE都提供了檢查重複代碼的工具,只需點幾下鼠標就能夠了。

除了代碼重複以外,不少熱衷於維護代碼質量的程序員新人很容易出現另外一類重複:信息重複。

我見過一些新人喜歡在每行代碼前面寫一句註釋,好比:

// 成員列表的長度>0而且<200
if(memberList.size() > 0 && memberList.size() < 200) {
    // 返回當前成員列表
    return memberList;
}
看起來似

看起來彷佛很好懂,可是幾年以後,這段代碼就變成了:

// 成員列表的長度>0而且<200
if(memberList.size() > 0 && memberList.size() < 200 || (tmp.isOpen() && flag)) {
    // 返回當前成員列表
    return memberList;
}

再以後可能會改爲這樣:

// edit by axb 2015.07.30
// 成員列表的長度>0而且<200
//if(memberList.size() > 0 && memberList.size() < 200 || (tmp.isOpen() && flag)) {
//     返回當前成員列表
//    return memberList;
//}
if(tmp.isOpen() && flag) {
    return memberList;
}

隨着項目的演進,無用的信息會越積越多,最終甚至讓人沒法分辨哪些信息是有效的,哪些是無效的。

若是在項目中發現好幾個東西都在作同一件事情,好比經過註釋描述代碼在作什麼,或者依靠註釋替代版本管理的功能,那麼這些代碼也不能稱爲好代碼。

2.4.2.模塊劃分

模塊內高內聚與模塊間低耦合是大部分設計遵循的標準,經過合理的模塊劃分可以把複雜的功能拆分爲更易於維護的更小的功能點。

通常來講能夠從代碼長度上初步評價一個模塊劃分的是否合理,一個類的長度大於2000行,或者一個函數的長度大於兩屏幕都是比較危險的信號。

另外一個可以體現模塊劃分水平的地方是依賴。若是一個模塊依賴特別多,甚至出現了循環依賴,那麼也能夠反映出做者對模塊的規劃比較差,從此在維護這個工程的時候頗有可能出現牽一髮而動全身的狀況。

通常來講有很多工具能提供依賴分析,好比IDEA中提供的Dependencies Analysis功能,學會這些工具的使用對於評價代碼質量會有很大的幫助。

值得一提的是,絕大部分狀況下,不恰當的模塊劃分也會伴隨着極低的單元測試覆蓋率:複雜模塊的單元測試很是難寫的,甚至是不可能完成的任務。因此直接查看單元測試覆蓋率也是一個比較靠譜的評價方式。

2.4.3.簡潔與抽象

只要提到代碼質量,必然會提到簡潔、優雅之類的形容詞。簡潔這個詞實際涵蓋了不少東西,代碼避免重複是簡潔、設計足夠抽象是簡潔,一切對於提升可維護性的嘗試實際都是在試圖作減法。

編程經驗不足的程序員每每不能意識到簡潔的重要性,樂於搗鼓一些複雜的玩意並樂此不疲。但複雜是代碼可維護性的天敵,也是程序員能力的一道門檻。

跨過門檻的程序員應該有能力控制逐漸增加的複雜度,總結和抽象出事物的本質,並體現到本身設計和編碼中。一個程序的生命週期也是在由簡入繁到化繁爲簡中不斷迭代的過程。

對於這部分我難以總結出簡單易行的評價標準,它更像是一種思惟方式,除了要理解、還須要練習。多看、多想、多交流,不少時候能夠簡化的東西會大大超出原先的預計。

2.2.4.推薦閱讀

  • 《重構-改善既有代碼的設計》
  • 《設計模式-可複用面向對象軟件的基礎》
  • 《Software Architecture Patterns-Understanding Common Architecture Patterns and When to Use Them》

3.結語

這篇文章主要介紹了一些評價代碼質量優劣的手段,這些手段中,有些比較客觀,有些主觀性更強。以前也說過,對代碼質量的評價是一件主觀的事情,這篇文章裏雖然列舉了不少評價手段。可是實際上,不少我認爲沒有問題的代碼也會被其餘人吐槽,因此這篇文章只能算是初稿,更多內容還須要從此繼續補充和完善。

雖然每一個人對於代碼質量評價的傾向都不同,可是整體來講評價代碼質量的能力能夠被比做程序員的「品味」,評價的準確度會隨着自身經驗的增長而增加。在這個過程當中,須要隨時保持思考、學習和批判的精神。

下篇文章裏,會談一談具體如何提升本身的代碼質量。

[轉自]http://blog.2baxb.me/archives/1378