代碼整潔

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

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

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

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

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

本文記錄閱讀《clean code》之後個人“深有同感”或者“醍醐灌頂”的一些觀點。

一、命名的藝術

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

好的命名應該有下面的特徵:

1.1 名副其實

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

如果需要通過註釋來解釋變量,那麼就先得不那麼名副其實了。

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

# 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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

1.2 避免誤導

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

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

1.3 有意義的區分

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

# bad
def copy(a_list, b_list):
    pass

good

def copy(source, destination):
pass

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

1.4 使用讀的出來的單詞

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

1.5 使用方便搜索的命名

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

1.6 避免思維映射

比如在代碼中寫一個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
  • 5
  • 6

因此,當想要添加註釋的時候,可以想想是否可以通過修改命名,或者修改函數(代碼)的抽象層級來展示代碼的意圖。

當然,也不能因噎廢食,書中指出了以下一些情況屬於好的註釋

  • 法務信息
  • 對意圖的註釋,爲什麼要這麼做
  • 警示
  • TODO註釋
  • 放大看似不合理之物的重要性

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

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

三、函數

3.1 函數的單一職責

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

函數要麼做什麼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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

3.2 函數的抽象層級

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

def pushElephantIntoRefrige():
    openRefrige()
    pushElephant()
    closeRefrige()
  • 1
  • 2
  • 3
  • 4

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

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

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

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

3.3 函數參數

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

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

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

3.4 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準則:

  • 快速(Fast)測試應該夠快,儘量自動化。
  • 獨立(Independent) 測試應該應該獨立。不要相互依賴
  • 可重複(Repeatable) 測試應該在任何環境上都能重複通過。
  • 自我驗證(Self-Validating) 測試應該有bool輸出。不要通過查看日誌這種低效率方式來判斷測試是否通過
  • 及時(Timely) 測試應該及時編寫,在其對應的生產代碼之前編寫
發佈了65 篇原創文章 · 獲贊 63 · 訪問量 2997
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章