---
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等等的從外部引入