Robert Martin:clean Code——a Handbook Of Agile Software Craftsmanship@2008
好的,這是一個針對您提供的資料的主要論點及其繁體中文解釋的提取:
《無瑕的程式碼》(Clean Code)核心論點解釋
本節旨在提取並詳盡解釋 Robert C. Martin 的著作《無瑕的程式碼》中提出的主要論點。本書強調了編寫乾淨、易於理解和維護的程式碼對於軟體開發專業人士的重要性,並提供了一系列關於命名、函式、註解、格式、物件與資料結構、錯誤處理、邊界、單元測試、類別、系統、併發以及如何透過持續重構來達成無瑕設計的原則、模式、實踐和啟發。
以下是從提供的資料中提取並詳細解釋的核心論點:
1. 無瑕程式碼的價值與重要性
- 程式碼是需求的最終表達:無論抽象層次如何提升,程式碼始終是精確表達需求細節的語言。程式設計師的職責在於提供嚴謹、準確、形式化的程式碼,而無瑕的程式碼更能清晰地溝通這些需求。
- 糟糕程式碼的代價高昂:混亂的程式碼會導致開發速度急劇下降,因為每一次修改或新增功能都需要深入理解糾纏不清的邏輯。這不僅降低生產力,增加維護成本,還可能導致專案停滯甚至失敗。LeBlanc 定律指出:「稍後等於永不」(Later equals never),意味著混亂一旦產生,如果不立即清理,將很難有機會回頭處理。
- 維護成本遠超開發成本:軟體生命週期中,絕大部分工作是維護(包括修復錯誤和新增功能)。可讀性高的無瑕程式碼極大降低了理解和修改的難度,從而顯著減少維護成本。反之,即使是看似很小的程式碼修改,在混亂的程式碼庫中也可能變得異常困難和耗時。
- 專業人士的責任:編寫無瑕的程式碼是軟體開發者的基本專業要求,就像醫生在手術前必須洗手一樣。專業人士應堅守程式碼品質,不應僅為了迎合不理解技術風險的管理者或客戶而妥協。他們應該勇於為程式碼的品質辯護。
- 無瑕程式碼提升開發速度:表面上,為了趕進度而編寫混亂的程式碼似乎更快,但實際上,混亂會立即拖慢開發速度,導致錯過截止日期。保持程式碼的整潔是維持高速開發的唯一途徑。
- 無瑕程式碼是一門手藝:編寫無瑕的程式碼不僅需要原則和模式的知識,更需要透過大量實踐才能培養出對「整潔」的直覺判斷(code-sense)。這是一門需要不斷磨練的手藝。
2. 關於程式碼結構與元素的指導原則
-
有意義的命名 (Meaningful Names):
- 意圖明確 (Intention-Revealing):變數、函式、類別等名稱應清楚表達其存在原因、用途及使用方式,無需額外註解。
-
避免誤導 (Avoid Disinformation):避免使用與既有概念相衝突的名稱 (如將帳戶列表命名為
accountList但實際不是 List),避免微小的名稱差異 (XYZControllerForEfficientHandlingOfStringsvsXYZControllerForEfficientStorageOfStrings),尤其避免使用難以區分的字元 (lvs1,Ovs0)。 -
製造有意義的區別 (Make Meaningful Distinctions):名稱差異應反映意義差異。避免使用數字序列 (
a1,a2) 或無意義的噪音詞 (Info,Data,Manager,Processor) 來區分名稱。 - 可發音 (Pronounceable):名稱應易於說出,因為程式設計是一項社會活動,開發者需要討論程式碼。
- 可搜尋 (Searchable):避免單字母名稱 (除了短函式中的迴圈計數器) 或數字常數,應使用易於在文字中搜尋的長名稱。
-
避免編碼 (Avoid Encodings):不要在名稱中編碼類型或範圍資訊 (如匈牙利命名法、成員變數前綴
m_)。現代 IDE 已提供這些資訊。 -
類別名應為名詞 (Class Names):類別和物件應使用名詞或名詞片語 (如
Customer,WikiPage)。 -
方法名應為動詞 (Method Names):方法應使用動詞或動詞片語 (如
postPayment,deletePage)。取值、設定值和布林判斷方法應遵循慣例 (get,set,is)。 -
每個概念一個詞 (Pick One Word per Concept):為同一個抽象概念選擇一個詞並堅持使用,避免混用同義詞 (如
fetch,retrieve,get)。 - 使用解法領域名稱 (Use Solution Domain Names):程式碼的讀者是程式設計師,可以使用電腦科學術語、演算法名稱、設計模式名稱等技術術語。
- 使用問題領域名稱 (Use Problem Domain Names):當沒有合適的技術術語時,使用問題領域的名稱,以便開發者可以向領域專家尋求解釋。
- 增加有意義的上下文 (Add Meaningful Context):名稱需要在上下文中才有意義。將相關變數組織到類別中可以提供上下文。
- 不要添加冗餘的上下文 (Don’t Add Gratuitous Context):不要在名稱中添加不必要的上下文,例如為每個類別添加專案縮寫前綴。
-
函式 (Functions):
- 短小 (Small!):函式應盡可能短小,最好不超過二十行,甚至更短。短函式易於理解和命名。
- 只做一件事 (Do One Thing):函式應只做一件事情,並且把它做好。判斷函式是否只做一件事的標準是,函式中的所有步驟都應該處於同一抽象層次,並且這些步驟是在描述函式名稱所代表的操作。
- 單一抽象層次原則 (One Level of Abstraction per Function):函式內的語句應處於同一抽象層次,避免高層概念與低層細節混雜。
- 向下規則 (The Stepdown Rule):程式碼應像一篇從上到下閱讀的敘述,每個函式後面緊接著是更低一層抽象層次的函式,方便讀者逐步深入細節。
- 避免多參數 (Function Arguments):理想的參數數量是零 (niladic),其次是一 (monadic),然後是二 (dyadic)。應盡量避免三 (triadic),超過三個參數需要極特殊理由且應避免。參數越多,理解和測試越困難。
- 避免旗標參數 (Flag Arguments):布林參數表明函式做了不止一件事,應將函式拆分為執行不同行為的多個函式。
- 無副作用 (Have No Side Effects):函式應只做名稱所暗示的事情,不應在不知不覺中修改自身類別的變數、傳入的參數或系統全域變數。副作用會導致時間性耦合和順序依賴。
- 命令與查詢分離 (Command Query Separation):函式應要不是做某事 (修改狀態),要不就是回答某事 (返回資訊),但不同時做兩者。
- 偏好使用例外而非返回錯誤碼 (Prefer Exceptions to Returning Error Codes):返回錯誤碼會使調用者必須立即處理錯誤,導致程式碼結構深層嵌套。使用例外可以將錯誤處理邏輯與正常邏輯分開,使程式碼更清晰。
- 提取 Try/Catch 區塊 (Extract Try/Catch Blocks):將 Try/Catch 區塊的主體提取到單獨的函式中,以分離錯誤處理與正常流程。
- 錯誤處理是一件事 (Error Handling Is One Thing):處理錯誤的函式不應再做其他事情。
- 不要重複自己 (Don’t Repeat Yourself – DRY):重複是軟體中的萬惡之源。避免程式碼、邏輯或功能的重複。將重複的邏輯提取到單獨的函式或類別中。
-
註解 (Comments):
- 註解並不能彌補寫得不好的程式碼 (Comments Do Not Make Up for Bad Code):清晰表達的程式碼比混亂的程式碼加上大量註解要優越得多。應花時間清理程式碼而非為其寫註解。
- 用程式碼解釋自己 (Explain Yourself in Code):盡可能用程式碼本身來表達意圖,而非依賴註解。創建名稱清晰的函式可以替代許多註解。
- 註解的必要性與缺點:註解充其量是必要的邪惡。它們容易過時、誤導或謊言,因為程式碼會變化而註解不易維護。只有程式碼本身能準確說明它做什麼。
- 好的註解類型:法律註解、提供資訊的註解 (有時可用名稱代替)、解釋意圖、釐清、警告後果、TODO 註解、放大重要性、公用 API 中的 Javadoc。
- 壞的註解類型:喃喃自語、冗餘 (重複程式碼意思)、誤導性、強制要求 (如每個函式都要有 Javadoc)、日誌式 (Log)、噪音、註釋掉的程式碼、HTML 註解、非本地資訊、過多資訊、不明顯的連接、函式頭部註解 (短函式不需要)、非公用程式碼中的 Javadoc。
- 註釋掉的程式碼是罪惡:永遠不要將程式碼註釋掉。版本控制系統會記住它。
-
格式 (Formatting):
- 格式為了溝通 (The Purpose of Formatting):良好的格式化是專業開發者的責任,它提高了程式碼的可讀性,影響長期的可維護性。
- 垂直格式 (Vertical Formatting):檔案應相對短小 (新聞報導比喻)。概念間用空行分隔,緊密相關的程式碼應垂直密集。變數應靠近使用處聲明。呼叫者應盡量放在被呼叫者上方 (向下規則)。概念相關性高的程式碼應放在一起。
- 水平格式 (Horizontal Formatting):限制行寬 (建議不超過 120 字元)。使用水平空格來區分相關性弱的元素 (如賦值操作符周圍),緊密相關的元素則不加空格 (如函式名與括號)。使用空格來強調運算符的優先級。
- 縮排 (Indentation):使用縮排來顯示程式碼的範圍層次。讀者依賴縮排來快速理解程式碼結構。
- 團隊規則 (Team Rules):團隊應遵循統一的格式化規則,並使用自動化工具執行。程式碼應具有一致的風格,不應展現個人風格差異。
-
物件與資料結構 (Objects and Data Structures):
- 資料抽象 (Data Abstraction):類別不應透過 getter/setter 直接暴露內部變數,而應暴露抽象介面,允許使用者操作資料本質,隱藏實現細節。
-
資料/物件 反對稱性 (Data/Object Anti-Symmetry):
- 物件:隱藏資料,暴露操作。新增類別容易,新增函式困難 (所有類別需修改)。
- 資料結構:暴露資料,無有意義操作。新增函式容易,新增資料結構困難 (所有函式需修改)。
- 應根據系統中新增型別或新增行為的可能性來選擇使用物件或資料結構。
-
Demeter 定律 (The Law of Demeter):一個模組不應了解其操作物件的內部細節。方法 f 不應呼叫從其參數或成員變數所獲得的物件的 方法 所返回的物件的方法 (即:只與朋友交談,不與陌生人交談)。避免「火車失事」式的連串呼叫 (
a.getB().getC().doSomething())。 - 混合結構 (Hybrids):避免創建既有行為又有公共變數/訪問器的混合結構,它們同時難以新增函式和資料結構。
- 資料傳輸物件 (Data Transfer Objects – DTOs):具有公共變數而無函式的類別,常用於與資料庫或外部服務通信。
-
Active Record:具有公共變數以及
save,find等導航方法的資料結構。應將業務規則放在獨立的物件中,而非 Active Record 本身。
-
錯誤處理 (Error Handling):
- 使用例外而非返回碼 (Use Exceptions Rather Than Return Codes):例外將錯誤處理與主要邏輯分離,使程式碼更清晰。
- 先寫 Try-Catch-Finally 語句 (Write Your Try-Catch-Finally Statement First):這有助於定義異常處理的範圍,並確保程式在遇到錯誤時處於一致狀態。
- 使用未檢查例外 (Use Unchecked Exceptions):除了少數關鍵函式庫,應用程式開發中應偏好未檢查例外。檢查例外導致底層變動向上層傳播,破壞封裝性。
- 例外應提供上下文 (Provide Context with Exceptions):例外應包含足夠的資訊 (操作、失敗型別、錯誤訊息) 以確定錯誤的來源和位置。
- 依呼叫者需求定義例外類別 (Define Exception Classes in Terms of a Caller’s Needs):依照程式碼的處理方式來分類例外,而非依照來源或型別。將第三方 API 的例外包裝成自己的例外類別。
- 定義正常流程 (Define the Normal Flow):盡可能使程式碼的主體流程看起來像沒有錯誤發生。使用特殊案例模式 (Special Case Pattern) 來處理異常情況,而非使用例外。
- 不要返回 Null (Don’t Return Null):返回 Null 會迫使調用者進行 Null 檢查,增加程式碼的複雜性並引入 NullPointerException 的風險。應返回空列表或特殊案例物件。
- 不要傳遞 Null (Don’t Pass Null):除非 API 有明確要求,不應傳遞 Null 作為參數。 Null 參數通常表示錯誤,且難以優雅地處理。
-
邊界 (Boundaries):
-
使用第三方程式碼 (Using Third-Party Code):第三方套件通常提供寬泛的介面,但我們只需要其中一部分功能。直接傳遞第三方介面物件 (
java.util.Map) 會在介面變動時影響系統。 - 封裝邊界 (Encapsulating Boundaries):應在內部封裝第三方介面,提供符合應用程式需求的受限介面。這降低了對第三方程式碼的依賴,並使其更容易替換。
- 學習測試 (Learning Tests):在整合第三方程式碼時,先編寫測試來探索和學習其 API 行為。這些測試是受控的實驗,驗證我們對 API 的理解,並在第三方套件更新時提供安全網。
- 使用尚未存在的程式碼 (Using Code That Does Not Yet Exist):當需要與尚未定義介面的子系統交互時,可以定義自己期望的介面,並使用 Adapter 模式將其與最終定義的實際介面隔離開來。這使得我們可以繼續開發而不被阻塞。
-
使用第三方程式碼 (Using Third-Party Code):第三方套件通常提供寬泛的介面,但我們只需要其中一部分功能。直接傳遞第三方介面物件 (
-
單元測試 (Unit Tests):
- 測試程式碼與產品程式碼同等重要:測試程式碼必須像產品程式碼一樣保持乾淨、可讀、可維護。髒亂的測試程式碼難以修改,會阻礙產品程式碼的演進,最終可能導致測試被放棄。
- 測試使得程式碼保有「能力」(-ilities):單元測試(特別是全面的自動化測試)降低了修改程式碼的恐懼,從而使得程式碼更容易被重構、重用和維護。測試是維持設計和架構整潔的關鍵。
- 無瑕測試的特徵:可讀性、可讀性,還是可讀性。測試程式碼需要清晰、簡潔、表達力強。
- 領域特定測試語言 (Domain-Specific Testing Language):透過重構測試程式碼,建立一套專門用於編寫和閱讀測試的函式和工具,以提高測試程式碼的表達力,隱藏不必要的細節。
- 雙重標準 (A Dual Standard):測試程式碼的工程標準與產品程式碼不同。測試程式碼可以為了可讀性和便利性而犧牲效率,但在整潔性方面應保持高標準。
- 每個測試一個斷言 (One Assert per Test):一項常見規則是每個測試函式只應有一個斷言。這使得測試結果的意圖清晰。雖然不是絕對強制,但應盡量減少每個測試中的斷言數量。
- 每個測試一個概念 (Single Concept per Test):每個測試函式應只測試一個概念或一個特定的行為。將多個不相關的測試合併到一個函式中會降低可讀性。
- F.I.R.S.T. 原則:單元測試應遵循快速 (Fast)、獨立 (Independent)、可重複 (Repeatable)、自我驗證 (Self-Validating)、及時 (Timely) 的原則。
-
類別 (Classes):
- 類別應短小 (Classes Should Be Small!):衡量類別大小的標準是其職責數量,而非程式碼行數。類別應具有單一職責。
- 單一職責原則 (Single Responsibility Principle – SRP):一個類別或模組應該只有一個改變的原因。這是最重要的 OO 設計原則之一,但常常被濫用。遵循 SRP 可以提高類別的內聚性並產生更好的抽象。
- 內聚性 (Cohesion):類別應具有少量實例變數,其方法應操作這些變數。高內聚性意味著類別的方法和變數邏輯上相關並協同工作。當類別失去內聚性時,應考慮將其分割。
- 組織以便變更 (Organizing for Change):應組織類別以降低變更帶來的風險。當需要為類別添加新功能時,應盡可能避免修改現有類別(即遵循開放/封閉原則 – OCP)。
- 隔離以便變更 (Isolating from Change):透過依賴抽象(介面或抽象類別)而非具體實現來隔離變更的影響(依賴反向原則 – DIP)。這也使得系統更容易測試和重用。
-
系統 (Systems):
- 分離系統建構與使用 (Separate Constructing a System from Using It):將物件的建立和依賴關係的連接(建構過程)與系統的運行時邏輯分開。建構過程是所有應用程式都必須處理的關注點,應將其模組化。
- Main 方法的分離 (Separation of Main):將所有建構相關的程式碼移至 Main 或其呼叫的模組中,其他部分假設物件已正確建構和連接。
- 工廠模式 (Factories):當應用程式需要在運行時建立物件時,使用抽象工廠模式來控制建立的時機,但將建立的細節從應用程式程式碼中分離。
- 依賴注入 (Dependency Injection – DI):依賴注入是分離建構與使用的一種強大機制。它將依賴關係的管理職責移交給專門的容器或機制,使得類別無需自行實例化其依賴項。
- 系統架構的增量演進 (Scaling Up Architecture Incrementally):軟體系統的架構可以增量演進,無需大而全的預先設計 (BDUF)。透過測試驅動開發、重構以及清晰的程式碼,可以從簡單開始,根據需要疊代增加基礎設施。
- 跨領域關注點 (Cross-Cutting Concerns):如持久性、安全性、事務處理等關注點會跨越物件邊界。應使用方面導向程式設計 (AOP) 或類似機制 (如 Java Proxies, Spring/JBoss AOP, AspectJ) 以非侵入式的方式處理這些關注點,恢復模組性。
- 測試驅動系統架構 (Test Drive the System Architecture):當領域邏輯以 POJO 編寫,與架構關注點分離時,可以像測試程式碼一樣測試驅動架構,使其根據需要從簡單演進到複雜。
- 延遲決策 (Optimize Decision Making):模組化和關注點分離使得可以推遲決策,直到獲得更多資訊時再做出更明智的選擇。
- 明智地使用標準 (Use Standards Wisely):標準有其價值,但應在能帶來可證明價值時才使用,避免為了遵循標準而過度工程。
- 系統需要領域特定語言 (Systems Need Domain-Specific Languages – DSLs):使用 DSL 可以提高抽象層次,縮小領域概念與程式碼實現之間的差距,使得程式碼讀起來像領域專家撰寫的結構化散文。
-
湧現設計 (Emergence):
- 透過簡單設計規則獲得整潔:遵循 Kent Beck 的四個簡單設計規則 (按重要性排序):1. 運行所有測試,2. 沒有重複,3. 闡明程式設計師的意圖,4. 盡量減少類別和方法的數量。
- 運行所有測試:全面測試並通過所有測試的系統是可測試的。可測試性推動我們寫出更小、更單一職責的類別,並鼓勵使用降低耦合的技術 (如 DIP)。寫測試本身能導向更好的設計。
- 重構:一旦有了測試,就可以自信地進行重構,持續改進程式碼和設計。重構是應用簡單設計規則 (消除重複、提高表達力、減少數量) 的過程。
- 消除重複:重複是設計不良的主要敵人,應在各個層次(程式碼行、邏輯、實現)積極消除重複。模式是消除重複的常用手段。
- 表達力:程式碼應清晰地表達作者的意圖,以便於理解和維護。良好的命名、短小的函式和類別、遵循標準慣例(如設計模式命名)以及編寫好的單元測試都能提高表達力。
- 最少化類別和方法數量:在滿足前三個更重要規則的前提下,應盡量減少類別和方法的數量,避免過度細分導致碎片化。
3. 總結
編寫無瑕的程式碼需要紀律、價值觀和持續的實踐。這不是一個可以速成的過程,而是透過不斷學習、反思和改進來培養的手藝。無瑕的程式碼提高了可讀性、可維護性、可重用性和靈活性,是專業軟體開發的基石。應將測試視為產品程式碼的重要組成部分,並利用測試來驅動設計和支持重構。在系統的各個層次,都應追求清晰的意圖和分離的關注點,選擇最簡單有效的解決方案。
以上提取與解釋涵蓋了從提供的資料中關於無瑕程式碼的核心論點。
comments
comments for this post are closed