設計模式之美 - 38 | 總結回顧面向對象、設計原則、編程規範、重構技巧等知識點

這系列相關博客,參考 設計模式之美

設計模式之美 - 38 | 總結回顧面向對象、設計原則、編程規範、重構技巧等知識點

到今天爲止,設計原則和思想已經全部講完了,其中包括:面向對象、設計原則、規範與重構三個模塊的內容。除此之外,我們還學習了貫穿整個專欄的代碼質量評判標準。專欄的進度已經接近一半,馬上就要進入設計模式內容的學習了。在此之前,我先帶你一塊來總結回顧一下,我們已經學過的所有知識點。

今天的內容比較多,有一萬多字,但都是之前學過的,算是一個總結回顧,主要是想帶你複習一下,溫故而知新。如果你看了之後,感覺都有印象,那就說明學得還不錯;如果還能在腦子裏形成自己的知識架構,閉上眼睛都能回憶上來,那說明你學得很好;如果能有自己的理解,並且在項目開發中,開始思考代碼質量問題,開始用已經學過的設計原則和思想來審視代碼,那說明你已經掌握這些內容的精髓。
在這裏插入圖片描述

一、代碼質量評判標準

如何評價代碼質量的高低?

代碼質量的評價有很強的主觀性,描述代碼質量的詞彙也有很多,比如可讀性、可維護性、靈活、優雅、簡潔。這些詞彙是從不同的維度去評價代碼質量的。它們之間有互相作用,並不是獨立的,比如,代碼的可讀性好、可擴展性好就意味着代碼的可維護性好。代碼質量高低是一個綜合各種因素得到的結論。我們並不能通過單一維度去評價一段代碼的好壞。

最常用的評價標準有哪幾個?

最常用到幾個評判代碼質量的標準有:可維護性、可讀性、可擴展性、靈活性、簡潔性、可複用性、可測試性。其中,可維護性、可讀性、可擴展性又是提到最多的、最重要的三個評價標準。

如何才能寫出高質量的代碼?

要寫出高質量代碼,我們就需要掌握一些更加細化、更加能落地的編程方法論,這就包含面向對象設計思想、設計原則、設計模式、編碼規範、重構技巧等。
在這裏插入圖片描述

二、面向對象

1. 面向對象概述

現在,主流的編程範式或者編程風格有三種,它們分別是面向過程、面向對象和函數式編程。面向對象這種編程風格又是這其中最主流的。現在比較流行的編程語言大部分都是面向對象編程語言。大部分項目也都是基於面向對象編程風格開發的。面向對象編程因爲其具有豐富的特性(封裝、抽象、繼承、多態),可以實現很多複雜的設計思路,是很多設計原則、設計模式編碼實現的基礎。

2. 面向對象四大特性

封裝也叫作信息隱藏或者數據訪問保護。類通過暴露有限的訪問接口,授權外部僅能通過類提供的方法來訪問內部信息或者數據。它需要編程語言提供權限訪問控制語法來支持,例如Java 中的 private、protected、public 關鍵字。封裝特性存在的意義,一方面是保護數據不被隨意修改,提高代碼的可維護性;另一方面是僅暴露有限的必要接口,提高類的易用性。

如果說封裝主要講如何隱藏信息、保護數據,那抽象就是講如何隱藏方法的具體實現,讓使用者只需要關心方法提供了哪些功能,不需要知道這些功能是如何實現的。抽象可以通過接口類或者抽象類來實現。抽象存在的意義,一方面是修改實現不需要改變定義;另一方面,它也是處理複雜系統的有效手段,能有效地過濾掉不必要關注的信息。

繼承用來表示類之間的 is-a 關係,分爲兩種模式:單繼承和多繼承。單繼承表示一個子類只繼承一個父類,多繼承表示一個子類可以繼承多個父類。爲了實現繼承這個特性,編程語言需要提供特殊的語法機制來支持。繼承主要是用來解決代碼複用的問題。

多態是指子類可以替換父類,在實際的代碼運行過程中,調用子類的方法實現。多態這種特性也需要編程語言提供特殊的語法機制來實現,比如繼承、接口類、duck-typing。多態可以提高代碼的擴展性和複用性,是很多設計模式、設計原則、編程技巧的代碼實現基礎。

3. 面向對象 VS 面向過程

面向對象編程相比面向過程編程的優勢主要有三個。

  • 對於大規模複雜程序的開發,程序的處理流程並非單一的一條主線,而是錯綜複雜的網狀結構。面向對象編程比起面向過程編程,更能應對這種複雜類型的程序開發。

  • 面向對象編程相比面向過程編程,具有更加豐富的特性(封裝、抽象、繼承、多態)。利用這些特性編寫出來的代碼,更加易擴展、易複用、易維護。

  • 從編程語言跟機器打交道方式的演進規律中,我們可以總結出:面向對象編程語言比起面向過程編程語言,更加人性化、更加高級、更加智能。

面向對象編程一般使用面向對象編程語言來進行,但是,不用面向對象編程語言,我們照樣可以進行面向對象編程。反過來講,即便我們使用面向對象編程語言,寫出來的代碼也不一定是面向對象編程風格的,也有可能是面向過程編程風格的。

面向對象和麪向過程兩種編程風格並不是非黑即白、完全對立的。在用面向對象編程語言開發的軟件中,面向過程風格的代碼並不少見,甚至在一些標準的開發庫(比如 JDK、Apache Commons、Google Guava)中,也有很多面向過程風格的代碼。

不管使用面向過程還是面向對象哪種風格來寫代碼,我們最終的目的還是寫出易維護、易讀、易複用、易擴展的高質量代碼。只要我們能避免面向過程編程風格的一些弊端,控制好它的副作用,在掌控範圍內爲我們所用,我們就大可不用避諱在面向對象編程中寫面向過程風格的代碼。

4. 面向對象分析、設計與編程

面向對象分析(OOA)、面向對象設計(OOD)、面向對象編程(OOP),是面向對象開發的三個主要環節。簡單點講,面向對象分析就是要搞清楚做什麼,面向對象設計就是要搞清楚怎麼做,面向對象編程就是將分析和設計的的結果翻譯成代碼的過程。

需求分析的過程實際上是一個不斷迭代優化的過程。我們不要試圖一下就給出一個完美的解決方案,而是先給出一個粗糙的、基礎的方案,有一個迭代的基礎,然後再慢慢優化。這樣一個思考過程能讓我們擺脫無從下手的窘境。

面向對象設計和實現要做的事情就是把合適的代碼放到合適的類中。至於到底選擇哪種劃分方法,判定的標準是讓代碼儘量地滿足“鬆耦合、高內聚”、單一職責、對擴展開放對修改關閉等我們之前講到的各種設計原則和思想,儘量地做到代碼可複用、易讀、易擴展、易維護。

面向對象分析的產出是詳細的需求描述。面向對象設計的產出是類。在面向對象設計這一環節中,我們將需求描述轉化爲具體的類的設計。這個環節的工作可以拆分爲下面四個部分。

  • 劃分職責進而識別出有哪些類

根據需求描述,我們把其中涉及的功能點,一個一個羅列出來,然後再去看哪些功能點職責相近,操作同樣的屬性,可否歸爲同一個類。

  • 定義類及其屬性和方法

我們識別出需求描述中的動詞,作爲候選的方法,再進一步過濾篩選出真正的方法,把功能點中涉及的名詞,作爲候選屬性,然後同樣再進行過濾篩選。

  • 定義類與類之間的交互關係

UML 統一建模語言中定義了六種類之間的關係。它們分別是:泛化、實現、關聯、聚合、組合、依賴。我們從更加貼近編程的角度,對類與類之間的關係做了調整,保留了四個關係:泛化、實現、組合、依賴。

  • 將類組裝起來並提供執行入口

我們要將所有的類組裝在一起,提供一個執行入口。這個入口可能是一個 main() 函數,也可能是一組給外部用的 API 接口。通過這個入口,我們能觸發整個代碼跑起來。

5. 接口 VS 抽象類

抽象類不允許被實例化,只能被繼承。它可以包含屬性和方法。方法既可以包含代碼實現,也可以不包含代碼實現。不包含代碼實現的方法叫作抽象方法。子類繼承抽象類,必須實現抽象類中的所有抽象方法。接口不能包含屬性(Java 可以定義靜態常量),只能聲明方法,方法不能包含代碼實現(Java8 以後可以有默認實現)。類實現接口的時候,必須實現接口中聲明的所有方法。

抽象類是對成員變量和方法的抽象,是一種 is-a 關係,是爲了解決代碼複用問題。接口僅僅是對方法的抽象,是一種 has-a 關係,表示具有某一組行爲特性,是爲了解決解耦問題,隔離接口和具體的實現,提高代碼的擴展性。

什麼時候該用抽象類?什麼時候該用接口?實際上,判斷的標準很簡單。如果要表示一種 is-a 的關係,並且是爲了解決代碼複用問題,我們就用抽象類;如果要表示一種 has-a 關係,並且是爲了解決抽象而非代碼複用問題,那我們就用接口。

6. 基於接口而非實現編程

應用這條原則,可以將接口和實現相分離,封裝不穩定的實現,暴露穩定的接口。上游系統面向接口而非實現編程,不依賴不穩定的實現細節,這樣當實現發生變化的時候,上游系統的代碼基本上不需要做改動,以此來降低耦合性,提高擴展性。

實際上,“基於接口而非實現編程”這條原則的另一個表述方式是,“基於抽象而非實現編程”。後者的表述方式其實更能體現這條原則的設計初衷。在軟件開發中,最大的挑戰之一就是需求的不斷變化,這也是考驗代碼設計好壞的一個標準。

越抽象、越頂層、越脫離具體某一實現的設計,越能提高代碼的靈活性,越能應對未來的需求變化。好的代碼設計,不僅能應對當下的需求,而且在將來需求發生變化的時候,仍然能夠在不破壞原有代碼設計的情況下靈活應對。而抽象就是提高代碼擴展性、靈活性、可維護性最有效的手段之一。

7. 多用組合少用繼承

爲什麼不推薦使用繼承?

繼承是面向對象的四大特性之一,用來表示類之間的 is-a 關係,可以解決代碼複用的問題。雖然繼承有諸多作用,但繼承層次過深、過複雜,也會影響到代碼的可維護性。在這種情況下,我們應該儘量少用,甚至不用繼承。

組合相比繼承有哪些優勢?

繼承主要有三個作用:表示 is-a 關係、支持多態特性、代碼複用。而這三個作用都可以通過組合、接口、委託三個技術手段來達成。除此之外,利用組合還能解決層次過深、過複雜的繼承關係影響代碼可維護性的問題。

如何判斷該用組合還是繼承?

儘管我們鼓勵多用組合少用繼承,但組合也並不是完美的,繼承也並非一無是處。在實際的項目開發中,我們還是要根據具體的情況,來選擇該用繼承還是組合。如果類之間的繼承結構穩定,層次比較淺,關係不復雜,我們就可以大膽地使用繼承。反之,我們就儘量使用組合來替代繼承。除此之外,還有一些設計模式、特殊的應用場景,會固定使用繼承或者組合。

8. 貧血模型 VS 充血模型

我們平時做 Web 項目的業務開發,大部分都是基於貧血模型的 MVC 三層架構,在專欄中我把它稱爲傳統的開發模式。之所以稱之爲“傳統”,是相對於新興的基於充血模型的DDD 開發模式來說的。基於貧血模型的傳統開發模式,是典型的面向過程的編程風格。相反,基於充血模型的 DDD 開發模式,是典型的面向對象的編程風格。

不過,DDD 也並非銀彈。對於業務不復雜的系統開發來說,基於貧血模型的傳統開發模式簡單夠用,基於充血模型的 DDD 開發模式有點大材小用,無法發揮作用。相反,對於業務複雜的系統開發來說,基於充血模型的 DDD 開發模式,因爲前期需要在設計上投入更多時間和精力,來提高代碼的複用性和可維護性,所以相比基於貧血模型的開發模式,更加有優勢。

基於充血模型的 DDD 開發模式跟基於貧血模型的傳統開發模式相比,主要區別在 Service層。在基於充血模型的開發模式下,我們將部分原來在 Service 類中的業務邏輯移動到了一個充血的 Domain 領域模型中,讓 Service 類的實現依賴這個 Domain 類。不過,Service 類並不會完全移除,而是負責一些不適合放在 Domain 類中的功能。比如,負責與 Repository 層打交道、跨領域模型的業務聚合功能、冪等事務等非功能性的工作。

基於充血模型的 DDD 開發模式跟基於貧血模型的傳統開發模式相比,Controller 層和Repository 層的代碼基本上相同。這是因爲,Repository 層的 Entity 生命週期有限,Controller 層的 VO 只是單純作爲一種 DTO。兩部分的業務邏輯都不會太複雜。業務邏輯主要集中在 Service 層。所以,Repository 層和 Controller 層繼續沿用貧血模型的設計思路是沒有問題的。
在這裏插入圖片描述

三、設計原則

1.SOLID 原則:SRP 單一職責原則

一個類只負責完成一個職責或者功能。單一職責原則通過避免設計大而全的類,避免將不相關的功能耦合在一起,來提高類的內聚性。同時,類職責單一,類依賴的和被依賴的其他類也會變少,減少了代碼的耦合性,以此來實現代碼的高內聚、鬆耦合。但是,如果拆分得過細,實際上會適得其反,反倒會降低內聚性,也會影響代碼的可維護性。

不同的應用場景、不同階段的需求背景、不同的業務層面,對同一個類的職責是否單一,可能會有不同的判定結果。實際上,一些側面的判斷指標更具有指導意義和可執行性,比如,出現下面這些情況就有可能說明這類的設計不滿足單一職責原則:

  • 類中的代碼行數、函數或者屬性過多;
  • 類依賴的其他類過多或者依賴類的其他類過多;
  • 私有方法過多;
  • 比較難給類起一個合適的名字;
  • 類中大量的方法都是集中操作類中的某幾個屬性。

2.SOLID 原則:OCP 開閉原則

如何理解“對擴展開放、修改關閉”?

添加一個新的功能,應該是通過在已有代碼基礎上擴展代碼(新增模塊、類、方法、屬性等),而非修改已有代碼(修改模塊、類、方法、屬性等)的方式來完成。關於定義,我們有兩點要注意。第一點是,開閉原則並不是說完全杜絕修改,而是以最小的修改代碼的代價來完成新功能的開發。第二點是,同樣的代碼改動,在粗代碼粒度下,可能被認定爲“修改”;在細代碼粒度下,可能又被認定爲“擴展”。

如何做到“對擴展開放、修改關閉”?

我們要時刻具備擴展意識、抽象意識、封裝意識。在寫代碼的時候,我們要多花點時間思考一下,這段代碼未來可能有哪些需求變更,如何設計代碼結構,事先留好擴展點,以便在未來需求變更的時候,在不改動代碼整體結構、做到最小代碼改動的情況下,將新的代碼靈活地插入到擴展點上。

很多設計原則、設計思想、設計模式,都是以提高代碼的擴展性爲最終目的的。特別是 23種經典設計模式,大部分都是爲了解決代碼的擴展性問題而總結出來的,都是以開閉原則爲指導原則的。最常用來提高代碼擴展性的方法有:多態、依賴注入、基於接口而非實現編程,以及大部分的設計模式(比如,裝飾、策略、模板、職責鏈、狀態)。

3.SOLID 原則:LSP 裏式替換原則

子類對象(object of subtype/derived class)能夠替換程序(program)中父類對象(object of base/parent class)出現的任何地方,並且保證原來程序的邏輯行爲(behavior)不變及正確性不被破壞。

裏式替換原則是用來指導繼承關係中子類該如何設計的一個原則。理解裏式替換原則,最核心的就是理解“design by contract,按照協議來設計”這幾個字。父類定義了函數的“約定”(或者叫協議),那子類可以改變函數的內部實現邏輯,但不能改變函數的原有“約定”。這裏的“約定”包括:函數聲明要實現的功能;對輸入、輸出、異常的約定;甚至包括註釋中所羅列的任何特殊說明。

理解這個原則,我們還要弄明白,裏式替換原則跟多態的區別。雖然從定義描述和代碼實現上來看,多態和裏式替換有點類似,但它們關注的角度是不一樣的。多態是面向對象編程的一大特性,也是面向對象編程語言的一種語法。它是一種代碼實現的思路。而裏式替換是一種設計原則,用來指導繼承關係中子類該如何設計,子類的設計要保證在替換父類的時候,不改變原有程序的邏輯及不破壞原有程序的正確性。

4.SOLID 原則:ISP 接口隔離原則

接口隔離原則的描述是:客戶端不應該強迫依賴它不需要的接口。其中的“客戶端”,可以理解爲接口的調用者或者使用者。理解“接口隔離原則”的重點是理解其中的“接口”二字。這裏有三種不同的理解。

如果把“接口”理解爲一組接口集合,可以是某個微服務的接口,也可以是某個類庫的接口等。如果部分接口只被部分調用者使用,我們就需要將這部分接口隔離出來,單獨給這部分調用者使用,而不強迫其他調用者也依賴這部分不會被用到的接口。

如果把“接口”理解爲單個 API 接口或函數,部分調用者只需要函數中的部分功能,那我們就需要把函數拆分成粒度更細的多個函數,讓調用者只依賴它需要的那個細粒度函數。

如果把“接口”理解爲 OOP 中的接口,也可以理解爲面向對象編程語言中的接口語法。那接口的設計要儘量單一,不要讓接口的實現類和調用者,依賴不需要的接口函數。

單一職責原則針對的是模塊、類、接口的設計。接口隔離原則相對於單一職責原則,一方面更側重於接口的設計,另一方面它的思考的角度也是不同的。接口隔離原則提供了一種判斷接口的職責是否單一的標準:通過調用者如何使用接口來間接地判定。如果調用者只使用部分接口或接口的部分功能,那接口的設計就不夠職責單一。

5.SOLID 原則:DIP 依賴倒置原則

控制反轉:實際上,控制反轉是一個比較籠統的設計思想,並不是一種具體的實現方法,一般用來指導框架層面的設計。這裏所說的“控制”指的是對程序執行流程的控制,而“反轉”指的是在沒有使用框架之前,程序員自己控制整個程序的執行。在使用框架之後,整個程序的執行流程通過框架來控制。流程的控制權從程序員“反轉”給了框架。

依賴注入:依賴注入和控制反轉恰恰相反,它是一種具體的編碼技巧。我們不通過 new 的方式在類內部創建依賴類的對象,而是將依賴的類對象在外部創建好之後,通過構造函數、函數參數等方式傳遞(或“注入”)給類來使用。

依賴注入框架:我們通過依賴注入框架提供的擴展點,簡單配置一下所有需要的類及其類與類之間的依賴關係,就可以實現由框架來自動創建對象、管理對象的生命週期、依賴注入等原本需要程序員來做的事情。

依賴反轉原則:依賴反轉原則也叫作依賴倒置原則。這條原則跟控制反轉有點類似,主要用來指導框架層面的設計。高層模塊不依賴低層模塊,它們共同依賴同一個抽象。抽象不需要依賴具體實現細節,具體實現細節依賴抽象。

6.KISS、YAGNI 原則

KISS 原則的中文描述是:儘量保持簡單。KISS 原則是保持代碼可讀和可維護的重要手段。KISS 原則中的“簡單“”並不是以代碼行數來考量的。代碼行數越少並不代表代碼越簡單,我們還要考慮邏輯複雜度、實現難度、代碼的可讀性等。而且,本身就複雜的問題,用複雜的方法解決,也並不違背 KISS 原則。除此之外,同樣的代碼,在某個業務場景下滿足KISS 原則,換一個應用場景可能就不滿足了。

對於如何寫出滿足 KISS 原則的代碼,我總結了下面幾條指導原則:

  • 不要使用同事可能不懂的技術來實現代碼;
  • 不要重複造輪子,善於使用已經有的工具類庫;
  • 不要過度優化。

YAGNI 原則的英文全稱是:You Ain’t Gonna Need It。直譯就是:你不會需要它。這條原則也算是萬金油了。當用在軟件開發中的時候,它的意思是:不要去設計當前用不到的功能;不要去編寫當前用不到的代碼。實際上,這條原則的核心思想就是:不要做過度設計。

YAGNI 原則跟 KISS 原則並非一回事兒。KISS 原則講的是“如何做”的問題(儘量保持簡單),而 YAGNI 原則說的是“要不要做”的問題(當前不需要的就不要做)。

7.DRY 原則

DRY 原則中文描述是:不要重複自己,將它應用在編程中,可以理解爲:不要寫重複的代碼。

專欄中講到了三種代碼重複的情況:實現邏輯重複、功能語義重複、代碼執行重複。實現邏輯重複,但功能語義不重複的代碼,並不違反 DRY 原則。實現邏輯不重複,但功能語義重複的代碼,也算是違反 DRY 原則。而代碼執行重複也算是違反 DRY 原則。

除此之外,我們還講到了提高代碼複用性的一些手段,包括:減少代碼耦合、滿足單一職責原則、模塊化、業務與非業務邏輯分離、通用代碼下沉、繼承、多態、抽象、封裝、應用模板等設計模式。複用意識也非常重要。在設計每個模塊、類、函數的時候,要像設計一個外部 API 一樣去思考它的複用性。

我們在第一次寫代碼的時候,如果當下沒有複用的需求,而未來的複用需求也不是特別明確,並且開發可複用代碼的成本比較高,那我們就不需要考慮代碼的複用性。在之後開發新的功能的時候,發現可以複用之前寫的這段代碼,那我們就重構這段代碼,讓其變得更加可複用。

相比於代碼的可複用性,DRY 原則適用性更強些。我們可以不寫可複用的代碼,但一定不能寫重複的代碼。

8.LOD 原則

如何理解“高內聚、鬆耦合”?

“高內聚、鬆耦合”是一個非常重要的設計思想,能夠有效提高代碼的可讀性和可維護性,縮小功能改動導致的代碼改動範圍。“高內聚”用來指導類本身的設計,“鬆耦合”用來指導類與類之間依賴關係的設計。所謂高內聚,就是指相近的功能應該放到同一個類中,不相近的功能不要放到同一類中。相近的功能往往會被同時修改,放到同一個類中,修改會比較集中。所謂“鬆耦合”指的是,在代碼中,類與類之間的依賴關係簡單清晰。即使兩個類有依賴關係,一個類的代碼改動也不會或者很少導致依賴類的代碼改動。

如何理解“迪米特法則”?

迪米特法則的描述爲:不該有直接依賴關係的類之間,不要有依賴;有依賴關係的類之間,儘量只依賴必要的接口。迪米特法則是希望減少類之間的耦合,讓類越獨立越好。每個類都應該少了解系統的其他部分。一旦發生變化,需要了解這一變化的類就會比較少。
在這裏插入圖片描述

四、規範與重構

1. 重構概述

重構的目的:爲什麼重構(why)?

對於項目來言,重構可以保持代碼質量持續處於一個可控狀態,不至於腐化到無可救藥的地步。對於個人而言,重構非常鍛鍊一個人的代碼能力,並且是一件非常有成就感的事情。它是我們學習的經典設計思想、原則、模式、編程規範等理論知識的練兵場。

重構的對象:重構什麼(what)?

按照重構的規模,我們可以將重構大致分爲大規模高層次的重構和小規模低層次的重構。大規模高層次重構包括對代碼分層、模塊化、解耦、梳理類之間的交互關係、抽象複用組件等等。這部分工作利用的更多的是比較抽象、比較頂層的設計思想、原則、模式。小規模低層次的重構包括規範命名、註釋、修正函數參數過多、消除超大類、提取重複代碼等編程細節問題,主要是針對類、函數級別的重構。小規模低層次的重構更多的是利用編碼規範這一理論知識。

重構的時機:什麼時候重構(when)?

我反覆強調,我們一定要建立持續重構意識,把重構作爲開發必不可少的部分融入到開發中,而不是等到代碼出現很大問題的時候,再大刀闊斧地重構。

重構的方法:如何重構(how)?

大規模高層次的重構難度比較大,需要有組織、有計劃地進行,分階段地小步快跑,時刻保持代碼處於一個可運行的狀態。而小規模低層次的重構,因爲影響範圍小,改動耗時短,所以,只要你願意並且有時間,隨時隨地都可以去做。

2. 單元測試

什麼是單元測試?

單元測試是代碼層面的測試,用於測試“自己”編寫的代碼的邏輯正確性。單元測試顧名思義是測試一個“單元”,這個“單元”一般是類或函數,而不是模塊或者系統。

爲什麼要寫單元測試?

單元測試能有效地發現代碼中的 Bug、代碼設計上的問題。寫單元測試的過程本身就是代碼重構的過程。單元測試是對集成測試的有力補充,能幫助我們快速熟悉代碼,是 TDD 可落地執行的折中方案。

如何編寫單元測試?

寫單元測試就是針對代碼設計覆蓋各種輸入、異常、邊界條件的測試用例,並將其翻譯成代碼的過程。我們可以利用一些測試框架來簡化測試代碼的編寫。對於單元測試,我們需要建立以下正確的認知:

  • 編寫單元測試儘管繁瑣,但並不是太耗時;
  • 我們可以稍微放低單元測試的質量要求;
  • 覆蓋率作爲衡量單元測試好壞的唯一標準是不合理的;
  • 寫單元測試一般不需要了解代碼的實現邏輯;
  • 單元測試框架無法測試多半是代碼的可測試性不好。

單元測試爲何難落地執行?

一方面,寫單元測試本身比較繁瑣,技術挑戰不大,很多程序員不願意去寫。另一方面,國內研發比較偏向“快糙猛”,容易因爲開發進度緊,導致單元測試的執行虎頭蛇尾,最後,沒有建立對單元測試的正確認識,覺得可有可無,單靠督促很難執行得很好。

3. 代碼的可測試性

什麼是代碼的可測試性?

粗略地講,所謂代碼的可測試性,就是針對代碼編寫單元測試的難易程度。對於一段代碼,如果很難爲其編寫單元測試,或者單元測試寫起來很費勁,需要依靠單元測試框架很高級的特性,那往往就意味着代碼設計得不夠合理,代碼的可測試性不好。

編寫可測試性代碼的最有效手段

依賴注入是編寫可測試性代碼的最有效手段。通過依賴注入,我們在編寫單元測試代碼的時候,可以通過 mock 的方法將不可控的依賴變得可控,這也是我們在編寫單元測試的過程中最有技術挑戰的地方。除了 mock 方式,我們還可以利用二次封裝來解決某些代碼行爲不可控的情況。

常見的 Anti-Patterns

典型的、常見的測試不友好的代碼有下面這 5 種:

  • 代碼中包含未決行爲邏輯;
  • 濫用可變全局變量;
  • 濫用靜態方法;
  • 使用複雜的繼承關係;
  • 高度耦合的代碼。

4. 大型重構:解耦

“解耦”爲何如此重要?

過於複雜的代碼往往在可讀性、可維護性上都不友好。解耦,保證代碼鬆耦合、高內聚,是控制代碼複雜度的有效手段。如果代碼高內聚、鬆耦合,也就是意味着,代碼結構清晰、分層、模塊化合理、依賴關係簡單、模塊或類之間的耦合小,那代碼整體的質量就不會差。

代碼是否需要“解耦”?

間接的衡量標準有很多,比如:改動一個模塊或類的代碼受影響的模塊或類是否有很多、改動一個模塊或者類的代碼依賴的模塊或者類是否需要改動、代碼的可測試性是否好等等。直接的衡量標準是把模塊與模塊之間及其類與類之間的依賴關係畫出來,根據依賴關係圖的複雜性來判斷是否需要解耦重構。

如何給代碼“解耦”?

給代碼解耦的方法有:封裝與抽象、中間層、模塊化,以及一些其他的設計思想與原則,比如:單一職責原則、基於接口而非實現編程、依賴注入、多用組合少用繼承、迪米特法則。當然,還有一些設計模式,比如觀察者模式。

5. 小型重構:編碼規範

前面我們講了很多設計原則,後面還會講到很多設計模式,利用好它們都可以有效地改善代碼的質量。但是,這些知識的合理應用非常依賴個人經驗,有時候用不好會適得其反。但是編碼規範正好相反,大部分都簡單明瞭,在代碼的細節方面,能立竿見影地改善質量。除此之外,我們前面也講到,持續低層次小規模重構依賴的基本上都是這些編碼規範,也是改善代碼可讀性的有效手段。

根據我自己的開發經驗,我總結羅列了 20 條我認爲最應該關注、最好用的編碼規範,分爲三個大的方面:命名與註釋(Naming and Comments)、代碼風格(Code Style)、編程技巧(Coding Tips)。

命名與註釋

  • 命名的關鍵是能準確的達意。對於不同作用域的命名,我們可以適當的選擇不同的長度,作用域小的命名,比如臨時變量等,可以適當的選擇短一些的命名方式。除此之外,命名中個也可以使用一些耳熟能詳的縮寫。

  • 我們藉助類的信息來簡化屬性、函數的命名,利用函數的信息來簡化函數參數的命名。

  • 命名要可讀、可搜索。不要使用生僻的、不好讀的英文單詞來命名。除此之外,命名要符合項目的統一規範,也不要用些反直覺的命名。

  • 接口有兩種命名方式。一種是在接口中帶前綴"I",另一種是在接口的實現類中帶後綴“Impl”。兩種命名方式都可以,關鍵是要在項目中統一。對於抽象類的命名,我們更傾向於帶有前綴“Abstract”。

  • 註釋的目的就是讓代碼更容易看懂,只要符合這個要求,你就可以寫。總結一下的話,註釋主要包含這樣三個方面的內容:做什麼、爲什麼、怎麼做。對於一些複雜的類和接口,我們可能還需要寫明“如何用”。

  • 註釋本身有一定的維護成本,所以並非越多越好。類和函數一定要寫註釋,而且要寫的儘可能全面詳細些,而函數內部的註釋會相對少一些,一般都是靠好的命名和提煉函數、解釋性變量、總結性註釋來做到代碼易讀。

代碼風格

代碼風格都沒有對錯和優劣之分,不同的編程語言風格都不太一樣,只要能在團隊、項目中統一即可,不過,最好能跟業內推薦的風格、開源項目的代碼風格相一致。所以,這裏就不展開羅列了,你可以對照着自己熟悉的編程語言的代碼風格,自己複習一下。

編程技巧

  • 將複雜的邏輯提煉拆分成函數和類;

  • 通過拆分成多個函數的方式來處理參數過多的情況;

  • 通過將參數封裝爲對象來處理參數過多的情況;

  • 函數中不要使用參數來做代碼執行邏輯的控制;

  • 移除過深的嵌套層次,方法包括:去掉多餘的 if 或 else 語句,使用 continue、break、return 關鍵字提前退出嵌套,調整執行順序來減少嵌套,將部分嵌套邏輯抽象成函數;

  • 用字面常量取代魔法數;

  • 利用解釋性變量來解釋複雜表達式。

統一編碼規範

除了細節的知識點之外,最後,還有一條非常重要的,那就是,項目、團隊,甚至公司,一定要制定統一的編碼規範,並且通過 Code Review 督促執行,這對提高代碼質量有立竿見影的效果。
在這裏插入圖片描述

課堂討論

不知不覺我們已經學了這麼多內容,在複習完這部分內容之後,你可以在留言區說一說你的掌握程度,看自己符合我開篇中講到的哪個層次。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章