Code Clean讀書筆記

代碼整潔之道讀書筆記

  • by fangpcjava

    序言部分

  • "神在細節之中" — 建築師路德維希
  • 5S哲學(精益)
  • 整理(Seiri):搞清楚事物之所在——經過恰當地命名之類的手段——相當重要
  • 整頓(Seiton):每段代碼都應該在你但願它所在的地方——若是不在那裏,就須要重構了
  • 清楚(Seiso):或謂清潔,清理工做地拉線、油污和邊角廢料
  • 清潔(Seiketsu):或謂標準化,開發組內使用統一的代碼風格和實踐手段
  • 身美(Shisuke):或謂紀律(自律),在實踐中貫徹規程,並時時體現於我的工做上,並且要樂於改進程序員

    代碼猴子和童子軍軍規

  • 沉迷測試(Test Obsessed)
  • 不要作代碼猴子(Do not be a code monkey)編程

    第一章——整潔代碼

    一、代碼永不消失
    代碼就是銜接人腦理解需求的含糊性和機器指令的精確性的橋樑。哪怕將來會有對如今高級編程語言的再一次抽象——但這個抽象規範自身仍舊是代碼。因此既然代碼會一直存在下去,且本身都幹了程序員這一行了,就好好的對待它吧。json

二、讀遠比寫多
當你錄下你平時的編碼的過程,回放時,你會發現讀代碼所花的時間遠比寫的多,甚至超過 10:1。因此整潔的代碼會增長可讀性。網絡

三、稍後等於永不
糟糕的代碼會致使項目的難以維護。而當你正在寫糟糕的代碼的時候,內心卻聖潔的想:「有朝一日再回來整理」。但現實是殘酷的,正如勒布朗(LeBlanc)法則:稍後等於永不(Later equals never)數據結構

四、精益求精
寫代碼就跟寫文章同樣,先自由發揮,再細節打磨。追求完美。架構

第二章——有意義的命名

1.名副其實而且也命名有意義。 提及來很簡單。選個好名字須要花時間,但省下的時間比花掉的多。注意命名,一旦有好的命名,就換掉舊的。app

int d;// 消失的時間,以日計。
int elapsedTimeInDays;框架

2.避免誤導。好比不是List類型,就不要用個accountList來命名,這樣造成誤導。編程語言

3.作有意的區分。

Public static void copyChars(char a1[],char a2[]){
    for(int i=0;i<a1.length;i++){
        a2[i]=a1[i];
    } 
}

若是參數名稱改成source和destination ,這個函數就會像樣不少。廢話都是冗餘的,Variable一詞 永遠不該當出如今變量名中。Table一詞永遠不該當出如今表名中。NameString 會比 Name好嗎,難道Name 會是一個浮點數不成?若有一個Customer的類,有又一個CustomerObject的類。是否是就凌亂了。

4.使用便於搜索的的名稱
單個字母或者數字常量是很難在一大堆文章中找出來。好比字母e,它是英文中最經常使用的字母。長名勝於短名稱,搜獲得的名稱勝於自編的名稱。 竊覺得單字母的名稱僅用於短方法中的本地變量。名稱長短應與其做用域大小相對應。

5.類名應該是名詞或短語,像Customer,Account,避免使用Manager,Processor,Data或者Info這樣的類名。類名不該當是動詞。方法名應該是動詞或動詞短語,如postPayment ,deletePage或Save,屬性訪問、修改和斷言應該根據其值來命名,並加上get,set,is這些前綴。

6.別扮可愛,耍寶,好比誰知道HolyHandGrenada 函數是幹什麼的,沒錯這個名字挺伶俐,可是不過DeleteItems或許是更好的名字。

7.每一個概念對應一個詞。而且一以貫之。
在一堆代碼中有Controller,又有manager,driver。就會使人困惑。好比DeviceManager和Protal-Controller之間又什麼本質區別?

第三章——函數

1.短小:函數要短小,更短小。極長的函數邏輯是不清晰的,讀很長的函數每每會被不少瑣碎的細節分神,好的函數應該是模塊化的。
2.只作一件事:一個函數應該儘可能只作並作好一件事。
3.向下規則:代碼的閱讀順序應該是自頂向下的,要讓每一個函數後面都跟着位於下一抽象層級的函數。Logic -> Service -> Dao。
4.函數命名:
    - 函數越短、功能越集中,就越便於取個好名字。
    - 別懼怕長名稱。長而具備描述性的名稱,要比短而使人費解的名稱好。長而具備描述性的名稱,要比描述性的長註釋要好。
    - 命名方式要保持一致。
5.函數參數:零參數函數 > 單參數函數 > 雙參數參數 > 三參數參數(避免),參數越少越好
    - 測試友好
    - 函數封閉性更好
    - 標識參數醜陋不堪,駭人聽聞
    - 無反作用:若是必定要時序性耦合,就該在函數名稱中說明。
    - 分隔指令與詢問:一個函數作一件事,表明一個動做,不易引發歧義。
    - 拋異常代替返回錯誤碼:把try-catch代碼塊抽出來造成檢測函數
    - DRY:杜絕重複代碼,凡是須要用至少兩次的代碼,給它單獨寫成一個類或函數
6.結構化編程:
    - Dijkstra結構化編程規則:每一個函數、函數中的每一個代碼塊都應該有一個入口、一個出口。每一個函數只能有一個return語句,循環中不能有break或continue語句,永遠不能由goto

第四章——註釋

1.註釋不能美化糟糕的代碼:與其花時間編寫解釋你寫出的糟糕代碼的註釋,不如花時間整理你那糟糕的代碼。
2.用代碼來闡述:用代碼來解釋意圖而不是註釋
3.好註釋:
    - 對意圖的解釋:某個決定後面的意圖。
    - 闡釋:翻譯參數或返回值,闡釋性註釋自己就有不正確的風險。當改變了參數或者返回值的時候記得更新闡釋性註釋。
    - 警示:不要那麼作,或者怎麼作會有怎樣的風險和結果
    - TODO註釋: 應該作,可是還沒作。不要讓TODO成爲在系統中留下糟糕代碼的藉口。
4.壞註釋:
    - 喃喃自語
    - 冗餘註釋
    - 誤導性註釋
    - 循規式註釋

第五章——格式

1.概念間垂直方向上的間隔:代碼中的空白行會增長代碼的可讀性。在封包聲明、導入聲明、每一個函數之間都要用空白行隔開。
@Service("userService")
    public class UserServiceImpl implements IUserService {
        @Resource
        private UserDao userDao;
        public User getUserById(int userId) {
            // TODO Auto-generated method stub
            return userDao.selectById(userId);
        }
        @Override
        public List<User> selectUser(int start, int limit) {
            // TODO Auto-generated method stub
            return userDao.selectUser(start,limit);
        }
        @Override
        public List<User> getUsersListPage(Page page) {
            // TODO Auto-generated method stub
            return userDao.getUsersListPage(page);
        }

    }
@Service("userService")
    public class UserServiceImpl implements IUserService {

        @Resource
        private UserDao userDao;
        public User getUserById(int userId) {
            // TODO Auto-generated method stub
            return userDao.selectById(userId);
        }

        @Override
        public List<User> selectUser(int start, int limit) {
            // TODO Auto-generated method stub
            return userDao.selectUser(start,limit);
        }

        @Override
        public List<User> getUsersListPage(Page page) {
            // TODO Auto-generated method stub
            return userDao.getUsersListPage(page);
        }

    }
2.垂直方向上的靠近:空白行隔開了概念,靠近的代碼行則暗示它們之間的緊密聯繫。緊密相關的代碼應該互相靠近。
3.垂直距離:
    - 變量聲明:變量聲明應該儘量在其使用的位置。
    - 實體變量:在類的頂部聲明,在設計良好的類中,實體變量應該被該類的大多數方法所用。
    - 相關函數:調用函數和被調用函數放在一塊兒,而且調用函數放在被調用函數上面。
private String distributionAward(ReferralRankRecord referralRankRecord) {
        Map<Integer, String> awardRules = JsonUtils.jsonToMap(awardsConfiguration,
                new TypeReference<Map<Integer, String>>() {
                });

        List<Integer> rankingDemarcation = new ArrayList<>(awardRules.keySet());
        int awardGradeIndex = binarySearchAward(rankingDemarcation, referralRankRecord.getRankingNumber());
        if (awardGradeIndex > awardDemarcation && referralRankRecord.getCount() < 3) {
            return noAward;
        }

        return awardRules.get(awardGradeIndex);
    }

    private int binarySearchAward(List<Integer> rankingDemarcation, int rankingNumber) {
        int left = 0;
        int right = rankingDemarcation.size() - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (rankingDemarcation.get(mid) == rankingNumber) {
                return rankingDemarcation.get(mid);
            } else if (rankingDemarcation.get(mid) < rankingNumber) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }

        return rankingDemarcation.get(right);
    }
- 概念相關:概念相關的代碼應該放到一塊兒。相關性越強,彼此之間的距離就應該越短。
    - 自頂向下貫穿源代碼模塊的信息流,好比自上向下展現函數調用依賴順序。
4.橫向格式:
    - 水平方向上的區隔與靠近:
        * 空格字符能夠把相關性較弱的事物分隔開
        * 賦值操做周圍加上空格字符用於強調賦值語句有兩個肯定而重要的要素:左邊和右邊。空格字符增強了分割效果。
left=mid+1;
left = mid + 1;
- 函數名和左圓括號之間不加空格代表函數和其參數緊密相關,括號中的參數要隔開代表參數是相互分離的。
private int binarySearchAward(List<Integer> rankingDemarcation, int rankingNumber)
private int binarySearchAward (List<Integer> rankingDemarcation, int rankingNumber)
- 水平對齊:
public class ReferralRankRecord {

                    private int rankingNumber;          // 排名
                    private int userId;             // 轉介紹人userId
                    private int count;              // 轉介紹數量
                    private long latestUpdateTime;  //最近一條轉介紹記錄更新時間
                }
public class ReferralRankRecord {

                    private int  rankingNumber;          // 排名
                    private int  userId;                 // 轉介紹人userId
                    private int  count;                  // 轉介紹數量
                    private long latestUpdateTime;       //最近一條轉介紹記錄更新時間
                }
- 縮進:縮進能夠幫助程序員快速跳過與當前關注的情形無關的範圍,若是沒有縮進,程序將變得沒法閱讀
            * 類聲明不縮進
            * 類中方法相對該類縮進一個層級
            * 方法實現相對於方法聲明縮進一個層級
            * 代碼塊的實現相對於其容器代碼塊縮進一個層級
            * 擴展和縮進範圍,避免範圍層級坍塌到一行
        - 團隊規則:
            * 同一個團隊,同一種代碼風格
            * 把規則編寫進IDE,一直沿用

第六章——對象和數據結構

1.數據抽象:數據抽象則是指對數據類型的定義和使用過程
    - 以抽象形態表述數據,而不是暴露數據的細節,具象機動車和抽象機動車的例子,若是機動車改爲以自然氣爲燃料,具象機動車接口須要作修改,而抽象機動車類不須要修改
public interface Vehicle{
              double getFuelTankCapacityInGallons();
              double getGallonsofGasoline();
            }
public interface Vehicle{
              double getPecentFuelRemaining();
            }
- 暴露抽象接口可使依賴方無須瞭解數據具體實現就能操做數據本體。
    - 若是上述的機動車改爲以電力爲能源,或者變成混動模式,抽象機動車類則也要作相應的修改?
    - 不必定都須要暴露抽象的概念,如pecent,若是就是想單純的取某個屬性,如長方體的長、寬、高,人的身高、體重、生日這些具體的屬性
2.數據、對象的反對稱性
    - 對象與數據結構之間的差別
        * 對象把數據隱藏於抽象以後,暴露出操做數據的函數
        * 數據結構暴露其具體數據,沒有提供有意義的函數。
    - 過程式代碼(使用數據結構的代碼)便於在不改動既有數據結構的前提下添加新函數,面向對象代碼便於在不改動既有函數的前提下添加新類。
    - 過程式代碼難以添加新數據結構,由於必須修改全部函數。面向對象代碼難以添加新函數,由於必須修改全部類。
3.得墨忒耳定律
    - 對象O的M方法,能夠訪問/調用以下的:
        * 對象O自己
        * M方法中建立或實例化的任意對象
        * 做爲參數傳遞給M的對象
        * 對象O直接的組件對象
    - 一些比喻
        * 不要和陌生人說話
        * 在超市購完物付款時,是將錢包中的錢取出交給收銀員,而不是直接把錢包交個收銀員讓收銀員把錢拿出來
        * 人能夠命令一條狗行走(walk),可是不該該直接指揮狗的腿行走,應該由狗去指揮控制它的腿如何行走
    - 爲何要遵循這必定律
        * 能夠更改一個類,而無需更改許多其它的類
        * 隱藏了某個類是如何工做的
        * 改變調用方法,而無需改變其餘的東西
        * 讓代碼更少的耦合,主叫方法只耦合一個對象,而並不是全部的內部依賴
        * 更好的模擬現實世界,想象超市付款和命令狗行走的比喻
    - 違反得墨忒耳定律的例子
        * 鏈式調用——火車失事代碼,這類連串的調用一般被認爲是骯髒的風格,應該避免
        ```java
        final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
        ```

        但有時消除這些"."可能並不太合適
        ```java
            getStudent().getParents().getBirthdays();
        ```
        改爲下面這樣可能並不太好,並無太大意義

        ```java
        getStudentParentsBirthdays()
        ```

        Java8的stream操做也不難理解,更簡潔?
activityService.getPermittedActivitysByUserId(userId).stream()
                .map(assistanceActivityWrapper::wrap).collect(Collectors.toList());
4.混雜:
    - 一半是對象,一半是數據結構,這是一種糟糕的設計。它增長了添加新函數的難度,也增長了添加新數據結構的難度。
    - 隱藏結構:
        * 既然取得路徑的目的是爲了建立文件,那麼不妨讓 ctxt 對象來作這件事:
        ```java
            BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);
        ```
    - 數據傳送對象
        * 最爲精煉的數據結構,是一個只有公共變量、沒有函數的類
        * 這種數據結構有時被稱爲 DTO,Data Transfer Objects,數據傳送對象
        * 最多見的是Bean結構,其擁有賦值器和取值器操做的私有變量。
public class CommodityInfo {

                    private String title; //商品詳情,產品參數,常見問題
                    private List<String> imageUrls;

                    public String getTitle() {
                        return title;
                    }

                    public void setTitle(String title) {
                        this.title = title;
                    }

                    public List<String> getImageUrls() {
                        return imageUrls;
                    }

                    public void setImageUrls(List<String> imageUrls) {
                        this.imageUrls = imageUrls;
                    }
                }
* Active Record:
            ** 是一種特殊的DTO形式,它擁有公共變量,一般也有save和find相似的可瀏覽的方法
            ** 但有些程序員在這類對象中塞入了業務規則方法,形成對象和數據結構的混合體
5.小結:
    - 對象暴露行爲,隱藏數據,因此便於添加新對象難於添加新行爲
    - 數據結構暴露數據,沒有明顯行爲,便於添加新行爲而難以添加新數據結構

五六章思考題

1.Lombok的反作用:
    - @Data註解 編譯時自動添加Setter、Getter、toString()、equals()和hashCode()方法,除了Setter、Getter外,其它方法不太能用到
    - 使用@Data、@Getter、@Setter後get和set方法不顯示了,不容易發現問題,isOpen,生成的get和set方法分別是isOpen()和getOpen,但有些框架如jackson默認的set方法是getIsOpen(),會報錯
    - @SneakyThrows 隱藏異常 將檢查異常包裝爲運行時異常,對API調用者不友好,調用者不能顯示的獲知須要處理檢查異常
3.帶業務邏輯的 Getter/Setter 是否是好的?
    - 很差,Getter/Setter單純的取和設置某個屬性的值就好,不要摻雜業務邏輯

第七章——錯誤處理

1.錯誤處理是必須的,但若是對錯誤的處理搞亂了原來代碼的邏輯,就是處理不當或者是錯誤的作法。
2.使用錯誤異常而非返回碼:
    - 拋異常,調用者能夠選擇處理即捕獲異常,也能夠選擇不處理即繼續向上層拋出異常
    - 使用返回碼,調用者須要知道每一個返回碼錶明的含義,寫一堆if-else邏輯,代碼不夠整潔
    - 使用異常可使錯誤處理從主邏輯中分離出來,使主邏輯清晰
3.先寫Try-Catch-Finally語句
    - try代碼塊像是事務,不管try代碼塊中發生了什麼,catch代碼塊將程序維持在一種持續狀態
    - 在編寫可能拋出異常的代碼時,最好先寫try-catch-finally語句,能幫你定義代碼的用戶應該期待什麼,不管try代碼塊中執行的代碼出什麼錯都同樣。
4.使用不可控異常(unchecked exception):
    - 可控異常(checked exception)就是指在方法簽名中標註異常。
    - 可控異常的代價是違反開放閉合原則,你對較底層的代碼修改時,可能會波及不少上層代碼。
    - 開放封閉原則Marting定義:
        - 「對於擴展是開放的」,這意味着模塊的行爲是能夠擴展的,當應用程序的需求改變時,咱們能夠對其模塊進行擴展,使其具備知足那些需求變動的新行爲。換句話說,不能夠改變模塊的功能。
        - 加入抽象類,底層便於擴展,高層代碼能夠保持不變。客戶端依賴抽象類基類,所以提供任何一個具體子類給客戶端都不會違背開放封閉原則
        - 接口繼承要好於實現繼承。接口繼承有強大的自適應能力。基於實現繼承的,給繼承頂部節點添加新成員的改動會影響到該層級結構下的全部成員,而接口要比類靈活的多。
        - 依賴接口的最大優點是接口變化的可能性要比實現小不少,若是接口發生變化,客戶端也必需要作相應的改動。
        - 需求迭代帶來的接口變化有時候是不免的
5.給出異常發生的環境說明:
    - 拋出的每一個異常,都應提供足夠的環境說明,以便判斷錯誤的來源
    - 利用日誌系統,傳遞足夠多的信息給catch塊,並記錄下來
    - 利於線上問題的排查
6.依調用者須要定義異常類:
    - 在應用程序中定義異常類時,最重要的要考慮這些異常是如何被捕獲的
    - 對重複式處理方法的多種異常應進一步打包後拋出,而不是多個相同的catch?
    - 對異常的處理如有固定流程,也應該打包?
7.避免NPE,從函數不返回null、不向函數傳遞null參開始

第八章——邊界

1.接口調用者和接口提供者之間存在張力,第三方程序包和框架提供者追求普適性,而使用者追求定製化以知足特定需求
2.學習性測試的好處不僅是免費
    - 學習性測試時一種精確試驗,幫助咱們增進對API的理解
        - 騰訊廣點通API的沙盒工具
        - 頭條巨量引擎的廣告預覽工具
    - 學習性測試不光免費,當第三方程序包發佈了新版本,能夠運行學習性測試,看看程序包的行爲有沒有改變,可能幫助咱們確保第三方程序包按照咱們想要的方式工做
3.學習使用log4j
4.使用尚不存在的代碼:
    - mock咱們想要獲得的接口和數據,好處之一是有助於保持客戶代碼集中於它該完成的工做,壞處多是對接有問題
    - 約定好API的邏輯,mock數據,並行開發,邏輯約定的越好,對接越無縫
5.整潔的邊界:
    - 邊界上的改動,有良好的軟件設計,無需巨大投入和重寫便可進行修改
    - 邊界上的代碼須要清晰的分割和定義指望的測試。依靠你能控制的東西,好過依靠你控制不了的東西,省得往後受它控制
    - 可使用適配器模式將咱們的接口轉換爲第三方提供的接口

第九章——單元測試

1.保持測試整潔:
    - 測試代碼和生產代碼同樣重要
2.測試帶來的好處:
    - 單元測試讓代碼可擴展、可維護、可複用
    - 測試可讓你毫無顧慮的改進架構和設計
    - 測試有利於防止生產代碼腐敗
3.測試代碼越髒,生產代碼就會越髒
4.整潔的測試:
    - 可讀性、可讀性、可讀性
    - 構造-操做-檢驗(BUILD_OPERATE-CHECK)
        - 第一個環節構造測試數據
        - 第二個環節操做測試數據
        - 第三個部分檢驗操做是否獲得指望的結果
    - 面向特定領域的測試語言:
        - 測試API並不是起初就被設計出來,而是由測試代碼進行後續重構逐漸演進而來
        - 開發者須要將測試代碼重構爲更簡潔而具備表達力的形式
    - 雙重標準
        - 與CPU效率或內存有關的問題,在生產環境不能夠作,可是在測試環境中徹底沒有問題
        - StringBuffer醜陋?
5.每一個測試一個概念:
    - 一個測試函數只測試一個概念
6.F.I.R.S.T
    - 測試代碼要夠快,要頻繁測試,以更早、更快的發現生產代碼的bug
    - 每一個測試都應該相互獨立,某個測試不該該爲下一個測試設定條件。應該能夠單獨運行每一個測試,以及以任何順序運行測試
    - 測試代碼應該不挑環境,不管是在測試環境、生產環境仍是沒有網絡的本地,在什麼時候何地都應該可重複的運行
    - 測試應該有布爾值輸出,測試的結果不該該從日誌或者對比文本等不直觀的方式被展示出來
    - 測試應該被及時編寫,最好應該在編寫生產代碼以前編寫測試代碼

第十章——類

1.類的組織:
    - 類應該從變量列表開始:
        - 公共靜態變量應該先出現,接着是私有實體變量、不多有公共變量
        - 公共函數在變量列表以後,公共函數調用的私有工具函數跟在公共函數後面,符合自頂向下原則
    - 封裝:
        - 保持變量和工具函數的私有性但並不執着於此
        - 放鬆封裝老是下策
2.類應該短小
    - 用權責來衡量類的大小
    - 單一權責原則(SRP):類或者模塊應該有且僅有一個加以修改的理由
    - 內聚:
        - 類中的每一個方法都應該操做一個或多個實體變量
        - 內聚性高意味着類中的方法和變量相互依賴、互相結合成一個邏輯總體
3.爲了修改而組織:
    - 開放-閉合原則(OCP):對擴展開放,對修改封閉
    - 隔離修改:藉助接口和抽象類來隔離這些細節帶來的影響

善用工具

  • 規範化本身的代碼不能只靠意志,更要靠工具,少點我的風格,多點通用規矩,並學會使用CheckStyle工具。
  • 很難命名的變量、函數,找小夥伴商量下,別本身一我的憋着
  • 寧肯變量名長,也不要讓變量名短得讓人沒法揣測其含義
  • 避免重複(DRY),儘量杜絕重複代碼,凡是須要用至少兩次的代碼,給它單獨寫成一個類或函數
相關文章
相關標籤/搜索