《編寫可讀代碼的藝術》——表面層次的改進

程序員之間的互相尊重體如今他所寫的代碼中。他們對工做的尊重也體如今那裏javascript

在《Clean Code》一書中Bob大叔認爲在代碼閱讀過程當中人們說髒話的頻率是衡量代碼質量的惟一標準。這也是一樣的道理。java

這樣,代碼最重要的讀者就再也不是編譯器、解釋器或者電腦了,而是人。寫出的代碼能讓人快速理解、輕鬆維護、容易擴展的程序員纔是專業的程序員。c++

代碼應當易於理解

可讀性基本定理:代碼的寫法應當使別人理解它所需的時間最小化程序員

本書的餘下部分將討論如何把「易讀」這條原則應用在不一樣的場景中。可是請記住,當你猶豫不決時,可讀性基本定理老是先於本書中任何其餘條例或原則算法

把信息裝到名字裏

咱們在程序中見到的不少名字都很模糊,例如tmp。就算是看上去合理的詞,如size或者get,也都沒有裝入不少信息。本章會告訴你如何把信息裝入名字中。數據庫

選擇專業的詞

「把信息裝入名字中」包括要選擇很是專業的詞,而且避免使用「空洞」的詞。編程

例如,「get」這個詞就很是不專業,例如在下面的例子中:數組

def GetPage(url): ...

「get」這個詞沒有表達出不少信息。這個方法是從本地的緩存中獲得一個頁面,仍是從數據庫中,或者從互聯網中?若是是從互聯網中,更專業的名字能夠是FetchPage()或者Download-Page()。緩存

下面是一個BinaryTree類的例子:安全

class BinaryTree    
    { 
        int Size();    
        ...
    };

你指望Size()方法返回什麼呢?樹的高度,節點數,仍是樹在內存中所佔的空間?

問題是Size()沒有承載不少信息。更專業的詞能夠是Height()、NumNodes()或者MemoryBytes()。

另一個例子,假設你有某種Thread類:

class Thread    
    { 
        void Stop();    
        ...
    }

Stop()這個名字還能夠,但根據它到底作什麼,可能會有更專業的名字。例如,你能夠叫它Kill(),若是這是一個重量級操做,不能恢復。或者你能夠叫它Pause(),若是有方法讓它Re-sume()。

找到更有表現力的詞

要敢於使用同義詞典或者問朋友更好的名字建議。英語是一門豐富的語言,有不少詞能夠選擇。

下面是一些例子,這些單詞更有表現力,可能適合你的語境:

image

但別忘乎所以。在PHP中,有一個函數能夠explode()一個字符串。這是個頗有表現力的名字,描繪了一幅把東西拆成碎片的景象。但這與split()有什麼不一樣?(這是兩個不同的函數,但很難經過它們的名字來猜出不一樣點在哪裏。)

清晰和精確比裝可愛好。

避免像 tmp 和 retval 這樣泛泛的名字

使用像tmp、retval和foo這樣的名字每每是「我想不出名字」的託辭。與其使用這樣空洞的名字,不如挑一個能描述這個實體的值或者目的的名字。

例如,下面的JavaScript函數使用了retval:

var euclidean_norm = function(v) {
    var retval = 0.0;
    for (var i = 0; i < v.length; i += 1)
        retval += v[i] * v[i];
    return Math.sqrt(retval);
};

當你想不出更好的名字來命名返回值時,很容易想到使用retval。但retval除了「我是一個返回值」外並無包含更多信息(這裏的意義每每也是很明顯的)。

好的名字應當描述變量的目的或者它所承載的值。在本例中,這個變量正在累加v的平方。所以更貼切的名字能夠是sum_squares。這樣就提早聲明瞭這個變量的目的,而且可能會幫忙找到缺陷。

例如,想象若是循環的內部被意外寫成:

retval += v[i];

若是名字換成sum_squares這個缺陷就會更明顯:

sum_squares += v[i]; //咱們要累加的"square"在哪裏?缺陷!

retval這個名字沒有包含不少信息。用一個描述該變量的值的名字來代替它。
若是你要使用像tmp、it或者retval這樣空泛的名字,那麼你要有個好的理由。

用具體的名字代替抽象的名字

在給變量、函數或者其餘元素命名時,要把它描述得更具體而不是更抽象。

例如,假設你有一個內部方法叫作ServerCanStart(),它檢測服務是否能夠監聽某個給定的TCP/IP端口。然而Server-CanStart()有點抽象。CanListenOnPort()就更具體一些。這個名字直接地描述了這個方法要作什麼事情。

爲名字附帶更多信息

咱們前面提到,一個變量名就像是一個小小的註釋。儘管空間不是很大,但無論你在名中擠進任何額外的信息,每次有人看到這個變量名時都會同時看到這些信息。

所以,若是關於一個變量有什麼重要事情的讀者必須知道,那麼是值得把額外的「詞」添加到名字中的。例如,假設你有一個變量包含一個十六進制字符串:

string id; // Example: "af84ef845cd8"

若是讓讀者記住這個ID的格式很重要的話,你能夠把它更名爲hex_id。

帶單位的值

若是你的變量是一個度量的話(如時間長度或者字節數),那麼最好把名字帶上它的單位。例如,這裏有些JavaScript代碼用來度量一個網頁的加載時間:

var start = (new Date()).getTime(); // top of the page
...
var elapsed = (new Date()).getTime() - start; // bottom of the page
document.writeln("Load time was: " + elapsed + " seconds");

這段代碼裏沒有明顯的錯誤,但它不能正常運行,由於get-Time()會返回毫秒而非秒。經過給變量結尾追加_ms,咱們可讓全部的地方更明確:

var start_ms = (new Date()).getTime(); // top of the page...
var elapsed_ms = (new Date()).getTime() - start_ms; // bottom of the page
document.writeln("Load time was: " + elapsed_ms / 1000 + " seconds");

除了時間,還有不少在編程時會遇到的單位。下表列出一些沒有單位的函數參數以及帶單位的版本:

函數參數 帶單位的參數
Start(int delay) delay → delay_secs
CreateCache(int size) size → size_mb
ThrottleDownload(float limit) limit → max_kbps
Rotate(float angle) angle → degrees_cw

附帶其餘重要屬性

這種給名字附帶額外信息的技巧不只限於單位。在對於這個變量存在危險或者意外的任什麼時候候你都該採用它。

例如,不少安全漏洞來源於沒有意識到你的程序接收到的某些數據尚未處於安全狀態。在這種狀況下,你可能想要使用像 untrustedUrl 或者 unsafeMessageBody 這樣的名字。在調用了清查不安全輸入的函數後,獲得的變量能夠命名爲 trustedUrl 或者 safeMessageBody 。

但你不該該給程序中每一個變量都加上像 unescaped_ 或者 _utf8 這樣的屬性。若是有人誤解了這個變量就很容易產生缺陷,尤爲是會產生像安全缺陷這樣可怕的結果,在這些地方這種技巧最有用武之地。基本上,若是這是一個須要理解的關鍵信息,那就把它放在名字裏

名字應該有多長

在小的做用域裏可使用短的名字

做用域 小的標識符(對於多少行其餘代碼可見)也不用帶上太多信息。也就是說,由於全部的信息(變量的類型、它的初值、如何析構等)都很容易看到,因此能夠用很短的名字。若是一個標識符有較大的做用域,那麼它的名字就要包含足夠的信息以便含義更清楚。

首字母縮略詞和縮寫

因此經驗原則是:團隊的新成員是否能理解這個名字的含義?若是能,那可能就沒有問題。例如,對程序員來說,使用eval來代替evaluation,用doc來代替document,用str來代替string是至關廣泛的。所以若是團隊的新成員看到FormatStr()可能會理解它是什麼意思,然而,理解BEManager可能有點困難。

丟掉沒用的詞

有時名字中的某些單詞能夠拿掉而不會損失任何信息。例如,Convert To String()就不如To String()這個更短的名字,並且沒有丟失任何有用的信息。一樣,不用DoServeLoop(),ServeLoop()也同樣清楚。

利用名字的格式來傳遞含義

對於下劃線、連字符和大小寫的使用方式也能夠把更多信息裝到名字中。對不一樣的實體使用不一樣的格式就像語法高亮顯示的形式同樣,能幫你更容易地閱讀代碼。

不會誤解的名字

要多問本身幾遍:「這個名字會被別人解讀成其餘的含義嗎?」要仔細審視這個名字。

Filter()

假設你在寫一段操做數據庫結果的代碼:

results = Database.all_objects.filter("year <= 2011")

結果如今包含哪些信息?

  • 年份小於或等於2011的對象?
  • 年份不小於或等於2011年的對象?

這裏的問題是「filter」是個二義性單詞。咱們不清楚它的含義究竟是「挑出」仍是「減掉」。最好避免使用「filter」這個名字,由於它太容易誤解。

Clip(text, length)

假設你有個函數用來剪切一個段落的內容:

# Cuts off the end of the text, and appends "..." 
def Clip(text, length):  
  ...

你可能會想象到Clip()的兩種行爲方式:

  • 從尾部刪除length的長度
  • 截掉最大長度爲length的一段

第二種方式(截掉)的可能性最大,但仍是不能確定。與其讓讀者亂猜代碼,還不如把函數的名字改爲Truncate(text,length)

然而,參數名length也不太好。若是叫max_length的話可能會更清楚。這樣也尚未完。就算是max_length這個名字也仍是會有多種解讀:

  • 字節數
  • 字符數
  • 字數

如你在前一章中所見,這屬於應當把單位附加在名字後面的那種狀況。在本例中,咱們是指「字符數」,因此不該該用max_length,而要用max_chars。

推薦用min和max來表示(包含)極限

命名極限最清楚的方式是在要限制的東西前加上max_或者min_。

推薦用first和last來表示包含的範圍

下面是另外一個例子,你無法判斷它是「少於」仍是「少於且包含」:

print integer_range(start=2, stop=4)
# Does this print [2,3] or [2,3,4] (or something else)?

儘管start是個合理的參數名,但stop能夠有多種解讀。對於這樣包含的範圍(這種範圍包含開頭和結尾),一個好的選擇是first/last。

例如:

set.PrintKeys(first="Bart", last="Maggie")

不像stop,last這個名字明顯是包含的。除了first/last,min/max這兩個名字也適用於包含的範圍,若是它們在上下文中「聽上去合理」的話。

推薦用begin和end來表示包含/排除範圍

對於命名包含/排除範圍典型的編程規範是使用begin/end。

可是end這個詞有點二義性。例如,在句子「我讀到這本書的end部分了」,這裏的end是包含的。遺憾的是,英語中沒有一個合適的詞來表示「恰好超過最後一個值」。

由於對begin/end的使用是如此常見(至少在C++標準庫中是這樣用的,還有大多數須要「分片」的數組也是這樣用的),它已是最好的選擇了。

給布爾值命名

一般來說,加上像is、has、can或should這樣的詞,能夠把布爾值變得更明確。

例如,SpaceLeft() 函數聽上去像是會返回一個數字,若是它的本意是返回一個布爾值,可能 HasSapceLeft() 個這名字更好一些。

最後,最好避免使用反義名字。

例如,不要用:bool disable_ssl = false;

而更簡單易讀(並且更緊湊)的表示方式是:bool use_ssl = true;

與使用者的指望相匹配

有些名字之因此會讓人誤解是由於用戶對它們的含義有先入爲主的印象,就算你的本意並不是如此。在這種狀況下,最好放棄這個名字而改用一個不會讓人誤解的名字。

get*()

不少程序員都習慣了把以get開始的方法當作輕量級訪問器這樣的用法,它只是簡單地返回一個內部成員變量。若是違背這個習慣極可能會誤導用戶。

如下是一個用Java寫的例子,請不要這樣作:

public class StatisticsCollector {
    public void addSample(double x) {}

    public double getMean() {
        // Iterate through all samples and return total / num_samples 
    }
}

在這個例子中,getMean()的實現是要遍歷全部通過的數據並同時計算中值。若是有大量的數據的話,這樣的一步可能會有很大的代價!但一個容易輕信的程序員可能會隨意地調用get-Mean(),還覺得這是個沒什麼代價的調用。

相反,這個方法應當重命名爲像computeMean()這樣的名字,後者聽起來更像是有些代價的操做。(另外一種作法是,用新的實現方法使它真的成爲一個輕量級的操做。)

list::size()

下面是一個來自C++標準庫中的例子。曾經有個很難發現的缺陷,使得咱們的一臺服務器慢得像蝸牛在爬,就是下面的代碼形成的:

void ShrinkList( list<Node> & list, int max_size )
{
    while ( list.size() > max_size )
    {
        FreeNode( list.back() );        
        list.pop_back();
    }
}

這裏的「缺陷」是,做者不知道list.size()是一個O(n)操做——它要一個節點一個節點地歷數列表,而不是隻返回一個事先算好的個數,這就使得ShrinkList()成了一個O(n2)操做。

這段代碼從技術上來說「正確」,事實上它也經過了全部的單元測試。但當把ShrinkList()應用於有100萬個元素的列表上時,要花超過一個小時來完成!

可能你在想:「這是調用者的錯,他應該更仔細地讀文檔。」有道理,但在本例中,list.size()不是一個固定時間的操做,這一點是出人意料的。全部其餘的C++容器類的size()方法都是時間固定的。

假使 size() 的名字是 countSize() 或者 countElements() ,極可能就會避免相同的錯誤。C++標準庫的做者多是但願把它命名爲 size() 以和全部其餘的容器一致,就像 vector 和 map 。可是正由於他們的這個選擇使得程序員很容易誤把它當成一個快速的操做,就像其餘的容器同樣。謝天謝地,如今最新的C++標準庫把size()改爲了O(1)。

總結

不會誤解的名字是最好的名字——閱讀你代碼的人應該理解你的本意,而且不會有其餘的理解。遺憾的是,不少英語單詞在用來編程時是多義性的,例如 filter、length和limit。

在你決定使用一個名字之前,要吹毛求疵一點,來想象一下你的名字會被誤解成什麼。最好的名字是不會誤解的。

  • 當要定義一個值的上限或下限時,max_和min_是很好的前綴。
  • 對於包含的範圍,first和last是好的選擇。
  • 對於包含/排除範圍,begin和end是最好的選擇,由於它們最經常使用。
  • 當爲布爾值命名時,使用is和has這樣的詞來明確表示它是個布爾值,避免使用反義的詞(例如disable_ssl)。
  • 要當心用戶對特定詞的指望。例如,用戶會指望get()或者size()是輕量的方法。

審美

好的源代碼應當「看上去養眼」。本章會告訴你們如何使用好的留白、對齊及順序來讓你的代碼變得更易讀。確切地說,有三條原則:

  • 使用一致的佈局,讓讀者很快就習慣這種風格。
  • 讓類似的代碼看上去類似。
  • 把相關的代碼行分組,造成代碼塊。

審美與設計

在這裏中,咱們只關注能夠改進代碼的簡單 審美 方法。這些類型的改變很簡單而且經常能大幅地提升可讀性。有時大規模地重構代碼(例如拆分出新的函數或者類)可能會更有幫助。咱們的觀點是好的審美與好的設計是兩種獨立的思想。最好是同時在兩個方向上努力作到更好

用方法來整理不規則的東西

使代碼「看上去漂亮」一般會帶來不限於表面層次的改進,它可能會幫你把代碼的結構作得更好。

選一個有意義的順序,始終一致地使用它

如: React生命週期的順序。

把聲明按塊組織起來

把代碼分紅「段落」

我的風格與一致性

一致的風格比「正確」的風格更重要。

該寫什麼樣的註釋

註釋的目的是儘可能幫助讀者瞭解得和做者同樣多。

什麼不須要註釋

不要爲那些從代碼自己就能快速推斷的事實寫註釋。

不要爲了註釋而註釋

不要給很差的名字加註釋——應該把名字改好

一個好的名字比一個好的註釋更重要,由於在任何用到這個函數的地方都能看獲得它。

一般來說,你不須要「柺杖式註釋」——試圖粉飾可讀性差的代碼的註釋。寫代碼的人經常把這條規則表述成:好代碼>壞代碼+好註釋。

記錄你的思想

不少好的註釋僅經過「記錄你的想法」就能獲得,也就是那些你在寫代碼時有過的重要想法。

加入「導演評論」

電影中常有「導演評論」部分,電影製做者在其中給出本身的看法而且經過講故事來幫助你理解這部電影是如何製做的。一樣,你應該在代碼中也加入註釋來記錄你對代碼有價值的看法。

爲代碼中的瑕疵寫註釋

代碼始終在演進,而且在這過程當中確定會有瑕疵。不要很差意思把這些瑕疵記錄下來。
例如,當代碼須要改進時:

// TODO: 採用更快算法

或者當代碼沒有完成時:

// TODO(dustin):處理除JPEG之外的圖像格式

有幾種標記在程序員中很流行:

  • 標記一般的意義TODO:我尚未處理的事情
  • FIXME:已知的沒法運行的代碼
  • HACK:對一個問題不得不採用的比較粗糙的解決方案
  • XXX:危險!這裏有重要的問題

重要的是你應該能夠隨時把代碼未來應該如何改動的想法用註釋記錄下來。這種註釋給讀者帶來對代碼質量和當前狀態的寶貴看法,甚至可能會給他們指出如何改進代碼的方向。

給常量加註釋

當定義常量時,一般在常量背後都有一個關於它是什麼或者爲何它是這個值的「故事」。

有些常量不須要註釋,由於它們的名字自己已經很清楚(例如SECONDS_PER_DAY)。可是在咱們的經驗中,不少常量能夠經過加註釋得以改進。這不過是匆匆記下你在決定這個常量值時的想法而已。

站在讀者的角度

  • 預料到代碼中哪些部分會讓讀者說:「啊?」而且給它們加上註釋。
  • 爲普通讀者意料以外的行爲加上註釋。
  • 在文件/類的級別上使用「全局觀」註釋來解釋全部的部分是如何一塊兒工做的。
  • 用註釋來總結代碼塊,使讀者不致迷失在細節中。

寫出言簡意賅的註釋

  • 當像「it」和「this」這樣的代詞可能指代多個事物時,避免使用它們。
  • 儘可能精確地描述函數的行爲。
  • 在註釋中用精心挑選的輸入/輸出例子進行說明。
  • 聲明代碼的高層次意圖,而非明顯的細節。
  • 用嵌入的註釋(如Function(/arg =/...))來解釋難以理解的函數參數。
  • 用含義豐富的詞來使註釋簡潔。
相關文章
相關標籤/搜索