--- 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等等的從外部引入
×
Sign in
Email
Password
Forgot password
or
Sign in via Google
Sign in via Facebook
Sign in via X(Twitter)
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
Continue with a different method
New to HackMD?
Sign up
By signing in, you agree to our
terms of service
.