《Clean Code》閱讀筆記

Richard_Yi發表於2020-04-07

原文地址:《Clean Code》 閱讀筆記

轉載請註明出處!

這是一本真正的好書,不過如果讀者沒有一定的經驗,以及缺乏對程式設計境界的追求的話,可能認為這本書很一般。當然,對於有心人來說,這本書裡面的部分東西可能都已經習以為常了。

那麼,你是怎樣的呢?

另外我為什麼寫的是《Clean Code》而不是《程式碼整潔之道》,因為這本書很多地方你需要看原版的文字才能get到作者真正想表達的意思。如果有能力還是看原版吧。

看原版書,你能學到很多術語表達,在你看外文技術文章的時候更容易幫助你理解全文。如increase cohesion - 增加內聚性,decrease coupling - 減少耦合,separate concerns - 關注點分離,modularize system concerns - 模組化系統關注點,這些都是很經典的表達。

I sincerely and strongly recommend u to read 《Clean Code》 rather than 《程式碼整潔之道》

一、整潔程式碼 ⭐

關鍵詞:優雅

  1. 程式碼邏輯直接了當,讓缺陷難以隱藏
  2. 儘量減少依賴關係,使之便於維護
  3. 依據某種分層策略完善錯誤處理程式碼
  4. 效能調至最優,省得引誘別人做沒規矩的優化
  5. 整潔的程式碼只做一件事
  6. 簡單直接,具有可讀性
  7. 有單元測試和驗收測試
  8. 有意義的命名
  9. 程式碼應在字面上表達其含義
  10. 儘量少的實體:類、方法、函式
  11. 沒有重複程式碼

整潔的程式碼讀起來令人愉悅

二、有意義的命名 ⭐⭐

  • 使用帶有語義的命名,能 夠讓維護程式碼的人更容易理解和修改程式碼
  • 程式設計本來就是一種社會活動(大部分的程式設計活動都是人與人協作的過程)
  • 避免思維對映,明確才是王道
  • 儘可能要做到“顧名思義”,看到名稱就能知道這個變數、函式、類、包的意義、用途。

具體規則

  1. 名副其實:名稱不需要註釋補充就可見其含義、用途

  2. 不要寫多餘的廢話或者容易讓人混淆的命名。

    比如"customerObject"和"customer", "ProductInfo"和"ProductData";這種就是意義混雜的廢話。如果真的有區別,就用特定的可以區分的命名來描述它。

  3. 使用讀得出來的名稱。

  4. 使用可搜尋的名稱。

    MAX_CLASSES_PER_STUDENT很容易,但想找數字7就麻煩了。

  5. 類名和物件名應該是名詞或名詞短語。

  6. 方法名應當是動詞或動詞短語。

    如postPayment、deletePage或save。屬性訪問器、修改器和斷言應該根據其值命名,並依Javabean標準加上get、set和is字首。

  7. 每個抽象概念選一個詞,並且一以貫之

    我的理解中,在同個領域模型中,就應該只有一個命名,比如訂單號,同個系統中不應該出現TradeNo、OrderNo等多個命名。

  8. 儘量用術語(CS術語,演算法,數學術語)命名

    儘管用那些電腦科學(Computer Science,CS)術語、演算法名、模式名、數學術語。

  9. 上一條無法做到的情況下,儘量使用源自所涉問題領域的名稱。

    如果不能用程式設計師熟悉的術語來給手頭的工作命名,就採用從所涉問題領域而來的名稱。

  10. 新增富有意義的語境,例如利用UserInfo類封裝各種個人資訊

三、函式 ⭐⭐

程式設計就像講故事,要用準確、清晰、富有表達力的語句(程式碼)

  • 好的函式應該做到自頂向下閱讀程式碼時,像是在閱讀報刊文章。
  • 寫程式碼很像是寫文章。先想怎麼寫就怎麼寫,然後再打磨:分解函式、修改名稱、消除重複
  • 程式設計其實是一門語言設計藝術,大師級程式設計師把程式系統當做故事來講。使用準確、清晰、富有表達力的程式碼來幫助你講故事。

具體規則

  1. 短小!短小!短小

    重要的事情說3遍。

  2. 函式應該做一件事。做好這件事。只做這一件事

  3. 每個函式一個抽象層級!!!

    這個是編者認為非常重要的一點,也是本人在開發過程當中看到最多的問題。應該處於不同抽象層級的程式碼混亂在一起時,閱讀和理解起來會很痛苦。

    引原文描述:

    函式中混雜不同抽象層級,往往讓人迷惑。讀者可能無法判斷某個表示式是基礎概念還是細節。更惡劣的是,就像破損的窗戶,一旦細節與基礎概念混雜,更多的細節就會在函式中糾結起來。

    但是,就像作者說的,這條規則很難

  4. 使用描述性的名稱

    長而具有描述性的名稱,要比短而令人費解的名稱好。長而具有描述性的名稱,要比描述性的長註釋好。

    為只做一件事的小函式取個好名字。函式越短小、功能越集中,就越便於取個好名字。

  5. 拒絕boolean型標識引數。

    例: CopyUtil.copyToDB(isWorkDB) --> CopyUtil.copyToWorkDB(), CopyUtil.copyToLiveDB()

    (但是編者閱讀很多原始碼裡面也沒有遵守,手動狗頭...)

  6. 如果一定需要多個引數,那麼可能需要對引數進行封裝

  7. 使用異常代替返回錯誤碼,錯誤處理程式碼就能從主路徑程式碼中分離出來得到簡化。

  8. Don't Repeat Yourself(經典的DRY原則)

  9. 先把函式寫出來,再規範化

四、註釋

這節實際上內容不多,儘量避免註釋

  • 別給糟糕的程式碼加註釋(專家建議不如重寫)
  • 把力氣花在寫清楚明白的程式碼上,直接保證無需編寫註釋。
  • 好的註釋:
    • 法律資訊
    • 提供資訊
    • 解釋意圖
    • 警示
    • TODO註釋

五、格式 ⭐

  • 程式碼格式很重要。程式碼格式關乎溝通,而溝通是專業開發者的頭等大事。
  • 向報紙格式學習程式碼編寫。

具體規則

  1. 垂直距離

    1. 變數宣告應該儘可能靠近使用位置,本地變數應該在函式頂部出現
    2. 實體變數應該放在類的頂部宣告
    3. 相關的函式應該放在一起
    4. 函式的排列順序保持其相互呼叫的順序
  2. 水平位置

    1. 一行程式碼儘量短,不超過100 - 120 個字元。

      這個在常見的IDE中可以設定提示線。下圖是IDEA的配置位置。

      效果:

    2. 用空格將相關性弱的分開

    3. 宣告和賦值不需要水平對齊

    4. 注意縮排

  3. 團隊之間形成一致的程式碼格式規範(Checkstyle 外掛瞭解一下?)

    不要使用不同的風格來編寫原始碼,會增加其複雜度。

六、物件與資料結構

這塊有兩個我比較在意的概念

  • 要弄清楚資料結構和物件的差異:物件把資料隱藏於抽象之後,曝露運算元據的函式。資料結構曝露其資料,沒有提供有意義的函式

  • The Law of Demeter:模組不應瞭解它所操作物件的內部情形。

    更準確更白話地說:方法不應呼叫由任何函式返回的物件的方法。只跟朋友談話,不與陌生人談話。

    反例:

    final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
    複製程式碼

七、錯誤處理 ⭐⭐

  • 一個原則:錯誤處理很重要,但是如果它搞亂了程式碼邏輯,就是錯誤的做法

  • 整潔程式碼是可讀的,但也要強固。可讀與強固並不衝突。如果將錯誤處理隔離看待,獨立於主要邏輯之外,就能寫出強固而整潔的程式碼。做到這一步,我們就能單獨處理它,也極大地提升了程式碼的可維護性。

具體規則

  1. 使用異常而非返回碼

  2. 使用不可控異常(這點深有體會,checked Exception的代價很大)

    這裡作者想說明的是,在使用受檢異常時,你首先要考慮這樣是否能值回票價。因為受檢異常違反了開閉原則,當你在一個方法內丟擲了受檢異常時,你就得在catch語句和丟擲異常之間的方法呼叫鏈中的每個方法簽名中宣告這個異常。

    這意味著,你對軟體較低層級的修改,會涉及到較高層級的簽名。封裝被打破了,因為在丟擲路徑中的每個函式都要去了解下一層級的異常細節。既然異常旨在讓你能在較遠處處理錯誤,可控異常以這種方式破壞封裝簡直就是一種恥辱

    如果你在編寫一套關鍵程式碼庫,則可控異常有時也會有用:你必須捕獲異常。但對於一般的應用開發,其依賴成本要高於收益。

  3. 給出異常發生的環境說明(這個也很重要)

    建立資訊充分的錯誤訊息,並和異常一起傳遞出去。在訊息中,包括失敗的操作和失敗型別。如果你的應用程式有日誌系統,傳遞足夠的資訊給catch塊,並記錄下來。

    良好的日誌和異常機制,是不應該出現除錯的。打日誌和拋異常,一定要把上下文給出來,否則,等於在毀滅命案現場,把後邊處理問題的人,往歪路上帶。

    需要除錯來查詢錯誤時,往往是一種對異常處理機制的侮辱

  4. 使用通用異常類打包第三方API包的異常(如呼叫一些第三方支付SDK等)

  5. 嘗試使用特例模式(SPECIAL CASE PATTERN),將異常行為封裝到特例物件中。

    很巧妙高階的一種設計模式。

    // 修改前
    try {
        MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
        m_total += expenses.getTotal();
    catch(MealExpensesNotFound e) {
        m_total += getMealPerDiem();
    }


    // 優化之後,當沒有餐食消耗(即上述程式碼丟擲MealExpensesNotFound的情況),返回特例物件
    MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
    m_total += expenses.getTotal();

    // 特例物件
    public class PerDiemMealExpenses implements MealExpenses {
        public int getTotal() {
        // return the per diem default
        }
    }
    複製程式碼
  6. 不要返回null,不要傳遞null

    相信不少程式設計師都深受null 的困擾。返回null值,基本上是在給自己增加工作量,也是在給呼叫者添亂。只要有一處沒檢查null值,應用程式就會失控。

    在大多數程式語言中,沒有良好的方法能對付由呼叫者意外傳入的null值。事已如此,恰當的做法就是禁止傳入null值。

八、邊界

邊界這一章個人讀起來比較難懂。感覺像是翻譯的問題。

原書這一章節的名字叫做"Boundaries"。

這一章篇幅較短,意義有點難懂,這裡簡單總結:作者的意思是讓我們自己的程式碼和第三方庫的程式碼不要耦合太緊密,需要有清晰的Boundaries。

同時也給出了第三方類庫的學習建議:探索性地學習測試,以此熟悉類庫,寫出良好的程式碼。

九、單元測試

  • 測試程式碼和生產程式碼一樣重要。它可不是二等公民。

    它需要被思考、被設計和被照料。它該像生產程式碼一般保持整潔。

    測試程式碼需要隨著生產程式碼的演進而修改,如果測試不能保持整潔,只會越來越難修改。

  • 整潔的測試有什麼要素?有三個要素:可讀性,可讀性和可讀性

  • 每個測試一個斷言,每個測試一個概念。

單測本身也應該成為Code Review的一部分,單測寫的好,bug一定少。

TDD 三定律

  • 定律一 在編寫不能通過的單元測試前,不可編寫生產程式碼。
  • 定律二 只可編寫剛好無法通過的單元測試,不能編譯也算不通過。
  • 定律三 只可編寫剛好足以通過當前失敗測試的生產程式碼。

任何一種迭代和增量的交付方式,都會遇到一個嚴肅的靈魂拷問:頻繁對軟體做修改,如何保障軟體不被改壞?這個問題,用人肉測試解決不了。交付越頻繁,人肉測試就越不可能跟上節奏。自動化的、快速且可靠的、覆蓋完善的測試必不可少。這種要求,後補式的、黑盒的測試方法不可能達到,必須在開發軟體的過程中內建。

當團隊被迫採用迭代和增量的需求管理和專案管理方式,對應的配置管理和質量保障手段就必須跟上。TDD不是錦上添花,而是迭代和增量交付不可或缺的基石

F.I.R.S.T.

整潔的測試應該遵循以下5條規則:

  • 快速(Fast)

    測試應該夠快。測試應該能快速執行。測試執行緩慢,你就不會想要頻繁地執行它。如果你不頻繁執行測試,就不能儘早發現問題,也無法輕易修正,從而也不能輕而易舉地清理程式碼。最終,程式碼就會腐壞。

  • 獨立(Independent)

    測試應該相互獨立。某個測試不應為下一個測試設定條件。你應該可以單獨執行每個測試,及以任何順序執行測試。當測試互相依賴時,頭一個沒通過就會導致一連串的測試失敗,使問題診斷變得困難,隱藏了下級錯誤。

  • 可重複(Repeatable)

    測試應當可在任何環境中重複通過。你應該能夠在生產環境、質檢環境中執行測試,也能夠在無網路的列車上用膝上型電腦執行測試。如果測試不能在任意環境中重複,你就總會有個解釋其失敗的介面。當環境條件不具備時,你也會無法執行測試。

  • 自足驗證(Self-Validating)

    測試應該有布林值輸出。無論是通過或失敗,你不應該檢視日誌檔案來確認測試是否通過。你不應該手工對比兩個不同文字檔案來確認測試是否通過。如果測試不能自足驗證,對失敗的判斷就會變得依賴主觀,而執行測試也需要更長的手工操作時間

  • 及時(Timely)

    測試應及時編寫。單元測試應該恰好在使其通過的生產程式碼之前編寫。如果在編寫生產程式碼之後編寫測試,你會發現生產程式碼難以測試。你可能會認為某些生產程式碼本身難以測試。你可能不會去設計可測試的程式碼

十、類 ⭐⭐

類應該儘量短小

對於衡量類的大小,這裡書中提出了一個不同的衡量方法:計算權責。我理解的意思就是,一個類承擔了太多的權責之後,這個類就算大了。

所以書中隨即提出了SRP - 單一權責原則(也叫單一職責原則)

單一權責原則

單一權責原則(SRP)認為,類或模組應有且只有一條加以修改的理由。該原則既給出了權責的定義,又是關於類的長度的指導方針。類只應有一個權責——只有一條修改的理由。

作者還提到了,系統應該由許多短小的類而不是少量巨大的類組成。每個小類封裝一個權責,只有一個修改的原因,並與少數其他類一起協同達成期望的系統行為。

內聚

同時,作者提出了保持內聚性就會得到許多短小的類。

類的高內聚的含義是:類的實體變數應儘可能少,類中方法儘可能多地使用到這些變數。(如果一個類中的每個變數都被每個方法所使用,則該類具有最大的內聚性)

組織類時考慮程式碼的修改

在整潔的系統中,我們對類加以組織,以降低修改的風險。

  • 開放-閉合原則(OCP)

    類應當對擴充套件開放,對修改封閉。通過子類化手段,類對新增新功能是開放的,而且可以同時不觸及其他類。

  • 依賴倒置原則(Dependency Inversion Principle,DIP)

    DIP認為類應當依賴於抽象而不是依賴於具體細節。通過這種抽象隔離了系統之間的元素,使得系統每個元素的理解變得更加容易,使用起來更加靈活、更加可複用。

十一、系統

系統構造與使用分開。

這裡我理解就是將一些物件例項的初始化和使用分離解耦,將構建例項的邏輯交給一個公共的模組/類/框架來做。這裡作者也介紹了開發中常見的兩種方式,體現了這種思想:

  • 工廠:使用工廠方法自行決定何時建立例項,但是構造細節卻在其他地方

  • 依賴注入:當A對B有依賴時,A中不負責B的例項化(這就是類的權責單一原則

後半章主要講的是AOP的思想和具體的框架實現。就是說將一些重複性、功能性的程式碼(如:效能監視、日誌記錄、事務管理、安全檢查、快取等)進行關注面切分,模組化,成就了分散化管理和決策。最終的效果也顯而易見,減少了重複程式碼,關注面的分離也使得設計、決策更加清晰容易。

十二、Emergence (迭進) ⭐

這一節主要是講了四個簡單的設計規則(design rules),通過遵循這四個規則,你可以編寫出很好的程式碼,深入瞭解程式碼的結構和設計,繼而以一種更簡單的方式來學習掌握SRP和DIP之類的設計原則。

Four rules of Simple Design are of significant help increating well-designed software

  • 執行所有的測試

    全面測試並持續通過所有測試。遵循SRP的類,測試起來較為簡單。測試編寫得越多,就越能持續走向編寫較易測試的程式碼。所以,確保系統完全可測試能幫助我們建立更好的設計。

    有了全面的測試保駕護航之後,我們就有條件一步一步地去重構完善我們的程式碼,目的是為了得到“高內聚,低耦合”的系統。書中也提出了下面三條簡單的規則。

  • 不要重複(DRY)

  • 寫出能清晰表達編碼者意圖的程式碼(Expressive)

  • 儘量減少類和方法(Mininal Classes and Methods)

    當你在重構時,按照SRP、程式碼可讀性等規則遵守,是有可能建立出比原來更多的細小的類。但這不在本條的針對範圍之內。

    這裡的儘量減少,作者舉例了一種情況,就是毫無意義的教條主義會導致編碼人員無意識的建立很多的類和方法。不知道你有沒有類似的經歷,我拿我親身體會舉個例子,我很難理解在某個專案中,對一個領域物件(如User),在構建對應的Service層和Dao層的時候,一定要為每個類建立介面,即使這些介面根本不可能有其他的實現類。

十三、併發

“Objects are abstractions of processing. Threads are abstractions of schedule.” — James O. Coplien

這一節作者討論了併發程式設計的需求和難點,並且給出了一些解決這些困難和編寫整潔併發程式碼的建議。因為關於併發程式設計有更好的資料可以學習,所以這裡我就簡單總結一下。

併發防禦原則

  • 單一權責原則(SRP):方法/類/元件應當只有一個修改的理由
  • 限制資料作用域:嚴格限制對可能被共享的資料的訪問
  • 使用資料複本:這點很好理解,避免資料的共享。(Java 中的ThreadLocal)
  • 執行緒應儘可能獨立:不與其他執行緒共享資料。每個執行緒處理一個客戶端請求,從不共享的源頭接納所有請求資料,儲存為本地變數

小結

其他未提到的章節,是我覺得相較來說非重點的章節。還有可能會有一些內容的遺漏,因為這本書中的精華,我覺得我還需要學習領會。

好書常讀常新,這本書就在我的工位上,我希望在經歷一段時間的工作實踐之後,再次開啟這本書,我能有更多更新的一些感悟。

如果本文有幫助到你,希望能點個贊,這是對我的最大動力????。

相關文章