初探 Clean Code
2023-01-25
筆記關於 Clean Code 的設計實務之道。
說明
Clean Code 強調的是程式碼的可讀性 (Readability),因為任何人都可以寫出電腦看得懂 (Compiler & Interpreter 可以通過) 的程式碼,但卻很難寫出讓未來的自己或其他人看得懂的程式碼。
筆記源自 Cory House 在 Pluralsight 的 Clean Coding Principles in C#,
進階參考資源包含 Steve McConnell 的 Code Complete、Robert C.Martin 的 Clean Code 以及 Andrew Hunt 與 David Thomas 的 The Pragmatic Programmer。
根據 Cory House 指出,Clean Code 有三個原則,分別是用正確的工具處理工作、讓程式碼呈現資訊而非噪音 (signal to noise ratio) 以及讓程式碼本身就是文件 (Self-Documenting)。
用正確的工具處理工作
注意程式碼的邊界,不要互相干涉以及混雜再一起,例如 MVC 專案當中 C#、HTML、CSS 以及 JS 各司其職,不要越俎代庖 (避免用一種程式語言去組合另一種程式語言),儘量用 Convention over Configuration 以及大家有共識的方式去處理問題。
太過複雜的 Regex 不要用,相較於硬體資源上的效率,可讀性仍是處在第一位重要。
讓程式碼呈現資訊而非噪音
訊號 (signal) 的特質包含 TED (Terse, Expressive, Does one thing);相對地噪音 (noise) 則是具有高 cyclomatic complexity、浮誇的縮排、不好的命名、重複的程式碼、冗長的單一函式以及巨大的類別等。
讓程式碼本身就是文件
透過程式碼排版的技巧來增進可讀性,讓程式碼的縮排 (Indent)與層次 (Layer)等結構本身向閱讀者傳達訊息,而非透過註解來傳達。
Why Clean Code
在程式碼的開發過程中,反覆閱讀程式碼的時間勝過撰寫程式碼,因此程式碼的可讀性特別重要,而好閱讀的程式碼能夠為需求改變或功能擴充帶來更快的實踐。
原則與技巧
Naming
類別的命名,使用明確的名詞,命名要能夠反映出單一責任與高內聚的特性,例如 User, Account, QueryBuilder, ProductRepository。
避免命名出現 And, If, Or,如果有這樣的命名表示類別的責任太多了,應該再切割為更小的類別
命名必須要與信任感,不要有命名預期外的狀況 (例如讀取的功能卻會造成資料寫入)
布林值的命名技巧:
isOpem
,isActive
,loggedIn
Conditoinals
適當的簡化條件的布林值,可以提升程式碼可讀性:
❌
if (loggedIn == ture)
✔️
if (loggedIn)
❌
bool doSomeThing;
if (weather == Weather.Sunny)
doSomeThing = true;
else
doSomeThing = false;
✔️
bool doSomeThing = weather == Weather.Sunny;
在條件撰寫上避免使用雙重否定。
適當使用三元運算子 (Ternary) 增進可讀性。
使用強型別 (Enum) 進行比較,減少使用字串比較。
避免使用 Magic Number (使用有意義命名的變數或者 Enum 替代)。
如果條件的判斷式有多項,另外抽成有意義命名的變數替代,增進可讀性。
❌
if (employee.Age > 65 && employee.IsRetired)
✔️
bool eligibleForPension = employee.Age > 65 && employee.IsRetired;
if (eligibleForPension)
如果複雜到變數 hold 不住,就抽出封裝成 Method。
使用多形 (Polymorphism) 取代 Switch 分歧。
使用陳述性的方式處理邏輯 (Linq 的 Predicate)。
提高程式碼的可維護性,使用 Table Driven 將資料儲存在資料庫當中,而非直接在程式碼的邏輯中處理。
Clean Method
考慮將程式碼抽為 Method 或 Function 的時機,包含發現程式碼出現相似的規律 (Pattern) 以及重複的程式碼時;出現箭頭形狀的縮排 (Arrow Code) 造成程式碼可讀性下降時;程式碼的用途不明確時。
此外在處理 Arrow Code 時,除了抽為 Method,也可以利用 Fail Fast 的技巧,打破巢狀的 If 讓會報錯直接被處理。
❌
public void RegisterUser(string username, string password)
{
if (!string.isNullOrWhitespace(username))
{
if (!string.isNullOrWhitespace(password))
{
...
}
else
{
throw new ArgumentException();
}
throw new ArgumentException();
}
}
✔️
public void RegisterUser(string username, string password)
{
if (string.isNullOrWhitespace(username))
{
throw new ArgumentException();
}
if (string.isNullOrWhitespace(password))
{
throw new ArgumentException();
}
}
而當程式碼的用途不明確時,也應該抽為 Method,例如:
❌
if ((fileExtension == ".jpg" || fileExtension == ".png" ) && isFileExists)
✔️
private bool ValidaFileExists(string fileExtension, bool isFileExists)
{
var validFileExtensions = new List<string>() {".jpg", ".png"};
return validFileExtensions.Contains(fileExtension) && isFileExists;
}
💡 在 Method 內部的變數使用上,不要集中宣告在開頭,而是要緊鄰使用此內部變數的程式碼段落,讓閱讀上可以更容易識別變數的用途。
💡 如果 Method 的參數過多,可以將參數另外設計為類別 (Class) 提升可讀性。
💡 避免在 Method 加入 Flag 參數,這樣會讓單一 Method 負擔額外的工作,分開成不同的 Method。
Clean Class
類別的設計上,首重高內聚 (Cohesion),設計上要避免讓類別淪為龐雜的程式碼聚合,以提升類別程式碼的可讀性以及被重複使用的可能。(相對於類別過於龐雜的問題,很少發生類別被抱怨設計的太"迷你")。
在物件導向設計上,要避免 Primitive Obsession (偏好使用 int, double, string 等基本型別) 而忽略使用類別 (Class) 藉由封裝所帶來的可讀性提升。
Clean Comment
正:寫註解是為了提升程式碼的可讀性
反:不寫註解,讓程式碼表現意義則是提升程式碼的可讀性
合:原則上不寫註解,正確的使用註解以提升可讀性
💡 避免廢話類型的註解 (這個變數宣告為 int 並賦值)
💡 避免用註解表現程式碼用途,使變數、方法、類別的命名賦予用途
💡 移除註解掉的程式碼,讓版本控制工具來管理被移除的程式碼
💡 移除分隔程式碼段落的註解以及標註程式碼起始的註解
💡 移除作者資訊以及程式碼用途摘要
使用 Visual Studio 所支援的 //TODO
以及 //HACK
,對於類別或者方法,使用 Summary
實作
檢驗指標
Maintainability Index
Cyclomatic
Code Smells
Arrow Code
- Nested Ifs
- Guard Clause
- Indents
Comments
- Repeat
- Version Control
Mayfly Variables
- Check dependency
Failing Fast
Table Driven
Tests before Refactoring
Avoid Abbreviation
- Long descriptive name is accepted
TED Principles
- Terse
- Expressive
- Doe one thing
Readability
- Naming
- Short & Simple
- Linq Predicate
Extension Method over Loop Methods
Least Surprise Principle