Clean Code 心得

一些重要章節的筆記

因為公司有這本書的中文版,在各種等待時間,慢慢把這本看完了,同時也一邊寫些自己認為的重點。

其實我覺得 Clean Code 適合的閱讀對象,是從來沒有接觸過任何 Coding Style 的人。

如果你跟我一樣,有和別人合作過,平常有遵守一些準則(例如 PEP8),或是使用包含 Styling 的 linter (pylint / eslint / rubocop …),這本書還是能找到能學習的地方,例如命名原則、封裝的部分、模組的拆分、測試撰寫等等,這些開發準則以及自動化的 formatter 沒辦法告訴你的部分。

也難怪第一次聽到別人推薦大家看這本書的時候,把它歸類在「選讀」,卻又說裡面的內容很重要。因為很多觀念在現在已經是日常了。

比較後面的章節,從系統、平行化開始,討論了一些比較深入的知識,Java code 也開始變難懂了,而持續精鍊、JUnit 等章節,就真的是針對 Java 的實例做說明,而非原則介紹的部分,這些對我來說讀起來就比較吃力一點。而平常會聽到大家討論的,也都是前面章節討論一些基本原則的部分。因此對於時間比較少的人或許可以先點到為止。

我看網路上的心得也很少人真的讀完後面 XD

列舉一下我覺得比較重要的章節:

  • Chp. 1 ~ Chp.10
  • Chp. 12 簡單的總結
  • Chp. 17 一個清單,其實包含了 Chp. 14-16 的一些啟發

我認為重要順序是倒著的,完全沒時間可以先看 Chp. 17

另外 Chp. 11 其實也蠻重要的,但是裡面太多 Java 的部分了,如果以後學 Java 再來細看吧。

大概是這樣,下面就附上我紀錄的每章節重點吧。


前言

Clean code 是一門學派,不是絕對正確

減少閱讀程式碼的時間

雜亂程式會拖累開發時間

讓程式清楚明白,不要隱藏意圖,避免 anti-pattern

童子軍規則:離開營地前,讓營地比使用前更乾淨

變數命名

  • 類別:名詞,方法:動詞
  • 避免無意義的字:the, variable, name
  • 避免跟形態衝突的字(會誤導),除非他真的是那個形態:list, array
    • 或是省去:Accounts > AccountList
  • 能搜尋
  • 能唸出來
  • 意義區別
    • e.g. source, destination 取代 a1, a2
  • 同一概念統一字詞
  • 在命名內加入 context,或是用 Class 包起來

善用詞性

類別:名詞
方法:動詞

函式

  • 多型 > switch case
  • 參數越少越好
    • 可以把參數包在類別裡,例如 point 取代 x, y
  • No side effect
    • 不要做名字以外的事情
  • 讓物件改變自己,而不是讓函式輸入物件
  • 查改分離(Commend / Query 分離)
  • 例外處理取代 if 回傳錯誤碼
    • try…finally block 應該獨佔函式(就是一件事)

Single Responsibility Principle, SRP 單一職責原則

一個類別或一個模組,應該只有一個讓你去修改他的理由(一種職責)

要能有簡潔的命名,否則就該拆開成兩個類別 / 兩個模組

  • 只有一個層次的抽象概念
  • 不要傳 flag(true/false)進函數讓他做兩件事

Open Closed Principle, OCP 開放閉合原則

要設計得易於延展(例如:繼承),不用因新型態、新需求加入而改變原本的部分

對擴展開放,對修改封閉

註解

彌補程式碼表達意圖的失敗
註解容易隨著時間失真,常常會沒有跟著程式碼一起改到(只有程式碼是真的

-> 透過修改程式碼來移除註解

使用 git 等版本控制系統後,就不需要的註解:版本日誌 / 程式碼修改 / 作者等資訊

註解應該最少,只留必要的註解

  • 法律型註解
  • 資訊型註解
    • function doc(產生文件的註解)
    • formatter 會挑出的格式
  • 意圖的解釋
    • 一段複雜 / magic code 是為了解決什麼
  • 後果告誡
    • e.g. 為什麼不跑這個測試
    • thread unsafety
  • TODO
  • 放大重要性:"下面這行很重要,因為…"

編排

寫 code,像編排一頁報紙

Vertical

  • 空行分隔:package / class / function
  • 關聯的 code 要連續(script / function …)
    • 互相呼叫 / 引用
    • Concept 相近
  • local variable 宣告盡可能靠近使用的地方
  • Instance variable 宣告在最上方 (也可以依照 C++ 的 Scissors rule 放在最下方)

Horizontal

  • 寬度 ~100-120 以下,以不要捲動畫面為主
  • assignment 兩端空白
    • user_name = 'david'
  • 用空白來強調運算子的優先序
    • return b*b - 4*a*c
  • 不要水平對齊,會需要水平對齊代表列表太長

物件與資料結構

  • OOP 容易添加新的類別(duck-typing)
  • Struct 容易添加新的 function

The Law of Demeter / Principle of Least Knowledge

Loose coupling

Abstraction: 模組不該知道它所操作的物件的內部運作

火車事故 (train wreck)

一連串的連續呼叫,對於物件 get_options 內部理解太深

output_dir = obj.get_options().get_scratch_dir().get_absolute_path()

Clean Code 認為違反了 Law of Demeter,以下列方式分割:

opt = obj.get_options()
scratch_dir = opt.get_scratch_dir()
output_dir = scratch_dir.get_absolute_path()

資料傳輸物件 Data Transfer Objects (DTO)

只有 public variable
沒有 method

e.g. active record

作者還批評了一般 Model 寫法,說應該要把 Bussiness rule 和 DTO 分開 XD

錯誤處理

  • 提供 error message
  • 從 caller 的角度,定義 Exception 的類別
  • 利用特殊類別配合 duck typing 來處理特殊情況,不用總是用 trycatch

Return Null?

盡量避免,改成:

  • empty array for iteration
  • 在函式內部做 assertion 來避免會造成 null 的結果

Interface

你的 code 和 3rd-party 的 code 的 interface

最小化會依賴於 3rd-party 的部分

Adapter

  • 把 3rd-party 封裝起來,不要直接在 code 內傳遞他

Unit Test

測試也要好維護

讓你的測試變得很好閱讀:封裝細節

Test-Driven Delopment

三大法則

  • 寫測試 -> 寫程式
  • 只寫剛好無法通過的測試
  • 只寫剛好能透過當前測試的程式

Styling

  • Given-When-Then
  • One Assert per example (原則上)
  • One concept per example

F.I.R.S.T.

  • Fast: 測試程式不能跑太慢,不然你會不想跑 XD
  • Independent: 區塊間不應該互相依賴,可以獨立跑某些測試,而且不能因為一個 fail 導致後續的 fail
  • Repeatable: 在 Test / Development / Production 都能跑
  • Self-validating: 通過 / 失敗 要明確,不要把重要結果放 log 裡面
  • Timely: 即時寫測試,不要拖到寫完功能之後

Class

封裝

Keep private 除非需要開放給測試或其他 module

凝聚性

盡量減少你的 instance variables

  • 當一群 Instance variables 在一些 methods 都需要用到時,也許他們該被拆出去變成一個新類別

Dependency Inversion Principle (DIP)

類別要 depends on 抽象概念,不要 depends on 具體細節

System

將所有關注的事分開

每個關注領域分開成不同模組,並以 interface 整合

  • Lazy initialization 同時進行了「initialize」和「執行」,少用
  • 使用一個主程式 Main 來建造物件,並讓物件各司其職
  • 使用 Dependency Injection 來減少元件之間的 Coupling
  • 善用 DSL (Domain-Specific Language) 來增加可讀性

註:這張多講了很多專有名詞,所以我是跳著看的(畢竟我跟 Java 不熟)。

羽化 Emergence

其實就是個小結。

四個簡單準則

重要性由上而下:

  • 執行完所有測試
  • 沒有重複的部分
  • 表達 Programmer 的本意
  • 最少的類別與方法

執行完所有測試

  • 能夠撰寫測試,類別自然較為小型且單一用途
  • 方便重構

平行化

將「做什麼」和「何時做」分離,讓你的 code 可以隨插即用 (跟寫輪眼一樣)

一些聲明

  • 平行化會帶來額外負擔
  • 正確的平行化是複雜的,即使原本的問題很簡單
  • 平行化程式的錯誤不容易重複出現,容易被忽略
  • 常常需要根本性的修改

能夠幫助平行化的原則

  • SRP 單一職責原則
  • 限制資料的視野
  • 使用資料的 copy 而不要直接共享
  • 讓 thread 盡可能獨立運行

當然還有了解當下語言的相關函式庫,撰寫各種順序的測試,讀一下作業系統相關章節等等

最後的清單:程式碼的氣味

一些會讓人修改 code 的原因,只寫下我覺得前面沒記錄到的:

  • 多步驟才能 Build / Test
  • 一個檔案多種語言(例如說不要再 .py 裡面硬寫 HTML)
  • Boundary Condition 壞掉(想起那個二分搜尋法…)
    • 封裝他們
  • 無視安全規範(例如:關掉失敗的測試、警告等)
  • Magic Number: 出現在 Code 裡面的意義不明的數字(給個名字吧)
  • Configuration 要放在高的抽象層
  • Scope 大,變數名稱拉長

延伸閱讀