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