代碼整潔之道

《代碼整潔之道》

代碼猴子與童子軍軍規

  1. 我們就像一羣代碼猴子,上躥下跳,自以爲領略了編程的真諦。可惜,當我們抓着幾個酸桃子,得意洋洋坐到樹枝上,卻對自己造成的混亂熟視無睹。那堆“可以運行”的亂麻程序,就在我們的眼皮底下慢慢腐壞。

第一章 整潔代碼

  1. 勒布朗法則:稍後等於永不(Later equals never)。
  2. 製造混亂無助於趕上期限。混亂只會立刻拖慢你,叫你錯過期限。趕上期限的唯一方法——做的快的唯一方法——就是始終儘可能保持代碼整潔。
  3. Javadoc 中的 @author 字段告訴我們自己是什麼人。我們是作者,作者都有讀者。實際上,作者有責任與讀者做良好溝通。下次你寫代碼的時候,記得自己是作者,要爲評判你工作的讀者寫代碼。

第三章 函數

  1. 函數的第一規則是要短小。第二條規則是還要更短小…經過漫長的試錯,經驗告訴我,函數就該小。
  2. 函數應該做一件事。做好這件事。只做這一件事。判斷函數是否不止做了一件事,有一個方法,就是看是否能再拆出一個函數,該函數不僅只是單純地重新詮釋其實現。
  3. 要確保函數只做一件事,函數中的語句都要在同一抽象層級上。函數中混雜不同的抽象層級,往往讓人迷惑。讀者可能無法判斷某個表達式是基礎概念還是細節。更惡劣的是,就像破損的窗戶,一旦細節與基礎概念混雜,更多的細節就會在函數中糾結起來。
  4. 寫出短小的 switch 語句往往很難。寫出只做一件事的 switch 語句也很難。我們總不發避開 switch 語句,不過還是能夠確保 switch 都埋藏在較低的抽象層級,而且永遠不重複。
  5. 最理想的參數數量是零,其次是一,再次是二…從測試的角度看,參數甚至更叫人爲難。想想看,要編寫能確保參數的各種組合運行正常的測試用例,是多麼困難的事。如果沒有參數,就是小菜一碟。
  6. 函數承諾只做一件事,但還是會做其它被藏起來的事。有時,它會對自己類中的變量做出未能預期的改動,導致古怪的時序性耦合及順序依賴。
public class UserValidator {
    private Cryptographer cryptographer;
    public boolean checkPassword(String userName, String password) {
        User user = UserGateway.findByName(userName);
        if (user != null) {
            String codePhrase = user.getPhraseEncodeByPassword();
            String phrase = cryptographer.decrypt(codePhrase, password);
            if ("Valid Password".equals(phrase)) {
                Session.initialize();
                return true;
            }
        }
        return false;
    }
}

副作用就在於對 Session.initialize() 的調用。checkPassword 函數是用來檢查密碼的。該名稱未暗示它會初始化該次會話。當某個誤信了函數名的調用者想要檢查用戶有效性時,就得冒着抹除現有會話數據的風險。這一副作用造成了一次時序性耦合。也就是說,checkPassword 只能在特定時刻調用。

第四章 註釋

  1. 註釋的恰當用法是彌補我們在用代碼表達意圖時遭遇的失敗。註釋總是一種失敗。我們總無法找到不用註釋就能表達自我的方法,所以總要有註釋,這並不值得慶賀。
  2. 如果你發現自己需要寫註釋,再想想看是否有辦法翻盤,用代碼來表達。
  3. 有時,有理由用 // TODO 形式在源代碼中放置要做的工作列表。TODO 是一種程序員認爲應該做,但由於某些原因目前還沒做的工作。
  4. 沒有什麼比被良好描述的公共 API 更有用和令人滿意的了。如果你在編寫公共 API,就該爲它編寫良好的 Javadoc。
  5. 刪掉無用而多餘的 Javadoc 吧,這些註釋只是一味將代碼搞得含糊不明,完全沒有文檔上的價值。
  6. 所謂每個函數都要有 Javadoc 或每個變量都要有註釋的規矩全然是愚蠢可笑的。這類註釋徒然讓代碼變得散亂,滿口胡言,令人迷惑不解。
  7. 20 世紀 60 年代,曾經有那麼一段時間,註釋掉的代碼可能有用。但我們已經擁有優良的源代碼控制系統如此之久,這些系統可以爲我們記住不要的代碼。我們無需再用註釋來標記,刪掉即可,它們丟不了,我擔保。

第五章 格式

  1. 代碼格式很重要,必須嚴肅對待。代碼格式關乎溝通,而溝通是專業開發者的頭等大事。
  2. 你今天編寫的功能,極有可能在下一版本中被修改,但代碼的可讀性卻會對以後可能發生的修改行爲產生深遠影響。原始代碼修改之後很久,其代碼風格和可讀性仍會影響到可維護性和擴展性。即便代碼不復存在,你的風格和律條仍會存活下來。
  3. 若某個函數調用了另外一個,就應該把它們放在一起,而且調用者應該儘可能放在被調用者上面。

第六章 對象和數據結構

  1. 最爲精煉的數據結構,是一個只有公共變量、沒有函數的類。這種數據結構有時被稱爲數據傳送對象,或 DTO(Data Transfer Objects)。DTO 是非常有用的結構,尤其是在於數據庫通信、或解析套接字傳輸的消息之類的場景中。

第七章 使用異常而非返回碼

  1. 在很久以前,許多語言都不支持異常。這些語言處理和彙報錯誤的手段都有限。你要麼設置一個錯誤標識,要麼返回給調用者檢查的錯誤碼。這類手段的問題在於,它們搞亂了調用者代碼。調用者必須在調用之後即可檢查錯誤。不幸的是,這個步驟很容易被遺忘。最好是拋出一個異常,這樣其邏輯不會被錯誤處理搞亂。
  2. 使用不可控異常。可控異常 checked exception 的代價是違反開閉原則。如果你在方法中拋出可控異常,而 catch 語句在三個層級之上,你就得在 catch 語句和拋出異常處之間的每個方法簽名中聲明該異常。這意味着對軟件中低層級的修改,都將涉及較高層級的簽名。最終得到的就是一個從軟件最底端貫穿到最高端的修改鏈。
  3. 別返回 null 值。我不想去計算曾經見過多少每行代碼都在檢查 null 值的應用程序。Java 中有 Colletions.emptyList() 方法,該方法返回一個預定義不可變列表,這樣編碼,就能儘量避免 NullPointerException 的出現,代碼也就更整潔了。
  4. 別傳遞 null 值。在大多數編程語言中,沒有良好的方法能對付由調用者意外傳入 null 值。事已如此,恰當的做法就是禁止傳入 null 值。

第八章 邊界

  1. 第三方代碼幫助我們在更少時間內發佈更豐富的功能。在利用第三方程序包時,該從何處入手呢?我們沒有測試第三方代碼的職責,但爲要使用的第三方代碼編寫測試,可能最符合我們的利益。
  2. 學習第三方代碼很難,整合第三方代碼也很難,同時做這兩件事難上加難。不要在生產代碼中試驗新東西,而是編寫測試來遍覽和理解第三方代碼,這叫“學習性測試”。

第九章 單元測試

  1. TDD 三定律:
    • 定律一 在編寫不能通過的單元測試前,不可編寫生產代碼。
    • 定律二 只可編寫剛好無法通過的單元測試,不能編譯也算不通過。
    • 定律三 只可編寫剛好足以通過當前失敗測試的生產代碼。
  2. TDD 三定律其實說的是,先寫失敗的 Case,寫完之後纔開始寫功能 Code,只要 Code 通過了 Case,就不要再寫功能代碼了。也就是說,寫完一個測試,就要寫對應的生產代碼。
  3. 測試代碼和生產代碼一樣重要。它可不是二等公民。它需要被思考、被設計和被照料。它該像生產代碼一般保持整潔。
  4. 如果測試不能保持整潔,你就會失去它們。沒有了測試,你就會失去保證生產代碼可擴展的一切要素。有了測試,你就不擔心對代碼的修改!沒有測試,每次修改都可能帶來缺陷。
  5. 覆蓋了生產代碼的自動化單元測試程序組能儘可能地保持設計和架構的整潔。測試帶來了一切好處,因爲測試使改動變得可能。
  6. 整潔的測試有三個要素:可讀性、可讀性、可讀性。測試應該明確、簡潔,還有足夠的表達力。在測試中,要以儘量少的文字表達大量的內容。
  7. F.I.R.S.T 規則:
    • Fast(快速) 測試應該能快速運行。測試運行緩慢,你就不會想要頻繁地運行它。如果你不頻繁運行測試,就不能儘早發現問題,也無法輕易修正。
    • Independent(獨立) 測試應該相互獨立。某個測試不應爲下一個測試設定條件。你應該可以單獨運行每個測試,及以任何順序運行測試。
    • Repeatable(可重複) 測試應當可在任何環境中重複通過。
    • Self-Validating (自足驗證) 測試應該有布爾值輸出。
    • Timely(及時) 測試應及時編寫。單元測試應該恰好在使其通過的生產代碼之前編寫。

第十章 類

  1. 面向對象的其中一個設計原則是“開放——閉合原則”,即類應當對擴展開放,對修改封閉。我們希望將系統打造成在添加或修改特性時儘可能少惹麻煩的架子。在理想系統中,我們通過擴展系統而不是修改現有代碼來添加新特性。
  2. 類的另一條設計原則是“依賴倒置原則”(Dependency Inversion Principle, DIP),DIP 認爲類應該依賴於抽象而不是依賴於具體細節。

第十一章 系統

  1. 有一種強大的機制可以實現分離構造與使用,那就是依賴注入(Dependency Injection, DI),它是控制反轉(Inversion of Control, IoC)在依賴管理中的一種應用手段。控制反轉將第二權責從對象中拿出來,轉移到另一個專注於此的對象中,從而遵循了單一權責原則。在依賴管理情境中,對象不應負責實體化對自身的依賴,而應當將這份權責移交給其它“有權力”的機制,從而實現控制的反轉。
  2. “一開始就做對系統”純屬神話。反之,我們應該只去實現今天的用戶故事,然後重構,明天再擴展系統、實現新的用戶故事。這就是迭代和增量敏捷的精髓所在。

第十二章 迭進

  1. 簡單設計的四條規則,按重要程度排序:
    • 運行所有測試;
    • 不可重複;
    • 表達了程序員的意圖;
    • 儘可能減少類和方法的數量。
  2. 全面測試並持續通過所有測試的系統,就是可測試的系統。看似淺顯,但卻重要。不可測試的系統同樣不可驗證。不可驗證的系統,絕不應該部署。
  3. 重複是擁有良好設計系統的大敵,它代表着額外的工作、額外的風險和額外且不必要的複雜度。要想創建整潔的系統,需要有消除重複的意願。
  4. 軟件項目的主要成本在於長期維護。代碼應當清晰地表達其作者的意圖。作者把代碼寫得越清晰,其他人花在理解代碼上的時間也就越少,從而減少缺陷,縮減維護成本。
  5. 爲了保持類和函數短小,我們可能會造出太多的細小類和方法。所以這條規則也主張函數和類的數量要少。我們的目標是在保持函數和類短小的同時,保持整個系統短小精悍。不過更重要的是測試、消除重複和表達力。

第十三章 併發編程

  1. 併發是一種解耦策略。它幫助我們把做什麼(目的)和何時做(時機)分解開。解耦目的與時機能明顯地改進應用程序的吞吐量和結構。
  2. 併發有時能改進性能,但只在多個線程或處理器之間能分享大量等待時間的時候管用,事情沒那麼簡單。
  3. 併發算法的設計有可能與單線程系統的設計極不相同。目的與時機的解耦往往對系統結構產生巨大影響。
  4. 併發編程中的一些基礎定義:
    • 限定資源:併發環境中有着固定尺寸或數量的資源。
    • 互斥:每一時刻僅有一個線程能訪問共享數據或共享資源。
    • 線程飢餓:一個或一組線程在很長時間內或永久被禁止。例如,總是讓執行得快的線程先運行,加入執行得快得線程沒完沒了,則執行時間長的線程就會“飢餓”。
    • 死鎖:兩個或多個線程互相等待執行結束。每個線程都擁有其它線程需要的資源,得不到其它線程擁有的資源,就無法終止。
    • 活鎖:執行次序一致的線程,每個都想要起步,但發現其它線程已經“在路上”。由於競步的原因,線程會持續嘗試起步,但在很長時間內卻無法如願,甚至永遠無法啓動。

第十四章 逐步改進

  1. 代碼能工作還不夠,能工作的代碼經常會嚴重崩潰。滿足於僅僅讓代碼工作的程序員不夠專業。他們會害怕沒時間改進代碼的結構和設計,我不敢苟同。沒什麼比糟糕的代碼給開發項目帶來更深遠和長期的損害了。
  2. 進度可以重訂,需求可以重新定義,團隊動態可以修正。糟糕的代碼只會一直腐敗發酵,無情地拖着團隊的後腿。
  3. 保持代碼持續整潔和簡單,永不讓腐壞有機會開始。

第十五章 JUnit 框架

  1. 成員變量的前綴可以刪除。在現今的運行環境中,這類範圍性編碼純屬多餘。
  2. 條件判斷應當封裝起來,從而更清晰地表達代碼的意圖。可以拆解處一個方法,解釋這個條件判斷。
public String compact(String message) {
    if (expected == null || actual == null || areStringsEqual()) {
        return Assert.format(message, expected, actual);
    }
}
// 拆解後...
public String compact(String message) {
    if (shouldNotCompact()) {
        return Assert.format(message, expected, actual);
    }
}
private boolean shouldNotCompact() {
    return expected == null || actual == null || areStringsEqual();
}

第十七章 味道與啓發

  1. 讓註釋傳達本該更好地在源代碼控制系統、問題追蹤系統或任何其它記錄系統中保存的信息,是不恰當的。
  2. 除函數簽名之外什麼也沒說的 Javadoc,也是多餘的。
  3. 看到註釋掉的代碼,就刪除它!別擔心,源代碼控制系統還會記得它。
  4. 每次看到重複代碼,都代表遺漏了抽象。將重複代碼疊放進類似的抽象,增加了你的設計語言的詞彙量。其它程序員可以用到你創建的抽象設施。編碼變得越來越快,錯誤越來越少,因爲你提升了抽象層級。
  5. 死代碼就是不執行的代碼,可以在檢查不會發生的條件的 if 語句中找到,可以在從不拋出異常的 try/catch 塊中找到,可以在從不調用的小工具方法中找到,也可以在不會發生 switch/case 條件中找到。如果你找到死代碼,就體面地埋葬它,將它從系統中刪除掉。
  6. 特性依戀是 Martin Fowler 提出的代碼味道之一。類的方法只應對其所屬類中的變量和函數感興趣,不該垂青其它類中的變量和函數。我們要消除特性依戀。
  7. 用多態替代 if/else 或 switch/case。對於給定的選擇類型,不應有多於一個 switch 語句。在那個 switch 語句中的多個 case,必須創建多態對象,取代系統中其它類似 switch 語句。
  8. 用命名常量替代魔術數。
  9. 現在 enum 已經加入 java 語言了,放心用吧!別再用那個 public static final int 老花招。那樣做 int 的意義就喪失了,而用 enum 則不然,因爲它們隸屬
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章