什麼是整潔的代碼

寫出整潔的代碼,是每一個程序員的追求。《clean code》指出,要想寫出好的代碼,首先得知道什麼是骯髒代碼、什麼是整潔代碼;而後經過大量的刻意練習,才能真正寫出整潔的代碼。程序員

WTF/min是衡量代碼質量的惟一標準,Uncle Bob在書中稱糟糕的代碼爲沼澤(wading),這隻突出了咱們是糟糕代碼的受害者。國內有一個更適合的詞彙:屎山,雖然不是很文雅可是更加客觀,程序員既是受害者也是加害者。算法

對於什麼是整潔的代碼,書中給出了大師們的總結:express

  • Bjarne Stroustrup:優雅且高效;直截了當;減小依賴;只作好一件事
  • Grady booch:簡單直接
  • Dave thomas:可讀,可維護,單元測試
  • Ron Jeffries:不要重複、單一職責,表達力(Expressiveness)

其中,我最喜歡的是表達力(Expressiveness)這個描述,這個詞彷佛道出了好代碼的真諦:用簡單直接的方式描繪出代碼的功能,很少也很多。編程

本文記錄閱讀《clean code》以後我的「深有同感」或者「醍醐灌頂」的一些觀點。架構

本文地址:http://www.javashuo.com/article/p-saqxykyw-bn.htmlapp

命名的藝術

坦白的說,命名是一件困難的事情,要想出一個恰到好處的命名須要一番功夫,尤爲咱們的母語還不是編程語言所通用的英語。不過這一切都是值得了,好的命名讓你的代碼更直觀,更有表達力。less

好的命名應該有下面的特徵:編程語言

名副其實函數

好的變量名告訴你:是什麼東西,爲何存在,該怎麼使用

若是須要經過註釋來解釋變量,那麼就先得不那麼名副其實了。

下面是書中的一個示例代碼,展現了命名對代碼質量的提高

# bad code
def getItem(theList):
   ret = []
   for x in theList:
      if x[0] == 4:
         ret.append(x)
   return ret

# good code
def getFlaggedCell(gameBoard):
   '''掃雷遊戲,flagged: 翻轉'''
   flaggedCells = []
   for cell in gameBoard:
      if cell.IsFlagged():
         flaggedCells.append(cell)
   return flaggedCells

避免誤導

  • 不要掛羊頭賣狗肉
  • 不要覆蓋慣用縮略語

這裏不得不吐槽前兩天才看到的一份代碼,竟然使用了 l 做爲變量名;並且,user竟然是一個list(單複數都沒學好!!)

有意義的區分

代碼是寫給機器執行,也是給人閱讀的,因此概念必定要有區分度。

# bad
def copy(a_list, b_list):
    pass
    
# good
def copy(source, destination):
    pass

使用讀的出來的單詞

若是名稱讀不出來,那麼討論的時候就會像個傻鳥

使用方便搜索的命名

名字長短應與其做用域大小相對應

避免思惟映射

好比在代碼中寫一個temp,那麼讀者就得每次看到這個單詞的時候翻譯成其真正的意義

註釋

表達力的代碼是無需註釋的。

The proper use of comments is to compensate for our failure to express ourself in code.

註釋的適看成用在於彌補咱們用代碼表達意圖時遇到的失敗,這聽起來讓人沮喪,但事實確實如此。The truth is in the code, 註釋只是二手信息,兩者的不一樣步或者不等價是註釋的最大問題。

書中給出了一個很是形象的例子來展現:用代碼來闡述,而非註釋

bad
// check to see if the employee is eligible for full benefit
if ((employee.flags & HOURLY_FLAG) && (employee.age > 65))

good
if (employee.isEligibleForFullBenefits())

所以,當想要添加註釋的時候,能夠想一想是否能夠經過修改命名,或者修改函數(代碼)的抽象層級來展現代碼的意圖。

固然,也不能因噎廢食,書中指出瞭如下一些狀況屬於好的註釋

  1. 法務信息
  2. 對意圖的註釋,爲何要這麼作
  3. 警示
  4. TODO註釋
  5. 放大看似不合理之物的重要性

其中我的最贊同的是第2點和第5點,作什麼很容易經過命名錶達,但爲何要這麼作則並不直觀,特別涉及到專業知識、算法的時候。另外,有些第一感受「不那麼優雅」的代碼,也許有其特殊願意,那麼這樣的代碼就應該加上註釋,說明爲何要這樣,好比爲了提高關鍵路徑的性能,可能會犧牲部分代碼的可讀性。

最壞的註釋就是過期或者錯誤的註釋,這對於代碼的維護者(也許就是幾個月後的本身)是巨大的傷害,惋惜除了code review,並無簡單易行的方法來保證代碼與註釋的同步。

函數

函數的單一職責

一個函數應該只作一件事,這件事應該能經過函數名就能清晰的展現。判斷方法很簡單:看看函數是否還能再拆出一個函數。

函數要麼作什麼do_sth, 要麼查詢什麼query_sth。最噁心的就是函數名錶示只會query_sth, 但事實上卻會do_sth, 這使得函數產生了反作用。好比書中的例子

public class UserValidator {
    private Cryptographer cryptographer;
    public boolean checkPassword(String userName, String password) { 
        User user = UserGateway.findByName(userName);
        if (user != User.NULL) {
            String codedPhrase = user.getPhraseEncodedByPassword(); 
            String phrase = cryptographer.decrypt(codedPhrase, password); 
            if ("Valid Password".equals(phrase)) {
                Session.initialize();
                return true; 
            }
        }
        return false; 
    }
}

函數的抽象層級

每一個函數一個抽象層次,函數中的語句都要在同一個抽象層級,不一樣的抽象層級不能放在一塊兒。好比咱們想把大象放進冰箱,應該是這個樣子的:

def pushElephantIntoRefrige():
    openRefrige()
    pushElephant()
    closeRefrige()

函數裏面的三句代碼在同一個層級(高度)描述了要完成把大象放進冰箱這件事順序相關的三個步驟。顯然,pushElephant這個步驟又可能包含不少子步驟,可是在pushElephantIntoRefrige這個層級,是無需知道太多細節的。

當咱們想經過閱讀代碼的方式來了解一個新的項目時,通常都是採起廣度優先的策略,自上而下的閱讀代碼,先了解總體結構,而後再深刻感興趣的細節。若是沒有對實現細節進行良好的抽象(並凝練出一個名副其實的函數),那麼閱讀者就容易迷失在細節的汪洋裏。

某種程度看來,這個跟金字塔原理也很像

每個層級都是爲了論證其上一層級的觀點,同時也須要下一層級的支持;同一層級之間的多個論點又須要以某種邏輯關係排序。pushElephantIntoRefrige就是中心論點,須要多個子步驟的支持,同時這些子步驟之間也有邏輯前後順序。

函數參數

函數的參數越多,組合出的輸入狀況就愈多,須要的測試用例也就越多,也就越容易出問題。

輸出參數相比返回值難以理解,這點深有同感,輸出參數實在是很不直觀。從函數調用者的角度,一眼就能看出返回值,而很難識別輸出參數。輸出參數一般逼迫調用者去檢查函數簽名,這個實在不友好。

向函數傳入Boolean(書中稱之爲 Flag Argument)一般不是好主意。尤爲是傳入True or False後的行爲並非一件事情的兩面,而是兩件不一樣的事情時。這很明顯違背了函數的單一職責約束,解決辦法很簡單,那就是用兩個函數。

Dont repear yourself

在函數這個層級,是最容易、最直觀實現複用的,不少IDE也難幫助咱們講一段代碼重構出一個函數。

不過在實踐中,也會出現這樣一種狀況:一段代碼在多個方法中都有使用,可是又不徹底同樣,若是抽象成一個通用函數,那麼就須要加參數、加if else區別。這樣就有點尷尬,貌似能夠重構,但又不是很完美。

形成上述問題的某種狀況是由於,這段代碼也違背了單一職責原則,作了不僅一件事情,這才致使很差複用,解決辦法是進行方法的細分,才能更好複用。也能夠考慮template method來處理差別的部分。

測試

很是慚愧的是,在我經歷的項目中,測試(尤爲是單元測試)一直都沒有獲得足夠的重視,也沒有試行過TDD。正由於缺失,才更感良好測試的珍貴。

咱們常說,好的代碼須要有可讀性、可維護性、可擴展性,好的代碼、架構須要不停的重構、迭代,但自動化測試是保證這一切的基礎,沒有高覆蓋率的、自動化的單元測試、迴歸測試,誰都不敢去修改代碼,只能任其腐爛。

即便針對核心模塊寫了單元測試,通常也很隨意,認爲這只是測試代碼,配不上生產代碼的地位,覺得只要能跑通就好了。這就致使測試代碼的可讀性、可維護性很是差,而後致使測試代碼很難跟隨生產代碼一塊兒更新、演化,最後致使測試代碼失效。因此說,髒測試 - 等同於 - 沒測試。

所以,測試代碼的三要素:可讀性,可讀性,可讀性。

對於測試的原則、準則以下:

  • You are not allowed to write any production code unless it is to make a failing unit test pass. 沒有測試以前不要寫任何功能代碼
  • You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures. 只編寫剛好可以體現一個失敗狀況的測試代碼
  • You are not allowed to write any more production code than is sufficient to pass the one failing unit test. 只編寫剛好能經過測試的功能代碼

測試的FIRST準則:

  1. 快速(Fast)測試應該夠快,儘可能自動化。
  2. 獨立(Independent) 測試應該應該獨立。不要相互依賴
  3. 可重複(Repeatable) 測試應該在任何環境上都能重複經過。
  4. 自我驗證(Self-Validating) 測試應該有bool輸出。不要經過查看日誌這種低效率方式來判斷測試是否經過
  5. 及時(Timely) 測試應該及時編寫,在其對應的生產代碼以前編寫
相關文章
相關標籤/搜索