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 價值體系。


 


 


留言

這個網誌中的熱門文章

異世界NTR web版第三章 觀後感 喧賓奪主 ,反派實力過強

持有縮小技能的D級冒險者,與聖女結婚並加入勇者團隊 漫畫 01-04 觀後感 大我與小我

泛而不精的我被逐出了勇者隊伍 web第三章 觀後感 菲莉真能打; 露娜超爽der