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_STUDENT
比7
還要好找,命名變數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
2int abc;
float def;不要使用一行文,包括單行while加上分號,很容易搞錯
這邊注意if如果是一行,也許加上大括號會比較好,常常會在下方加上其他敘述卻忘了加括號,這樣會有不同的行為
物件及資料結構
不要幫每個private都加上get, set,應該要思考怎樣才能隱藏更多資訊
例如
1 | FuelTankCapacityInGallons(){ |
會差於
1 | publice interface Vehicle{ |
因為使用者知道怎麼計算油量
資料與物件有反對稱性
結構化的程式碼容易添加新函式,而不需變更原有的資料結構
物件導向的程式碼容易添加新類別,而不用變動已有函式
錯誤處理
- 如前所說,作者建議用例外事件而非錯誤碼
- 不要使用java中的checked exception,因為如果底層函式被修改,會強制所有被呼叫的函式都要做錯誤檢查,會破壞函式封閉原則
- 不要回傳null,會讓使用者需要做額外判斷
- 傳遞null也會讓函式需要做額外判斷
邊界
當使用第三方軟體時,可以考慮製作一個介面來與第三方API銜接,這樣如果有大改動時,我們只需要修改介面即可
單元測試
TDD(Test-Driven Development)有三大準則
- 在撰寫一個單元測試前,不可以撰寫任何產品程式
- 只撰寫剛好無法通過的單元測試,不能編譯也算無法通過
- 只撰寫剛好能通過當前測試失敗的產品程式
以上準則可以讓程式設計師被限制在30s的循環:測試程式和產品程式是一起被撰寫的。
一個測試一個assert,但作者認為不需要嚴格遵守,盡可能少即可
一個測試一個概念
FIRST
- Fast:測試要夠快,能夠快速執行,讓人有意願去執行
- Independent:測試程式不應該互相依賴,會讓人更難尋找錯誤
- Repeatable:可以在任何環境下執行
- Self-Validating:要輸出boolean告知程式成功或失敗,而非去看log主觀判斷
- Timely:單元測試要再寫產品程式之前,這樣才能寫出可被測試的程式
類別
- Single Responsibility Principle(SRP):單一職責原則
- 一個類別或模組應該只有一個被修改的理由
- 保持類別的凝聚性(每個變數被多個method使用):這樣會得到許多小class
系統
軟體系統相較於實體系統來說是獨特的,如果我們持續保持適當的關注點分離,軟體系統的架構就能遞增地成長
羽化
遵守四個原則:
- 執行完所有測試:有測試就會消除整理程式碼會破壞程式的恐慌
- 沒有重複的部分
- 表達程式設計師的本意
- 最小化類別和方法的數量:這條守則的優先權是最低的
平行化
- 物件是處理過程的抽象化,執行緒是排程的抽象化
- 平行化是去耦合的抽象化,讓「做什麼」和「什麼時候做」分開
- 另外也可以解決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 - 測試
- 不足夠的測試程式
- 不要跳過簡單的測試
- 對程式錯誤處進行詳細測試
- 測試要夠快速