欢迎光临散文网 会员登陆 & 注册

Clean Code 無瑕的程式碼 第17章 程式碼的氣味和啟發

2021-06-25 23:16 作者:tkchenhaha  | 我要投稿

    第十七章作者用歸納法整理出一連串程式碼注意事項。

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



Clean Code 無瑕的程式碼 第17章 程式碼的氣味和啟發的评论 (共 条)

分享到微博请遵守国家法律