1. 程式人生 > 其它 >設計模式之美-設計原則

設計模式之美-設計原則

什麼是“高內聚,低耦合”

高內聚指所有相近的功能應該放在一個類中,不相近的功能應該區分出來,單一職責就是就可以很好的實現高內聚,低耦合指類與類之間的依賴應該簡單清晰。依賴注入、介面隔離、基於介面而非實現程式設計,可以很好的實現低耦合。

高內聚用來指導類的設計,低耦合用來指導類之間的依賴關係,高內聚有助於鬆耦合,鬆耦合需要高內聚的支援。


單一職責原則 Single Responsibility Principle(SRP)

一個類或者模組只負責完成一個職責(或者功能)。

模組可以看做是比類更加抽象的的程式碼塊,多個類組成一個模組。 單一職責原則主要用來約束我們不要設計大而全的類,要設計粒度小、功能單一的類。如果一個類包含了兩個以上不相干的業務那麼這個類的職責就不夠單一。譬如(在使用者模組中存在訂單的業務)。

但大部分情況下,類裡的方法是歸為同一類功能,還是歸為不相關的兩類功能,並不是那麼容易判定的。不同的應用場景、不同階段的需求背景、不同的業務層面,對同一個類的職責是否單一,可能會有不同的判定結果。一些側面的判斷指標更具有指導意義和可執行性,比如:

  • 類中的程式碼行數、函式或屬性過多,會影響程式碼的可讀性和可維護性,我們就需要考慮對類進行拆分;
  • 類依賴的其他類過多,或者依賴類的其他類過多,不符合高內聚、低耦合的設計思想,就需要考慮對類進行拆分;
  • 私有方法過多,就要考慮能否將私有方法獨立到新的類中,設定為 public 方法,供更多的類使用,從而提高程式碼的複用性;
  • 類中大量的方法都是集中操作類中的某幾個屬性。
  • 比較難給類起一個合適名字,很難用一個業務名詞概括。

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


開閉原則 Open Closed Principle(OCP)

新增一個新的功能,應該是通過在已有程式碼基礎上擴充套件程式碼(新增模組、類、方法、屬性等),而非修改已有程式碼(修改模組、類、方法、屬性等)的方式來完成。

開閉原則並不是說完全杜絕修改,而是以最小的修改程式碼的代價來完成新功能的開發。第二點是,同樣的程式碼改動,在粗程式碼粒度下,可能被認定為“修改”;在細程式碼粒度下,可能又被認定為“擴充套件”。比如,新增屬性和方法相當於修改類,在類這個層面,這個程式碼改動可以被認定為“修改”;但這個程式碼改動並沒有修改已有的屬性和方法,在方法(及其屬性)這一層面,它又可以被認定為“擴充套件”。

只要它沒有破壞原有的程式碼的正常執行,沒有破壞原有的單元測試,我們就可以說,這是一個合格的程式碼改動。


如何在開發中做到“開閉原則”

如果開發的是一個業務導向的系統,比如金融系統、電商系統、物流系統等,要想識別出盡可能多的擴充套件點,就要對業務有足夠的瞭解,能夠知道當下以及未來可能要支援的業務需求。如果你開發的是跟業務無關的、通用的、偏底層的系統,比如,框架、元件、類庫,你需要了解“它們會被如何使用?今後你打算新增哪些功能?使用者未來會有哪些更多的功能需求?”等問題。要時刻具備擴充套件意識、抽象意識、封裝意識。

即便對業務、對系統有足夠的瞭解,那也不可能識別出所有的擴充套件點,即便你能識別出所有的擴充套件點,為這些地方都預留擴充套件點,這樣做的成本也是不可接受的。我們沒必要為一些遙遠的、不一定發生的需求去提前買單,做過度設計。最合理的做法是,對於一些比較確定的、短期內可能就會擴充套件,或者需求改動對程式碼結構影響比較大的情況,或者實現成本不高的擴充套件點,在編寫程式碼的時候之後,就可以事先做些擴充套件性設計。但對於一些不確定未來是否要支援的需求,或者實現起來比較複雜的擴充套件點,可以等到有需求驅動的時候,再通過重構程式碼的方式來支援擴充套件的需求。


里氏替換原則 Liskov Substitution Principle(LSP)

子類物件能夠替換程式中父類物件出現的任何地方,並且保證原來程式的邏輯行為不變及正確性不被破壞。最核心的就是理解“design by contract“,按照協議來設計”這幾個字。父類定義了函式的“約定”(或者叫協議),那子類可以改變函式的內部實現邏輯,但不能改變函式原有的“約定”。這裡的約定包括:函式宣告要實現的功能;對輸入、輸出、異常的約定;甚至包括註釋中所羅列的任何特殊說明。

哪些程式碼違反了”里氏替換原則“
  • 子類違背父類宣告要實現的功能
    譬如:父類中提供的 SortOrdersByAmount() 訂單排序函式,是按照金額從小到大來給訂單排序的,而子類重寫這個 SortOrdersByAmount() 訂單排序函式之後,是按照建立日期來給訂單排序的。那子類的設計就違背裡式替換原則。
  • 子類違背父類對輸入、輸出、異常的約定
    譬如:在父類中,某個函式約定:執行出錯的時候返回 null;獲取資料為空的時候返回空集合。而子類過載函式之後,實現變了,執行出錯返回異常(exception),獲取不到資料返回 null。那子類的設計就違背裡式替換原則。
  • 在父類中,某個函式約定,輸入資料可以是任意整數,但子類實現的時候,只允許輸入資料是正整數,負數就丟擲,也就是說,子類對輸入的資料的校驗比父類更加嚴格,那子類的設計就違背了裡式替換原則。
  • 在父類中,某個函式約定,只會丟擲 ArgumentNullException 異常,那子類的設計實現中只允許丟擲 ArgumentNullException 異常,任何其他異常的丟擲,都會導致子類違背裡式替換原則。
  • 子類違背父類註釋中所羅列的任何特殊說明
    父類中定義的 Withdrawal() 提現函式的註釋是這麼寫的:“使用者的提現金額不得超過賬戶餘額……”,而子類重寫 Withdrawal() 函式之後,針對 VIP 賬號實現了透支提現的功能,也就是提現金額可以大於賬戶餘額,那這個子類的設計也是不符合裡式替換原則的。

介面隔離原則 Interface Segregation Principle(ISP)

儘量將臃腫龐大的介面拆分成更小的和更具體的介面,讓介面中只包含客戶感興趣的方法。客戶端不應該依賴它不需要的介面。

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

如果把“介面”理解為單個 API 介面或函式,部分呼叫者只需要函式中的部分功能,那就需要把函式拆分成粒度更細的多個函式,讓呼叫者只依賴它需要的那個細粒度函式。

介面隔離原則與單一職責原則的區別

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


依賴反轉原則 Dependency Inversion Principle (DIP)

高層模組要依賴低層模組。高層模組和低層模組應該通過抽象來互相依賴。除此之外,抽象要依賴具體實現細節,具體實現細節依賴抽象。核心思想是:要面向介面程式設計,不要面向實現程式設計。細節具有多變性,而抽象層則相對穩定,因此以抽象為基礎搭建起來的架構要比以細節為基礎搭建起來的架構要穩定得多。這裡的抽象指的是介面或者抽象類,而細節是指具體的實現類。

迪米特原則 Law of Demeter (LOD)

又叫作最少知識原則(The Least Knowledge Principle,每個模組應該只關心自己密切相關的模組的有限知識。不應該直接有依賴的類之間不要有依賴,有依賴關係的類應該儘量只依賴必要的介面。


KISS 原則

KISS 原則就是保持程式碼可讀和可維護的重要手段。程式碼足夠簡單,也就意味著很容易讀懂,bug 比較難隱藏。即便出現 bug,修復起來也比較簡單。並不是程式碼數量越少就月簡單,也並不越複雜的程式碼就違背了Kiss原則,本身就複雜的問題,用複雜的方法解決,並不違背 KISS 原則。同樣的程式碼,在某個業務場景下滿足 KISS 原則,換一個應用場景可能就不滿足了。

幾個滿足Kiss的方法

  • 不要使用同事可能不懂的技術來實現程式碼。比如前面例子中的正則表示式,還有一些程式語言中過於高階的語法等。
  • 不要重複造輪子,要善於使用已經有的工具類庫。經驗證明,自己去實現這些類庫,出 bug 的概率會更高,維護的成本也比較高。
  • 不要過度優化。不要過度使用一些奇技淫巧(比如,位運算代替算術運算、複雜的條件語句代替 if-else、使用一些過於底層的函式等)來優化程式碼,犧牲程式碼的可讀性。
  • code review 的時候,同事對你的程式碼有很多疑問,那就說明你的程式碼有可能不夠“簡單”,需要優化。

做開發的時候,一定不要過度設計,不要覺得簡單的東西就沒有技術含量。實際上,越是能用簡單的方法解決複雜的問題,越能體現一個人的能力。


YAGNI 原則

YAGNI 原則的英文全稱是:You Ain’t Gonna Need It。直譯就是:你不會需要它。它的意思是:不要去設計當前用不到的功能;不要去編寫當前用不到的程式碼。實際上,這條原則的核心思想就是:不要做過度設計。當然,這並不是說就不需要考慮程式碼的擴充套件性。我們還是要預留好擴充套件點,等到需要的時候,再去實現。

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

設計原則本身沒有對錯,只有能否用對之說。不要為了應用設計原則而應用設計原則,在應用設計原則的時候,一定要具體問題具體分析。


DRY 原則

DRY 原則。它的英文描述為:Don’t Repeat Yourself。中文直譯為:不要重複自己。將它應用在程式設計中,可以理解為:不要寫重複的程式碼。

幾種的程式碼重複情況,分別是:

  • 實現邏輯重複
    函式中有有重複的程式碼,但是如果函式語義不重複就沒有違反 DRY 原則。
  • 功能語義重複
    譬如兩個函式都是判斷ip是否正確,就算它們實現邏輯不一致,也算違反了 DRY 原則。
  • 程式碼執行重複
    相同的函式在一次請求裡被重複執行且冪等,算違反了 DRY 原則。

DRY不是隻程式碼重複,而是“語義”的重複,意思是指業務邏輯。例如由於溝通不足,兩個程式設計師用兩種不同的方法實現同樣功能的校驗。

什麼是程式碼的複用性

首先來區分三個概念:程式碼複用性(Code Reusability)、程式碼複用(Code Resue)和 DRY 原則。
程式碼複用表示一種行為:在開發新功能的時候,儘量複用已經存在的程式碼。程式碼的可複用性表示一段程式碼可被複用的特性或能力:在編寫程式碼的時候,讓程式碼儘量可複用。

DRY 原則是一條原則:不要寫重複的程式碼。從定義描述上,它們好像有點類似,但深究起來,三者的區別還是蠻大的。首先,“不重複”並不代表“可複用”。在一個專案程式碼中,可能不存在任何重複的程式碼,但也並不表示裡面有可複用的程式碼,不重複和可複用完全是兩個概念。所以,從這個角度來說,DRY 原則跟程式碼的可複用性講的是兩回事。

其次,“複用”和“可複用性”關注角度不同。程式碼“可複用性”是從程式碼開發者的角度來講的,“複用”是從程式碼使用者的角度來講的。比如,A 同事編寫了一個 UrlUtils 類,程式碼的“可複用性”很好。B 同事在開發新功能的時候,直接“複用”A 同事編寫的 UrlUtils 類。

儘管複用、可複用性、DRY 原則這三者從理解上有所區別,但實際上要達到的目的都是類似的,都是為了減少程式碼量,提高程式碼的可讀性、可維護性。除此之外,複用已經經過測試的老程式碼,bug 會比從零重新開發要少。


備註

整理自王爭《設計模式之美》設計原則篇