程式碼整潔之道Clean Code筆記

好奇新發表於2021-12-02

@

線上閱讀:書棧網:https://www.bookstack.cn/read/Clean-Code-zh/spilt.8.docs-ch1.md

每個章節都會做一個自己的總結,併為這個章節打一個重要性的參考分數,滿分五星(僅個人的角度)。

如果想盡快的瞭解一些程式碼的規範,最好看一下阿里程式碼規範,idea也可以安裝阿里程式碼規範外掛。

阿里開發手冊是實踐,這本書本身更多的是作者理念引導。理念的引導必然少不了佐證、和一些看似冗餘的語句,但這都是我們站在如今開發環境上的結果論,因為所有技術性文章都是具有很強的時效性。

第 1 章 Clean Code 整潔程式碼(3星)

第一章主要介紹這本書的背景和意圖,以及總結下整潔程式碼的理念

有的章節幾節可以直接越過,但是看完的話既知所以然,又知其然

?為什麼要整潔的程式碼

反證:糟糕程式碼的壞處

團隊中各司其職,程式碼是程式碼人應有的責任,要主動維護,去說服那些阻礙我們優化的。。。

?什麼叫做整潔程式碼

每個人對於整潔的定義都不同,這裡只是作者代表的一些理念,要學會自我思考

  • 意圖明確:只做一件事,提高表達力
  • 擴充套件性:提早構建小規模、簡單抽象
  • 簡潔:減少重複程式碼,包括儘量少的實體,比如類、方法、函式等
  • 正確性:能通過所有測試
  • 體現系統中的全部設計理念
  • 讀與寫花費時間的比例超過 10:1,程式碼閱讀重要性,要對自己的讀者負責
  • 整潔程式碼需要從點滴做起

成功的案例並不能讓你成為成功者,只能分享給別人成功的過程,技巧

第 2 章 Meaningful Names 有意義的命名(3星)

語義明確,語境明確,不冗餘

  • 語義明確:最好通過變數名講解變數的意義,而不是註釋。例如:魔法值,就是不能明確意義的常量

  • 避免誤導:例如:縮寫不明確;專有名詞同名;命名型別不準確;相似命名放一起區分,明確註釋;相似數字字母不明確

  • 有意義的區分,廢話都是冗餘。比如:Table 一詞永遠不應當出現在表名中

  • 使用可以讀的出來的名稱

  • 使用可以搜尋的名字:單字母名稱和數字常量僅用在短方法中的本地變數,最好不要用

  • 避免思維對映:不要你覺得,別人也會這樣覺得

  • 類名和物件名應該是名詞或名詞短語,不應當是動詞

  • 方法名應當是動詞或動詞短語

過載構造器時,使用描述了引數的靜態工廠方法名。例如,

Complex fulcrumPoint = Complex.FromRealNumber(23.0);

通常好於

Complex fulcrumPoint = new Complex(23.0);

可以考慮將相應的構造器設定為 private,強制使用這種命名手段。

  • 別用雙關語

  • 使用程式設計師熟悉的術語,或者所涉問題領域命名

  • 新增有意義的語境:對欄位進行補充說明

第 3 章 Functions 函式(3星)

本章所講述的是有關編寫良好函式的機制

  • 函式的第一規則是要短小

  • 函式應該只做好這一件事

  • 每個函式一個抽象層級

讓程式碼讀起來像是一系列自頂向下的 TO 起頭段落是保持抽象層級協調一致的有效技巧

  • switch

利用多型來實現,確保每個 switch 都埋藏在較低的抽象層級,而且永遠不重複

例子:所有的員工都有一樣的流程,是否發薪日、計算薪水、發薪水,但是不同型別的員工具體流程動作不一樣

public Money calculatePay(Employee e)
        throws InvalidEmployeeType {
    switch (e.type) {
        case COMMISSIONED:
            return calculateCommissionedPay(e);
        case HOURLY:
            return calculateHourlyPay(e);
        case SALARIED:
            return calculateSalariedPay(e);
        default:
            throw new InvalidEmployeeType(e.type);
    }
}

多型實現:更改之後再增加職員型別,每種職工只需要做自己的事情,不需要所有的型別當方法都更改,只需更改實現工廠實現類

public abstract class Employee {
    public abstract boolean isPayday();
    public abstract Money calculatePay();
    public abstract void deliverPay(Money pay);
}
-----------------
public interface EmployeeFactory {
    public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}
-----------------
public class EmployeeFactoryImpl implements
        EmployeeFactory {
    public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
        switch (r.type) {
            case COMMISSIONED:
                return new CommissionedEmployee(r);
            case HOURLY:
                return new HourlyEmployee(r);
            case SALARIED:
                return new SalariedEmploye(r);
            default:
                throw new InvalidEmployeeType(r.type);
        }
    }
}
  • 使用描述性的名稱

別害怕長名稱、別害怕花時間取名字、命名方式要保持一致

  • 函式引數: 最理想的引數數量是零
  • 無副作用函式:不要把相關性不強的邏輯放進來,強調複用性
  • 分隔指令與詢問:避免混亂
  • 使用異常替代返回錯誤碼
if (deletePage(page) == E_OK) {    if (registry.deleteReference(page.name) == E_OK) {        if (configKeys.deleteKey(page.name.makeKey()) == E_OK) {            logger.log("page deleted");        } else {            logger.log("configKey not deleted");        }    } else {        logger.log("deleteReference from registry failed");    }} else {    logger.log("delete failed");    return E_ERROR;}

On the other hand, if you use exceptions instead of returned error codes, then the error processing code can be separated from the happy path code and can be simplified:

另一方面,如果使用異常替代返回錯誤碼,錯誤處理程式碼就能從主路徑程式碼中分離出來,得到簡化:

 複製程式碼try {    deletePage(page);    registry.deleteReference(page.name);    configKeys.deleteKey(page.name.makeKey());} catch (Exception e) {    logger.log(e.getMessage());}
  • 重複可能是軟體中一切邪惡的根源
  • 好的程式碼需要慢慢打磨,精簡,優化(這正是我喜歡的過程,樂此不疲)

第 4 章 Comments 註釋(2星)

作者畢竟站在英語母語的基礎上,還是要考慮下我們自己的環境

  • 註釋不能美化糟糕的程式碼,註釋不能成為糟糕程式碼的發言人,程式碼才是核心
  • 別誤導,別廢話,適時整理TODO、註釋的程式碼塊

第 5 章 Formatting 格式 (1星)

幾乎不用看

  • 垂直、橫向格式:程式碼縮排

第 6 章 Objects and Data Structures 物件和資料結構(4星)

物件曝露行為,隱藏資料。便於新增新物件型別而無需修改既有行為,同時也難以在既有物件中新增新行為。

資料結構曝露資料,沒有明顯的行為。便於向既有資料結構新增新行為,同時也難以向既有函式新增新資料結構。

  • 資料抽象:資料封裝,隱藏具體行為
  • 資料、物件的反對稱性

過程式程式碼(使用資料結構的程式碼)便於在不改動既有資料結構的前提下新增新函式。物件導向程式碼便於在不改動既有函式的前提下新增新類。

  • 得墨忒耳律

方法不應呼叫由任何函式返回的物件的方法

下列程式碼違反了得墨忒耳律(除了違反其他規則之外),因為它呼叫了 getOptions( )返回值的 getScratchDir( )函式,又呼叫了 getScratchDir( )返回值的 getAbsolutePath( )方法。

final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();

第 7 章 Error Handling 錯誤處理(4星)

在本章中,要列出編寫既整潔又強固的程式碼——優雅地處理錯誤程式碼的一些技巧和思路

整潔程式碼是可讀的,但也要強固。可讀與強固並不衝突。如果將錯誤處理隔離看待,獨立於主要邏輯之外,就能寫出強固而整潔的程式碼

  • 使用異常而非返回碼,用統一異常處理處理異常
  • 在編寫可能丟擲異常的程式碼時, 先寫 Try-Catch-Finally 語句
  • 使用不可控異常,可控異常的代價就是違反開放/閉合原則
  • 給出異常發生的環境說明
  • 依呼叫者需要定義異常類

我們在應用程式中定義異常類時,最重要的考慮應該是它們如何被捕獲,然後根據捕獲規律去優化異常捕獲

  • 別返回 null 值
  • 別傳遞 null 值

第 9 章 Unit Tests 單元測試(3星)

  • 保持測試整潔,測試程式碼和生產程式碼一樣重要
  • 整潔的測試,最重要的要素是可讀性
  • 每個測試一個斷言
  • 測試應該有以下規則:快速、獨立、可重複、自足驗證、及時

第 10 章 Classes 類(5星)

程式碼組織的更高層面——Classes

  • 將系統的構造與使用分開
  • 類的組織

遵循標準的 Java 約定,類應該從一組變數列表開始。如果有公共靜態常量,應該先出現。然後是私有靜態變數,以及私有實體變數。很少會有公共變數。公共函式應跟在變數列表之後。封裝

  • 類應該短小

對於函式,我們通過計算程式碼行數衡量大小。對於類,我們採用不同的衡量方法,計算權責(responsibility)。類的名稱應當描述其權責,從命名開始規範。

單一權責原則(SRP)認為,類或模組應有且只有一條加以修改的理由。系統應該由許多短小的類而不是少量巨大的類組成。每個小類封裝一個權責,只有一個修改的原因,並與少數其他類一起協同達成期望的系統行為。

內聚:類應該只有少量實體變數。類中的每個方法都應該操作一個或多個這種變數。通常而言,方法操作的變數越多,就越黏聚到類上。

擴充套件性:需求會改變,所以程式碼也會改變。具體類包含實現細節(程式碼),而抽象類則只呈現概念。依賴於具體細節的客戶類,當細節改變時,就會有風險。我們可以藉助介面和抽象類來隔離這些細節帶來的影響。依賴倒置原則(Dependency Inversion Principle,DIP),DIP 認為類應當依賴於抽象而不是依賴於具體細節。

單一權責和內聚都是程度值,保證的他們平衡,邏輯內聚,權責解耦,這並不簡單,SRP也充分考慮到了程式碼擴充套件性。

第 11 章 Systems 系統(5星)

本章將討論如何在較高的抽象層級——系統層級——上保持整潔

無論是設計系統或單獨的模組,別忘了使用大概可工作的最簡單方案。

  • 將系統的構造與使用分開(編譯和執行,java的環境中Spring通過依賴注入(Dependency Injection,DI),控制反轉(Inversion of Control,IoC)已經幫我們把這件事做了)。延後初始化的好處:這種手段在 DI 中也有其作用。首先,多數 DI 容器在需要物件之前並不構造物件。其次,許多這類容器提供呼叫工廠或構造代理的機制,而這種機制可為延遲賦值或類似的優化處理所用。

  • **AOP: **在 AOP 中,被稱為方面(aspect)的模組構造指明瞭系統中哪些點的行為會以某種一致的方式被修改,從而支援某種特定的場景。這種說明是用某種簡潔的宣告或程式設計機制來實現的。

  • 代理:使用代理,程式碼量和複雜度是代理的兩大弱點,建立整潔程式碼變得很難!另外,代理也沒有提供在系統範圍內指定執行點的機制,而那正是真正的 AOP 解決方案所必須的

  • 純淨的 Java AOP 框架:Bean工廠內,每個 bean 就像是巢狀“俄羅斯套娃”中的一個,每個由資料存取器物件(DAO)代理(包裝)的 Bank 都有個域物件,而 bean 本身又是由 JDBC 驅動程式資料來源代理。通過XML/註解的方式減少對程式碼的入侵,只留下純POJO。

  • AspectJ ASPECTS AspectJ 的方面:AspectJ 卻提供了一套用以切分關注面的豐富而強有力的工具。

  • 測試驅動系統架構:大設計(Big Design Up Front,BDUF)——系統架構。最佳的系統架構由模組化的關注面領域組成,每個關注面均用純 Java(或其他語言)物件實現。不同的領域之間用最不具有侵害性的方面或類方面工具整合起來。這種架構能測試驅動,就像程式碼一樣。

  • 優化決策:模組化和關注面切分成就了分散化管理和決策。擁有模組化關注面的 POJO 系統提供的敏捷能力,允許我們基於最新的知識做出優化的、時機剛好的決策。決策的複雜性也降低了。

  • 選擇合適的架構——標準

  • 系統需要領域特定語言:領域特定語言(Domain-Specific Language,DSL)。DSL 是一種單獨的小型指令碼語言或以標準語言寫就的 API,領域專家可以用它編寫讀起來像是組織嚴謹的散文一般的程式碼。領域特定語言允許所有抽象層級和應用程式中的所有領域,從高階策略到底層細節,使用 POJO 來表達。

第 12 章 Emergence 迭進

本章中寫到的實踐來自於本書作者數十年經驗的精練總結。遵循簡單設計的實踐手段,開發者不必經年學習就能掌握好的原則和模式。

提升內聚性,降低耦合度,切分關注面,模組化系統性關注面,縮小函式和類的尺寸,選用更好的名稱,如此等等。這也是應用簡單設計後三條規則的地方:消除重複,保證表達力,儘可能減少類和方法的數量。

通過迭進設計達到整潔目的,Kent Beck 關於簡單設計的四條規則,據 Kent 所述,只要遵循以下規則,設計就能變得“簡單”,以下規則按其重要程度降序排列:

  • 執行所有測試;
  • 不可重複;
  • 表達了程式設計師的意圖;
  • 儘可能減少類和方法的數量;

第 13 章 Concurrency 併發程式設計(5星)

“物件是過程的抽象。執行緒是排程的抽象。” ——James O

這個章節主要講述了併發程式設計的來源、優勢和劣勢,以及如何避免、解決併發錯誤的方法和方向

? 為什麼要併發

  • 併發是一種解耦策略。

  • 解耦目的與時機能明顯地改進應用程式的吞吐量和結構。

  • 併發會在效能和編寫額外程式碼上增加一些開銷;

  • 正確的併發是複雜的,即便對於簡單的問題也是如此;

  • 併發缺陷並非總能重現,所以常被看做偶發事件而忽略,未被當做真的缺陷看待;

  • 併發常常需要對設計策略的根本性修改。

併發防禦原則

  • 單一權責原則

單一權責原則(SRP)認為,方法/類/元件應當只有一個修改的理由。併發設計自身足夠複雜到成為修改的理由,所以也該從其他程式碼中分離出來。不幸的是,併發實現細節常常直接嵌入到其他生產程式碼中。

需要考慮的問題:

併發相關程式碼有自己的開發、修改和調優生命週期;

開發相關程式碼有自己要對付的挑戰,和非併發相關程式碼不同,而且往往更為困難;

即便沒有周邊應用程式增加的負擔,寫得不好的併發程式碼可能的出錯方式數量也已經足具挑戰性。

建議:分離併發相關程式碼與其他程式碼。

  • 限制資料作用域

兩個執行緒修改共享物件的同一欄位時,可能互相干擾,導致未預期的行為。解決方案之一是採用 synchronized 關鍵字在程式碼中保護一塊使用共享物件的臨界區(critical section)。限制臨界區的數量很重要。更新共享資料的地方越多,就越可能:

謹記資料封裝;嚴格限制對可能被共享的資料的訪問。

避免共享資料的好方法之一就是一開始就避免共享資料。

執行緒應儘可能地獨立,讓每個執行緒在自己的世界中存在,不與其他執行緒共享資料。

瞭解 Java 庫

學習類庫,瞭解基本演算法。理解類庫提供的與基礎演算法類似的解決問題的特性。

瞭解執行模型

學習這些基礎演算法,理解其解決方案。

  • Producer-Consumer 生產者-消費者模型
  • Readers-Writers 讀者-作者模型
  • Dining Philosophers 哲學家用餐模式

警惕同步方法之間的依賴

  • 避免使用一個共享物件的多個方法
  • 有時必須使用一個共享物件的多個方法,有 3 種應對手段:
  • 基於客戶端的鎖定——客戶端程式碼在呼叫第一個方法前鎖定服務端,確保鎖的範圍覆蓋了呼叫最後一個方法的程式碼;
  • 基於服務端的鎖定——在服務端內建立鎖定服務端的方法,呼叫所有方法,然後解鎖。讓客戶端程式碼呼叫新方法;
  • 適配服務端——建立執行鎖定的中間層。這是一種基於服務端的鎖定的例子,但不修改原始服務端程式碼。

儘可能減小同步區域

儘早考慮關閉問題,儘早令其工作正常。

測試執行緒程式碼

編寫有潛力曝露問題的測試,在不同的程式設計配置、系統配置和負載條件下頻繁執行。如果測試失敗,跟蹤錯誤。別因為後來測試通過了後來的執行就忽略失敗。

有一大堆問題要考慮。下面是一些精練的建議:

  • 將偽失敗看作可能的執行緒問題,不要將系統錯誤歸咎於偶發事件
  • 先使非執行緒程式碼可工作, 不要同時追蹤非執行緒缺陷和執行緒缺陷。確保程式碼線上程之外可工作。
  • 編寫可插拔的執行緒程式碼,這樣就能在不同的配置環境下執行。
  • 編寫可調整的執行緒程式碼,要獲得良好的執行緒平衡,常常需要試錯。一開始,在不同的配置環境下監測系統效能。要允許執行緒數量可調整。在系統執行時允許執行緒發生變動。允許執行緒依據吞吐量和系統使用率自我調整。
  • 執行多於處理器數量的執行緒,系統在切換任務時會發生一些事。為了促使任務交換的發生,執行多於處理器或處理器核心數量的執行緒。任務交換越頻繁,越有可能找到錯過臨界區或導致死鎖的程式碼。
  • 在不同平臺上執行
  • 裝置試錯程式碼,並強迫錯誤發生:有兩種裝置程式碼的方法:硬編碼、自動化

第 15 章 JUnit Internals JUnit 內幕(2星)

本章介紹了JUnit的一些簡單的模組

第 16 章 重構 SerialDate(4星)

本章詳解對 org.jfree.date庫中的SerialDate日期類進行重構,簡化的過程。增加了測試覆蓋率,修復了一些錯誤,澄清並縮小了程式碼。

第 17 章 味道與啟發(3星)

本章又列舉了作者之前列出過的,一些不好的習慣,並把這些比作難聞的氣味

乾淨的程式碼不是通過遵循一組規則來編寫的。

附錄 A 併發程式設計 II(4星)

併發程式設計的一些擴充資訊,多了很多的示例講解

在本章中,我們談到併發更新,還有清理及避免同步的規程。我們談到執行緒如何提升與 I/O 有關的系統的吞吐量,展示了獲得這種提升的整潔技術。我們談到死鎖及乾淨地避免死鎖的規程。最後,我們談到通過裝置程式碼暴露併發問題的策略。

  • 死鎖

死鎖的發生需要 4 個條件:

互斥:無法在同一時間為多個執行緒所用;數量上有限制

這種資源的常見例子是資料庫連線、開啟後用於寫入的檔案、記錄鎖或是訊號量。

上鎖及等待:當某個執行緒獲取一個資源,在獲取到其他全部所需資源並完成其工作之前,不會釋放這個資源。

無搶先機制:執行緒無法從其他執行緒處奪取資源。一個執行緒持有資源時,其他執行緒獲得這個資源的唯一手段就是等待該執行緒釋放資源。

迴圈等待:這也被稱為“死命擁抱”。想象兩個執行緒,T1 和 T2,還有兩個資源,R1 和 R2。T1 擁有 R1,T2 擁有 R2。T1 需要 R2,T2 需要 R1。

這 4 種條件都是死鎖所必需的。只要其中一個不滿足,死鎖就不會發生。

避免死鎖的一種策略是規避互斥條件。你可以:

  • 使用允許同時使用的資源;
  • 增加資源數量,使其等於或大於競爭執行緒的數量;
  • 在獲取資源之前,檢查是否可用。
  • 不上鎖及等待
  • 滿足搶先機制
  • 不做迴圈等待

將解決方案中與執行緒相關的部分分隔出來,再加以調整和試驗,是獲得判斷最佳策略所需的洞見的正道。

總結

乾淨有經驗值,也有固定分,不是通過遵循一組規則來編寫的,需要的是迭進,不需要鑽牛角尖。

讀英文原文的時候突然想到:英語大多是結果論,喜歡陳述事實,就好像罪犯的對白

相關文章