clean code

這邊記錄一下我看clean code的筆記整理。不過我得承認其實書裡很多做法我是不太贊同的,例如function要壓在20行以內這種。在C要這樣實踐很有難度,而且我看很多open source的project也都沒有遵循這樣的規範。但是這本書還是很有參考價值的,至少會讓自己反省寫code時有沒有犯了這些問題。

書中提到一個概念:

程式設計師有大半時間花在看code,因此好閱讀的code對快速寫程式是有幫助的

我想這就是我從書裡得到最大的收穫吧!

有意義的名稱

  • 讓程式碼的隱含性高,名稱和意圖符合

  • 避免誤導
    讓變數不會相似高(autocomplete好找)
    像是l和1,0和O

  • 產生有意義的區別
    例如a1,a2、ProductInfo,ProductData,或是table, variable等等都看不出差別

  • 使用能念出來的名稱
    人類大腦有一大部分都專注在字詞的概念,字詞是可發音的,方便理解
    方便同事間彼此溝通

  • 使用可被搜尋的名字
    使用單一字母變數或是數值常數很難被找到
    例如MAX_CLASSES_PER_STUDENT7還要好找,命名變數e幾乎每段程式碼都會用到
    一般來說,長命名比短命名好,命名長度應該和scope作對應,範圍越大的應該要用比較容易識別的名字

  • 避免編碼
    這邊的編碼是指形態或視野,作者不推薦使用匈牙利標誌法,因為那是早期編譯器不會進形型態檢查,但是現在編譯器已經會做了,所以不需要浪費時間做這件事

    可是有時候在看code,有提示還是比較容易記憶的,見仁見智

  • 成員的字首
    不需要在變數前加上類似m_

  • 類別方法命名
    類別:要用名詞來命名
    方法:用動詞命名,而且accessors要用get開頭、mutators用set、predicates要用is

  • 使用一致性的詞彙
    例如取得方法不要同時有get, fetch, retrieve

  • 將命名放在有意義的上下文
    例如state要配合address看才會知道是州,如果沒辦法配合上下文那就手動加上前綴,例如addrState

  • 不要添加過多無意義資訊
    例如每個變數名稱都加上特定前綴,會讓IDE無法幫你找變數

函式

  • 函式要非常簡短,盡量低於20行
  • 函式內的縮排也不要大過一兩層
  • 做一件的函式是無法被區分成不同段落的,如宣告區、初始區等等
  • 降層準則:程式閱讀應該是由上而下敘事,希望每個函式後面都進接著下一層次的概念。
  • 函式應該做一件事,他們應該把這件事做好,而且他們應該只做這件事。
  • 函式參數最多不要超過三個,越少越好,如果需要超過三個,可以考慮使用物件代替
  • 越多參數要測試會越困難,而且參數有傳達概念的能力,讀者必須去了解它
  • 另外不要用輸出型參數,用回傳值
  • 分離指令和查詢(set和get)
  • 使用例外處理來取代回傳錯誤碼,讓使用者不需要一遇到錯誤碼就必須馬上處理錯誤
  • 結構化程式:雖然Dijkstra提到在一個函式內只能有一個return,不要有break和continue,以及goto,但是作者認為在小函式時可以適當使用,但是不應該用goto,因為只有大函式goto才有用處

註解

  • 適當使用註解是用來彌補我們用程式碼表達意圖的失敗
  • 沒有提供比程式碼更多資訊或是反而誤導的註解就是壞註解,當然過多資訊也是
  • git已經提供修改紀錄,不該寫在註解中,包括程式碼的註解也應該拿掉

適合用註解的時機

  • 法律型註解
  • 資訊型的註解
  • 對意圖的解釋
  • 闡明
  • 對後果的告誡
  • TODO
  • 放大重要性
  • Javadoc

編排

每個檔案應該大部分用200行,最多不超過500行

垂直距離

  • 程式中,應該要用垂直空白區分思緒(兩個函式間),而且如果兩個函式密且相關的話,不要放太多註解在兩者之間
  • 變數宣告:變數的宣告應該盡可能靠近變數被使用的地方
  • 實體變數(instance):盡量集中在最上方或最下方,方便被查詢
  • 相依的函式應該放在同一區塊,相依性越高,彼此的垂直距離就要越短

水平距離

  • 通常程式設計師偏好短的程式碼,不一定要80,但不要超過120

  • 水平空白也可以用來表示優先權,例如乘法之間不要有空白,加減法則要

  • 水平的對齊也沒有必要,因為自動化編排程式會毀掉這樣的對齊,而且這樣會讓人忽略變數型態,只關注上下的對齊

    不過像define我就覺得有必要對齊,這樣方便觀看

    1
    2
    int    abc;
    float def;
  • 不要使用一行文,包括單行while加上分號,很容易搞錯

    這邊注意if如果是一行,也許加上大括號會比較好,常常會在下方加上其他敘述卻忘了加括號,這樣會有不同的行為

物件及資料結構

不要幫每個private都加上get, set,應該要思考怎樣才能隱藏更多資訊
例如

1
2
3
FuelTankCapacityInGallons(){
double getGallonsOfGasoline();
}

會差於

1
2
3
publice interface Vehicle{
double getPercentFuelRemaining();
}

因為使用者知道怎麼計算油量

資料與物件有反對稱性

結構化的程式碼容易添加新函式,而不需變更原有的資料結構
物件導向的程式碼容易添加新類別,而不用變動已有函式

錯誤處理

  • 如前所說,作者建議用例外事件而非錯誤碼
  • 不要使用java中的checked exception,因為如果底層函式被修改,會強制所有被呼叫的函式都要做錯誤檢查,會破壞函式封閉原則
  • 不要回傳null,會讓使用者需要做額外判斷
  • 傳遞null也會讓函式需要做額外判斷

邊界

當使用第三方軟體時,可以考慮製作一個介面來與第三方API銜接,這樣如果有大改動時,我們只需要修改介面即可

單元測試

TDD(Test-Driven Development)有三大準則

  1. 在撰寫一個單元測試前,不可以撰寫任何產品程式
  2. 只撰寫剛好無法通過的單元測試,不能編譯也算無法通過
  3. 只撰寫剛好能通過當前測試失敗的產品程式
    以上準則可以讓程式設計師被限制在30s的循環:測試程式和產品程式是一起被撰寫的。

一個測試一個assert,但作者認為不需要嚴格遵守,盡可能少即可
一個測試一個概念

FIRST

  • Fast:測試要夠快,能夠快速執行,讓人有意願去執行
  • Independent:測試程式不應該互相依賴,會讓人更難尋找錯誤
  • Repeatable:可以在任何環境下執行
  • Self-Validating:要輸出boolean告知程式成功或失敗,而非去看log主觀判斷
  • Timely:單元測試要再寫產品程式之前,這樣才能寫出可被測試的程式

類別

  • Single Responsibility Principle(SRP):單一職責原則
  • 一個類別或模組應該只有一個被修改的理由
  • 保持類別的凝聚性(每個變數被多個method使用):這樣會得到許多小class

系統

軟體系統相較於實體系統來說是獨特的,如果我們持續保持適當的關注點分離,軟體系統的架構就能遞增地成長

羽化

遵守四個原則:

  1. 執行完所有測試:有測試就會消除整理程式碼會破壞程式的恐慌
  2. 沒有重複的部分
  3. 表達程式設計師的本意
  4. 最小化類別和方法的數量:這條守則的優先權是最低的

平行化

  • 物件是處理過程的抽象化,執行緒是排程的抽象化
  • 平行化是去耦合的抽象化,讓「做什麼」和「什麼時候做」分開
  • 另外也可以解決response time和throughput的限制

迷思

X 平行化總是能改善效能
X 撰寫平行化程式並不需要修改原有的設計
O 平行化會帶來額外負擔
O 正確的平行化是複雜的
O 平行化的錯誤通常不容易重複出現

平行化防禦方法

  • 單一職責原則(SRP):保持平行化程式碼與其他程式有清楚劃分
  • 限制資料視野:嚴格限制共享資料的存取次數
  • 使用資料的副本:複製唯讀副本,減少共享資料次數
  • 執行緒盡可能獨立運行
  • 另外也要保持同步區塊的簡短,因為cost高
  • 優雅的關閉平行化程式碼,注意能讓子thread可以關閉

平行化 - 測試

  • 不要因為系統後來通過測試就忽略失敗,因為未來會有越來越多錯誤建立在有缺陷的基礎上
  • 先讓非執行緒的程式碼可以運作,不要兩者同時debug
  • 讓執行緒是可以自我調校
  • 執行比處理器還多的執行緒,bug會越容易找到
  • 在不同平台執行
  • 調整程式碼,使其產生失敗

Bad Smell

Bad Smell - 註解

  • 無效、沒意義、不適當的註解
  • 被註解掉的程式碼

開發環境

  • 需要多個步驟建立專案或系統
  • 需要多個步驟進行測試

Bad Smell - 函式

  • 過多參數
  • 輸出行參數
  • 旗標參數
  • 被遺棄的函式

一般狀況

  • 同份原始碼有多種語言
  • 明顯該有的行為沒被實現(最小驚奇原則)
  • 在邊界上的不正確行為:不要依賴直覺,為邊界條件寫測試程式
  • 無視安全規範:不要關閉編譯器的警告
  • 重複的程式碼:這是最重要的規範之一
  • 在錯誤抽象層次上的程式碼:高層次概念都要在基底類別
  • 基底類別相依於其衍生類別
  • 過多資訊:應該要把暴露在介面上的資訊減少
  • 被遺棄的程式碼
  • 垂直分隔:如前所示
  • 不一致性:維持命名一致性
  • 雜亂程式:保持原始檔整潔
  • 人為耦合:不要將變數隨意宣告,然後就留在某處
  • 特色留戀:類別的方法應該只對同一類別裡的變數和函式感興趣,不應該操作其他類的變數或函式
  • 選擇型參數:在函式尾端加上true, false
  • 模糊的意圖:盡可能讓程式碼有表達力
  • 錯置的職責:像是要把常數放在讀者認為他應該要在的地方
  • 使用具解釋性的變數:將計算過程拆解成富有意義名稱的暫存變數
  • 函數名稱要說到做到
  • 了解演算法:不是調整函式做到想讓其做到的事,而是確定解決方法是正確,並且讓函式明顯透露出是怎麼運作的
  • 用多型取代if-else和switch-case
  • 遵循標準的慣例:用團隊的慣例
  • 用有名稱的常數取代魔術數字
  • 要精確:不要有模擬兩可的程式碼,精確表達該函式要做什麼
  • 封裝條件判斷:if(shouldBeDeleted(timer))取代if(timer.hasExpired() && !timre.isRecurrent())
  • 避免否定條件判斷
  • 函式只該做一件事
  • 不要隨意:如果程式碼保持一致性,這樣後來修改的人就會按造前面的原則改
  • 封裝邊界條件:如果a+1常被用到,那就用b=a+1取代,這點跟refactoring似乎不太一樣

命名

  • 選擇有描述性的名稱
  • 在適當的抽象層次使用適當的名稱
  • 越大的scope就要用較長的名稱

Bad Smell - 測試

  • 不足夠的測試程式
  • 不要跳過簡單的測試
  • 對程式錯誤處進行詳細測試
  • 測試要夠快速