Clean Code 無瑕的程式碼 第17章 程式碼的氣味和啟發
第十七章作者用歸納法整理出一連串程式碼注意事項。
註解 (Comments)
C1:不適當的資訊 (Inappropriate Information)
註解應該寫程式碼相關的資訊。
不應該記錄要存放在別地方的資訊,例如歷史修改、修改日期。
C2:廢棄的註解 (Obsolete Comment)
程式會一直修改,會產生過時的廢棄註解。發現過時的廢棄註解要儘快移除,避免受到廢棄註解誤導。
C3:多餘的註解 (Redundant Comment)
直接看程式碼能夠表達的意圖,就可以不用再寫多餘的註解。
C4:寫得不好的註解 (Poorly Written Comment)
好的註解要正確簡潔有力。
C5:被註解掉的程式碼 (Commented-Out Code)
變成註解的程式碼可以直接刪除,因為有程式碼控管軟體。
開發環境 (Environment)
E1:需要多個步驟以建立專案或系統 (Build Requires More Than One Step)
一個指令就可以建立系統。
E2:需要多個步驟以進行測試 (Tests Require More Than One Step)
一個指令可完成一個測試。
函式 (Functions)
F1:過多的參數 (Too Many Arguments)
函式的參數越少越好,儘量不要讓參數超過三個以上。
F2:輸出型參數 (Output Arguments)
讀者預期參數是輸入,輸出型參數會影響可讀性,物件狀態可取代輸出型參數。
F3:旗標參數 (Flag Arguments)
出現Boolean參數表示函式做了超過一件的任務。Boolean參數應該被移除。
F4:被遺棄的函式 (Dead Function)
不再被呼叫的函式應該被移除。
一般狀況 (General)
G1:同份原始檔存在多種語言 (Multiple Languages in One Source File)
儘量做到一個檔案只存在一種程式語言。
G2:明顯該有的行為未被實現 (Obvious Behavior Is Unimplemented)
程式碼能夠達成原有的意圖,讀者不用查看底層的程式碼。
G3:在邊界上的不正確行為 (Incorrect Behavior at the Boundaries)
撰寫測試程式測試所有邊界條件,而不是依靠直覺。
G4:無視安全規範 (Overridden Safeties)
車諾比核災事件是人禍,電廠管理員無視安全規範。
有出現測試錯誤就要解決,不能假裝沒有出現。
G5:重複的程式碼 (Duplication)
出現重複的程式碼表示表示發生未進行抽象的情形。
由「為什麼 “abstraction”不應該譯為“抽象化”」談正名
這篇文章寫得好,abstraction不應該翻譯成為抽象化,萃取、提煉與歸納比較適合。
switch/case 與 if/else 在不同的模組一直出現,應該用多型的方式來代替重複狀況。
設計模式模版方法(TEMPLATE METHOD)與策略模式(STRATEGY)可以取代重複的程式碼。
G6:在錯誤抽象層次上的程式碼 (Code at Wrong Level of Abstraction)
「高層次一般概念」與「低層次細節概念」是重要的工作。所有的低層次概念都放在衍生類別,所有的高層次概念都放在基底類別。
常數、變數或工具函式與細節有關,應該只出現在衍生類別。
範例程式碼:
public interface Stack{
Object pop() throws EmptyyException;
void push(Object o) throws FullException;
double percentFull();
………………….
}
percentFull()處於不對的抽象層次,算是細節, percentFull()最好放在衍生介面BoundedStack。
G7:基底類別相依於其衍生類別 (Base Classes Depending on Their Derivatives)
高層次基底類別,可以獨立於低層次衍生類別概念之外,基底類別不應該知道衍生類別的資訊。
有限狀態機的實作會出現例外,基底類別與衍生類別兩者強烈耦合,一起佈署至相同的jar檔案。
通常將基底類別與衍生類別佈署在不同的jar檔案,一次變動不用演變成為整體的變動。
G8:過多的資訊 (Too Much Information)
降低耦合度,模組介面越少越好,類別擁有的方法越少越好,一個函數知道的變數越少越好。
G9:被遺棄的程式碼 (Dead Code)
永遠不會被執行的程式碼要立刻刪除。
G10:垂直分隔 (Vertical Separation)
變數和函式應該定義在靠近被使用的地方。
區域變數應該在第一次被使用的位置上方進行宣告。
私有函式應該在第一次被使用的位置下方進行定義。
G11:不一致性 (Inconsistency)
變數名稱、物件名稱與函數名稱要有一致性,讓程式碼容易閱讀修改。
G12:雜亂的程式 (Clutter)
沒有用的建構函數、不會被使用的變數、不會被呼叫的函數、沒有意義的註解都應該被移除。
G13:人為的耦合 (Artifucial Coupling)
人為的耦合沒有直接目的的兩個模組之間加上了耦合。
不相依的程式不應該被人為耦合,例如enums不應該包含在特定類別,會強迫整個程式知道這些特定類別。
無特殊用途的static函式宣告在特定的類別,有可能會產生人為的耦合。
G14:特色留戀 (Feature Envy)
類別的方法應該只對同一個類別裡的變數和函數感興趣。
public class HourlyPayCalculator{
public Money calculateWeeklyPay(HourlyEmployee e){
int tenthRate = e.getTenthRate().getPennies();
……………..
}
}
calculateWeeklyPay 方法操作的資料,從物件HourlyEmployee 取出,代表calculateWeeklyPay 方法希望自己就在HourlyEmployee 類別裡面。特色留戀會將類別內部的資料,暴露給另一個類別。
有時候特色留戀是必要之惡,作者舉一個例子。
reportHour方法留戀 HourlyEmployee 類別,但我們也不想讓HourlyEmployee 類別了解時間報告格式。將時間格式移入HourlyEmployee 類別,會出現耦合的情況。
G15:選擇型參數 (Selector Arguments)
函數要避免使用boolean 型態、列舉、整數輸入參數,可用來選擇函式行為的參數型態。
G16:模糊的意圖 (Obscured Intent)
跨行的表達式、匈牙利命名法、魔術數字都會模糊作者的意圖。
G17:錯置的職責 (Misplaced Responsibility)
程式碼應該放在讀者自然認為應該存在的地方。例如PI 常數應該放在三角函數被宣告的地方。
G18:不適當的靜態宣告 (Inappropriate Static)
Math.max(double a, double b) 適合靜態方法,因為不會改變。
HourlyPayCalculator.calculatePay(employee, overtimeRate) 不適合宣告為靜態方法,因為可能會用到多型。
G19:使用具解釋性的變數 (Use Explanatory Variables)
命名有意義的暫存性變數,可以增加程式的可讀性。
例如
string key = match.group(1);
string value = match.group(2);
key 與 value 就容易讓讀者了解在做查詢的工作
G20:函數名稱要說到做到 (Function Names Should Say What They Do)
從函數名稱就要能夠看出函數要做什麼。
G21:瞭解演算法 (Understand the Algorithm)
瞭解演算法才能寫出正確的程式。
G22:讓邏輯相依變成實體相依 (Make Logical Dependencies Physical)
只看字面上說明一定不懂,可看17-1 作者的範例說明。
private final int PGAE_SIZE = 55 這行 PGAE_SIZE 產生邏輯相依。
HourlyReporter 類別不需要知道頁面大小。可在 HourlyReporterFormatter 類別裡建立 getMaxPageSize這個新方法,將邏輯相依轉為實體相依。
G23:用多型取代If/Else 或 Switch/Case (Prefer Polymorphism to If/Else or Switch/Case )
這段內容要對照第六章看。
大部分人使用Switch/Case 式因為Switch/Case 比較簡單,不一定Switch/Case 真的適合這種情況,在使用Switch/Case 之前要先考慮多型的解法。
函式變化比型態變化更強烈的情況相對而言少很多。
選擇的型態不應該有超過一個以上的switch。 唯一的switch可建立多型物件來取代Switch敘述。
G24:遵循標準的慣例 (Follow Standard Conventions)
每個團隊理應遵循同一個程式碼開發標準規範。
G25:用有名稱的常數取代魔術數字 (Replace Magic Numbers with Named Constants)
用有名稱的常數增加程式的可讀性,魔術數字無法表達常數的意圖。
G26:要精確 (Be Precise)
作者講述程式注重的細節,例如如果有傳null ,就要有檢查null的機制。如果有同步更新,就要有某種鎖定機制。
G27:結構勝於常規 (Structure over Convention)
「具有強制決策設計特性的結構」勝過「慣例」。例如有良好列舉命名的switch/case,不如有抽象方法的基底類別。
G28:封裝條件判斷 (Encapsulate Conditionals)
看範例可知函數提取可以增加程式的可讀性。
if(timer.hasExpired()&&!timer.isRecurrent()) 寫法較差
if(shouldBeDeleted(timer)) 寫法較佳
G29:避免否定的條件判斷 (Avoid Negative Conditionals)
盡可能使用肯定的條件判斷。
G30:函式應該只做一件事 (Functions Should Do One Thing)
函式應該只做一件事。
G31:隱藏時序耦合 (Hidden Temporal Couplings)
看範例可懂
第一個範例三個函數要依序執行才能有正確的結果。
第二個範例讓三個函數傳參數,來達成正確的時間順序。
G32:不要隨意 (Don’t Be Arbitrary)
這邊談到類別的視野範圍,類別要放對位置。
G33:封裝邊界條件 (Encapsulate Boundary Conditions)
看範例就懂,第一個範例原本邊界條件 level+1 出現了兩次,第二個範例改成改成 int nextLevel = level +1
G34:函式內容應該下降抽象層次一層 ( Functions Should Descend Only One Level of Abstraction)
這段內容要與第三章對照看,每個函數都有階層,實作的部分要放在最底層。
G35:可調整的資料應放置於高階層次 (Keep Configurable Date at High Levels)
預設值或設定值要放在高階函數,當做參數傳下去。
G36:避免傳遞性導覽 (Avoid Transitive Navigation)
內容就是迪米特法則,只與朋友說話,可參考第六章的內容。
Java
J1:利用萬用字元來避免冗長的引入列表 (Avoid Long Import Lists by Using Wildcards)
J2:不要繼承常數 (Don’t Inherit Constants)
不要在介面中寫常數讓別的類別繼承
public interface PayrollConstants{
public static final int TENTHS_PER_WEEK = 400;
………..
}
應該使用靜態的引入敘述來取代。
import static PayrollConstants.*
J3:常數和列舉 (Constants versus Enums)
用列舉的方式取代常數
命名 (Names)
N1:選擇具描述性質的名稱 (Choose Descriptive Names)
變數與函數要取有意義的名稱,避免出現魔術數字。
N2:在適當的抽象層次選擇適當的命名 (Choose Names at the Appropriate Level of Abstraction)
比較兩個範例,隨著時代進步。dial 改為 connect
N3:盡可能使用標準命名法 (Use Standard Nomenclature Where Possible)
使用已有的慣例或用法來進行命名,例如有使用裝飾者模式,就應該在裝飾者類別名稱使用Decorator。
團隊通常會發明自己的命名系統。
N4:非模稜兩可的名稱 (Unambiguous Names)
函數名稱要能夠正確表達做什麼事
N5:較大範圍的視野使用較長的名稱 (Use Long Names for Long Scopes)
區域變數可取短的名稱,視野範圍廣的變數如全域變數可以取較長的名稱。
N6:避免編碼 (Avoid Encodings)
名稱不應該出現型態或事也編碼,例如 m_或 f。作者反對使用匈牙利命名法。
N7:命名應該描述可能的程式副作用 (Names Should Describe Side_Effects)
函數做多少事要反應在函數名稱。
測試(Tests)
T1:不足夠的測試 (Insufficient Tests)
測試範圍要足夠夠廣
T2:使用涵蓋率工具 (Use a Coverage Tool!)
使用涵蓋率工具發現哪些程式未測試。
T3:不要跳過簡單的測試 (Don’t Skip Trivial Tests)
簡單測試容易寫,不可以跳過。
T4:被忽略的測試是對模稜兩可的疑問 (An Ignored Test Is a Question about an Ambiguity)
????????
T5:測試邊界條件 (Test Boundary Conditions)
演算法有可能一般狀況運行順利,在邊界條件會出現錯誤。
T6:在程式錯誤附近進行詳盡的測試 (Exhaustively test Near Bugs)
錯誤往往聚集,在錯誤附近的地區要進行詳盡測試。
T7:失敗的模式是某種啟示 (Patterns of Failure Are Revealing)
用測試失敗的現象找出程式出問題的地方。
T8:測試涵蓋率模式可以是一種啟示 (Test Coverage Patterns Can Be Revealing)
從測試涵蓋率哪些程式執行過,哪些沒執行過,找出測試失敗的原因。
T9:測試要夠快速 (Tests Should Be Fast)
測試程式要夠快速。
總結
提出一個Clean Code 價值體系。
留言
張貼留言