--- title: Clean Code 乾淨的程式碼 tags: code description: clean code --- # Clean Code 乾淨的程式碼 ## 命名 * 取有意義且精準表達的名字,不帶有歧意 * 是處理用途的方法,就用動詞表達(如postForm()...); 變量盡量表達及意(如userAccountName); 類名(class)則以名詞短語為主(如Customer, User...) * 不要使用魔術數(使用像是DEFAULT_WIDTH,而非直接使用100) ## 函數 * 無副作用 * 短小,只做一件事 * 一元最好,二元次之,三元更次之,三元以上建議包裝為物件 * 使用標示性的名稱 * 每個函數一個抽象層級 * 錯誤處理 ## 註解 總之不要寫就對了 :) 讓函數或變數本身就可以說明自己 ## 格式 縮排 對齊 ## 數據與結構 對象曝露行為,隱藏數據 數據暴露結構,沒有明顯的行為 ## 錯誤處理 將錯誤處理分開隔離,獨立於主邏輯之外,就可以單獨處理其錯誤 ## 邊界 API邊界 尚未存在的程式邊界 ## 單元測試 可讀性!! Build-Operate-Check 構造-操作-檢驗 測試宣告:BDD(Behavior Driven Development): Given-When-Then原則 * 不該有太多介面相關細節,否則UI一旦更改則全部可能都要更改 每個測試一個概念 * 像是如果測試某函數有兩種案例行為,則該把它分成兩個測試 >> 因為偷懶常常放在一起測試 F.I.R.S.T Fast:快速 頻繁且快速地進行測試 Independent:獨立 每個測試互相獨立且不受順序影響 Repeatable:可重複 無論在任何環境皆可以重複測試成功 Self-Validating:自我驗證 測試本身應該要有boolean輸出以自動檢驗測試,不該用log檔手動看 Timely:及時 要在實際運作的code之前編寫,否則code可能會被發現難以測試 ## class 類 應該短小,且更加短小 * 單一權責原則(Single Responsibility Principle): class / module應該只會有==一條修改其的理由==,一個系統應該由許多短小的class而不是少量龐大的class組成,每個小class只封裝一個權責,且只有一個修改他的理由,與其他class達成期望的系統行為 * 內聚性 class 中應該只有少數的變量,class內的方法應該要操作一或多個其中的變量,這表示class的方法與變數互相有關,形成一個邏輯整體 * OCP Open-Close-Principle 開放封閉原則 透過將class中各個方法拆解為class的派生(繼承)子類別後,可以利用擴充方式來擴充既有的子類別,而非動到原始的class去更改程式碼 e.g. 像是class apple 有許多方法(make flowers, get taller, add one leaf...等),透過將其各種方法拆解成class bloom extends apple, class grow extends apple,這樣我們就可以利用派生的子class去擴充子class的方法,而不會動到原本的class apple ## 系統 測試驅動系統架構,而非先做大設計(從整個系統框架都想好後再做),在之後的迭代和更新新增功能會更加彈性 整潔的系統有利於敏捷開發 使用==大概可以工作==的最簡單方案 ## 迭代 簡單設計的四條規則(重要性由上至下) 1. 跑過所有測試 可以測試即為可以驗證,才能部署 只要可測試,就會趨向短小單一的設計方案 2~4:重構 每次增加或修改代碼之後,回顧其修改後的代碼,讓設計更加整潔(提升內聚性 降低耦合 模塊化 縮小尺寸 更好的命名),並運行測試且通過它! 2. 不可重複 把可以共用的部分抽取出來,如此一來不僅減少測試,且也減少額外的風險(萬一被修改或刪除) 3. 表達程式設計者的意圖 如果花十分鐘寫程式(一般較具有系統的專案),讀9分寫1分,不管是新增還是之後的維護,都會花大量的時間去理解既有的程式碼,因此如果能把程式保持短小精簡,且命名各個功能/變數到位,那麼讀起來會更加輕鬆,你開心大家都開心 :) 4. 盡可能減少classes / methods的數量 不要有太多細小的classes / methods,但是此條規則為優先程度最低的,意即在達到以上三點之後,再來考慮這點吧! ## 開發編碼 開發上的防禦原則 1. SRP 單一權責原則 將多線程執行的代碼和其他代碼分離,否則如果商業邏輯與多線程互相耦合,則會難以修改與除錯 2. 限制數據作用域 當數據有共享時,要注意各個線程可能會互相干擾,導致無法預期的錯誤,因此要限制其數據的作用區域範圍! 3. 使用數據複製 呈上,避免共享數據是更好的方法,透過複製唯讀數據的方式來拿取數據的副本,在最後再將其數據做整合處理 4. 多線程之間盡可能獨立 各個線程就像是平行世界,從源頭複製所需的數據,再各自處理和暫存數據,並可以再透過資料庫的方式共享資源 ## 逐步改進 一開始就寫完美程式,是不可能的!擁抱醜陋,先從醜醜的草稿開始吧! 再來逐漸改進,越改越好,但不是說一開始能工作的醜醜程式就把她留著(很多人寫完能動的程式碼就認為完工了),一有機會就要將他重構或改寫的更好! 透過一次次的小小重構和抽離,並通過測試確保程式可行,將程式各個模組分開,降低依賴耦合程度! 如果在上午寫了混亂的程式碼,下午就趁熱把它解決,不讓混亂變得更亂! ## 重構 * 透過抽象化方法,將程式寫得更加簡潔且清楚,在每次寫程式後都比原來更整潔 * 將各個方法或函式分類整理到適合該class或檔案的地方,讓語意更加達意 * 修改變數名稱,讓變數名稱能更符合修改後的情境,且更好理解 ## 味道與啟發 * 註釋 冗餘或程式本身就可以解釋的註釋都是不用寫的! ==特別是被注釋掉的程式,現在可以透過git等工具去取得之前的版本,儘管刪掉沒關係!== * 函數 1. 過多的參數: 沒參數最好,一個次之,二個 三個再次之,三個以上最好包成物件或是拆分,盡量避免 2. 標示參數:像是透過true / false 來做兩件事的參數,就把它拆成兩個吧 3. 不要用的函數:一樣也是把它刪掉,反正git都會記得! * 一般問題 1. 多語言問題:盡量減少文件中的多種語言(以一種為主) 2. 沒實現明顯的行為:明顯的行為應該要被實現,像是如果有個函數(monthNumToMonthStr)是將月份(數字: 1, 2, 3...)轉換成月份(字串: Jan...),那麼就應該要有期望的輸出,否則就要看源代碼的細節了 3. 不正確的邊界行為:代碼應該要有辦法在大多數的邊界與情況下運作 4. 忽視安全:例如把某些編譯的警告關掉,或關掉失敗測試,這有可能導致之後的除錯與維護災難 5. 重複:維持DRY(Don't Repeat Yourself),將重複的代碼提高層次(抽象化),如此一來不僅可以減少測試,也可以減少錯誤發生的機會(例如複製貼上遺漏了...) 6. 錯誤的抽象層級:越通用與抽象的層級,通常放在基礎類別;而越具體及細節相關的層級,會放在派生的類別,例如Radius 這個屬性應該放在Circle之下,而非Shape 7. 訊息過多:一個class / module提供過多的接口,將會造成太深太廣的耦合,導致難以修改或是未知的問題,應該要限制其能使用的方法/ 接口/內部變數 8. 死碼:不會執行到的代碼,透過方法得知該方法不會被執行時,就果斷刪掉吧! 9. 選擇算子參數:透過輸入的參數來決定要做函數中的哪一件事,例如透過true/false來決定要對calc(isPluse)中做加或減,這樣就必須記住該函數的詳細規則,且每次調用都要去查看! 10. 晦澀的意圖:代碼本身就要具有表達力,任何無法一眼就大概知道意圖的(特別是魔術數),像是time = prevTime + 1000,此時的1000就不知道為何,要改成 time = prevTime + thounsandMillSec相對比較好 11. 權責位置錯誤:代碼應該在讀者覺得他應該在的地方,像是getSalary就應該在Employee之類的類別底下 12. 用命名常數來替代魔術數:像是38, 可以用Keycode.ARROW_UP之類的來替代,來更表達其數字含義 13. 函數應該要只做一件事:函數一次應該只做一件事,像是判斷if/else, for, ... 14. 隱藏了時序耦合:如果代碼的執行順序有其順序性,那麼可以透過return值的方式,傳遞處理好的結果給下一個函數使用,如此一來就清楚個函數的順序了 15. 封裝邊界條件:例如lastIndex = array.length - 1 之類的,應該要抽離作為變數集中在一起,會更加清楚! 16. 拆分不同抽象層級:像是 code.16 17. 將可以被配置的值放在較高的抽象層級,不要埋到底下的派生或較低層級,不然就要一個個去找 18. 避免傳遞瀏覽:避免像是 A.getB().getC().getD()之類的語法,盡量將方法直接曝露出來,像是 A.getDeepD() 之類的d code.16 ```typescript= //此範例就是混合了兩個抽象層級, 一個是針對字串的直接操作,一個是調用函數 function renderTypedString(str: string) { return '_' + str + getSeparatedStr(str) } //可以改成像是以下,不僅比較好測試,且更清楚! function getUnderlinedStr(str: string)) { return '_' + str } function renderTypedString(str: string) { return getUnderlinedStr(str) + getSeparatedStr(str) } ``` * 名稱 1. 採用描述性的名稱:除非像是==for中的i==這種常用名稱,否則都應該要取名得一眼就能看出他是什麼用途的名稱,像是 ```newScore = score + averageScore``` 而非 ```k = j + 10``` 2. 使用具體且無其他意思的名稱:像是有個方法會在取得資料後,關閉資料庫的連接 ```function doGetDataFromDB() { ... }``` 然而以上方法只有提到取得資料而沒有描述該關閉連接的功能,就應該改為 ```function doGetDataFromDBThenDiconnectDB() { ... } ```會比較好(儘管名稱真的很長) * 測試 1. 測試不足:測試應該要測試所有可能失敗的狀況或是例外規則,只要有條件沒有被測到,就還不夠! 2. 使用覆蓋率工具:前端React為例,你可以使用jest coverage來幫助你測試代碼的覆蓋率(但是不要完全相信,因為此覆蓋率僅為代碼有跑過那個部分,並不一定是有根據邏輯去測試過!) 3. 別略過小測試:小測試不僅容易編寫,因為其表達與相對大測試,能更為快速驗證,因此更有價值去寫測試 4. 測試邊界條件:注意測試邊界的條件,有時候會有邊界判斷錯誤的問題 5. 測試失敗或許能夠沒發現過的問題:當在重構或是在測試完整用例時的測試錯誤,或許可以發現bug的解決方法! 6. 測試應該快速:當時間不夠時(通常都是這樣),不會去運行比較慢的測試,因此盡量將測試精簡快速吧! constants 用config等等的從外部引入