專欄地址:xiaozhuanlan.com/fullstack編程
做爲開發人員,或多或少都會熟悉或瞭解一些設計模式,如單例模式、工廠模式、觀察者模式等等。但並不是都能理解這些設計模式背後的本質,從而可能會致使對模式單純的套用或濫用的狀況出現。不要爲了模式而模式,要明白使用模式的目的,要正確理解模式背後的設計原理,要理解背後的基本設計原則。後端
首先,咱們要明白使用設計模式的目的:爲了代碼可重用性、讓代碼更容易被他人理解、保證代碼可靠性。那麼,若是咱們開發的應用並非爲了這些目的,其實就不必使用設計模式,好比 Solidity 智能合約目前就不太適合直接套用設計模式。設計模式
其次,要理解設計模式背後一些重要的設計原則,全部設計模式基本都是基於這些設計原則總結出來的,這纔是設計模式的本質和精髓所在。架構
人們總結出來的設計原則也不少,而從源頭開始,GoF(Gang of Four)在《設計模式》一書中只提到兩個設計原則:框架
後來的人們給上面兩個設計原則分別起了專業的名字:依賴倒置原則和合成複用原則。並且,還總結出了其餘設計原則,主要包括里氏替換原則、單一職責原則、接口隔離原則、迪米特法則、開閉原則等。接下來就詳細闡述下這幾個設計原則。前後端分離
依賴倒置原則(Dependence Inversion Principle,DIP),其原始定義爲:函數
High level modules should not depend upon low level modules, Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstracts.翻譯
翻譯過來就是:設計
所謂抽象,就是指接口或抽象類;所謂細節,就是指實現了接口或繼承了抽象類的具體實現類。上面內容便是說,模塊之間的依賴關係,應該經過接口或抽象類而產生,模塊的實現類之間不要發生直接的依賴關係;並且接口或抽象類不該該依賴於實現類,實現類應該依賴於接口或抽象類。其核心思想也是 GoF 所提的針對接口編程,而不是針對實現編程。cdn
咱們知道,具體實現類是頗有可能常常發生變動的,但接口或抽象類則不多會改變。所以,依賴於抽象,能夠大大減低模塊間的耦合度,以及能夠提升模塊的可複用性和程序的穩定性。不過,相應地,也會增長代碼量。
不少設計模式都遵循了該原則,好比工廠類模式、觀察者模式、適配器模式、策略模式等等。
在咱們平時的實際開發中,若是想提升代碼的可重用性、擴展性,那就應該儘可能遵循該原則。可是,也不要陷入另外一個誤區,就是每個類都抽象出一個對應的接口。
合成複用原則(Composite Reuse Principle,CRP),也稱爲組合/聚合複用原則(Composition/Aggregate Reuse Principle,CARP),該原則提出:優先使用合成複用,而不是繼承複用。
咱們知道,類的複用有兩種方式:合成與繼承。合成便是組合或聚合。爲何要優先使用合成複用呢?這是由於繼承複用主要有兩個缺陷:
使用合成複用則能夠將已有對象(也稱爲成員對象)歸入到新對象中,使之成爲新對象的一部分,新對象能夠調用已有對象的功能。所以,已有對象的內部實現細節對新對象就是不可見的,這就是黑箱複用,不會破壞類的封裝性,其耦合度也相對較低,所以能夠提升擴展性。
所以,須要複用時,咱們要優先考慮能不能使用合成,實在不合適才考慮繼承。而使用繼承時,還須要遵循另外一個設計原則:里氏替換原則。關於這個原則,後面再講。
另外,使用合成複用時,還能夠再結合上面的依賴倒置原則,讓新對象和已有對象的交互經過接口或抽象類進行,從而能夠更進一步減低耦合度。
里氏替換原則(Liskov Substitution Principle,LSP)主要用來規範如何正確地使用繼承,其定義有兩種:
If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T,the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.
翻譯:若是對每個類型爲 S 的對象 o1,都有一個類型爲 T 的對象 o2,使得以 T 定義的全部程序 P 在全部的對象 o1 都替換成 o2 時,程序 P 的行爲沒有發生變化,那麼類型 S 是類型 T 的子類型。
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
翻譯:全部引用基類的地方必須能透明地使用其子類的對象。
很明顯,第二種定義更通俗易懂,其實就是說,只要基類出現的地方,均可以替換爲子類,並且程序的功能不會發生變化。
注意最後一點很關鍵,要保證替換爲子類以後,程序功能不會發生變化,那麼,子類不能重寫(覆蓋)父類已實現的方法。若是子類重寫了父類已實現的方法,那極可能就會獲得不同的結果。由於替換成子類對象以後,調用該對象的方法時,實際上就會調用子類的方法,那結果就和調用父類方法不同了。
雖然子類不能重寫父類已實現的方法,但能夠重載父類已實現的方法,但要求重載的方法形參要比父類方法的輸入參數更寬鬆。好比,父類有一個方法爲 func(HashMap map),那子類方法能夠爲 func(Map map),由於 Map 比 HashMap 更寬鬆。假設父類實例爲 fa,子類實例爲 su,那 fa.func(HashMap或其子類) 和 su.func(HashMap或其子類) 所調用的都是父類的方法 func(HashMap map),這樣,替換以後的結果就能保證一致。而若是反過來,父類的形參爲 Map,子類的形參爲 HashMap,那調用 su.func(HashMap或其子類) 時就會優先調用子類的方法了,那結果和調用父類方法可能就不同了,所以,這是違背里氏替換原則的。
通常來講,程序中的父類大可能是抽象類,只定義了一個框架,具體功能須要子類來實現。並且父類中已實現的代碼自己已經足夠好,子類只須要進行擴展便可,儘可能避免對其已經實現的方法再去重寫。
單一職責原則(Single Responsibility Principle,SRP)是你們最熟悉、也最容易理解的一個設計原則了,其定義也是很是簡單:
There should never be more than one reason for a class to change.
意思就是,致使類變動的緣由不能超過一個。換句話說就是,一個類只負責一個職責。類的職責單一,類的複雜度就會下降,代碼維護起來天然也更容易。咱們都知道,若是一個類包含了不少職責,那這個類就會變得很是臃腫,很差維護。
其實,單一職責原則不僅是適用於類,對於接口和方法也適用。
雖然單一職責原則很是簡單,也很是好理解,但若是應用到實際開發中,其實又不是那麼容易。要應用好單一職責原則,核心在於如何能作好職責的劃分,如何定義職責的粒度大小,缺少設計經驗的人很容易將一個類的職責粒度定義得過粗或過細。因此,能把該設計原則應用得好,實際上是須要很強的分析設計能力的。
若是再延伸出去,單一職責原則其實還普遍應用到架構中,如先後端分離、讀寫分離、架構分層、數據模型與業務邏輯分離等等,其實都是將大粒度的職責進行拆解分離。所謂大道至簡,因此不要小看一個簡單的單一職責原則。
接口隔離原則(Interface Segregation Principle,ISP)也有兩個定義:
Clients should not be forced to depend upon interfaces that they don`t use.
客戶端不該該依賴它不須要的接口。
The dependency of one class to another one should depend on the smallest possible.
一個類對另外一個類的依賴應該創建在最小的接口上。
咱們知道,一個類若是要實現一個接口,就必須實現這個接口所要求的全部方法。那麼,若是這個接口裏包含了這個類不須要的方法,這其實就會形成接口污染。要避免接口污染,就須要將這個接口拆分,只提取出這個類須要的方法,組成一個新的接口,而後讓這個類去實現這個新接口,這就是接口隔離原則。
所謂接口隔離,隔離的其實就是多餘的方法。遵循接口隔離原則,就能夠避免創建龐大臃腫的接口,避免形成接口污染,可提升程序的靈活性和可維護性。
在具體的應用中,咱們應該儘可能細化接口,讓接口中的方法儘可能少。儘可能爲不一樣的類創建不一樣的專用接口,避免創建一個綜合性的接口供多個不一樣需求的類調用。
不過,細化的程度也不是越細越好,若是過分細化,則會形成接口數量過多,反而使得程序複雜化,因此,細化接口也要適度。
另外,不少人都會發覺接口隔離原則跟單一職責原則很類似,其實二者的關注的角度不一樣。單一職責原則的關注點是業務邏輯上的職責劃分,而接口隔離原則關注的則是接口數量要小。實際上,咱們在平時設計接口時,應該兩個原則都要遵循。
迪米特法則(Law of Demeter,LoD)又叫做最少知識原則(Least Knowledge Principle,LKP),其定義也很是好理解:
Each unit should have only limited knowledge about other units: only units "closely" related to the current unit.
每一個單元對其餘單元只擁有有限的知識,只瞭解與當前單元緊密聯繫的單元。
第一句話比較容易作到,只要儘可能減小一個類對外暴露的方法便可。而第二句話,等同於下面這句對迪米特法則的另外一個更直白的定義:
Only talk to your immediate friends.
只與直接的朋友通訊。
所謂直接的朋友,就是指在邏輯上有直接耦合關係的對象和類。通常來講,出如今成員變量、方法參數、方法返回值中的類爲直接的朋友,而出如今局部變量中的類則不是直接的朋友。也就是說,陌生的類最好不要做爲局部變量的形式出如今類的內部。
迪米特法則的初衷在於下降類之間的耦合,讓每一個類儘可能減小對其餘類的依賴,才能提升代碼的複用率。
你會發覺,迪米特法則也正好應對了高內聚低耦合的設計思想。減小一個類對外暴露的方法,從而讓其餘類減小對它的瞭解,這就是高內聚;只與直接的朋友通訊,減小對其餘類的依賴,這就是低耦合。
開閉原則(Open Closed Principle,OCP)是咱們今天要講的最後一個原則,也是其餘設計原則的基石,能夠說,其餘設計原則都只是實現開閉原則的一些手段。先來看看開閉原則的定義:
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
軟件實體(類、模塊、函數等)應對擴展開放,但對修改封閉。
意思就是說,當咱們的軟件實體須要變化時,要儘可能經過擴展軟件實體的行爲來實現變化,而不是經過修改已有的代碼。
咱們知道,全部軟件系統都不會一成不變,若是一個需求變化會致使多個依賴的模塊都發生級聯式的改動,說明程序已經呈現出「壞設計(Bad Design)」的特質了。這樣的程序就會相應地變得脆弱、僵化、沒法預期和沒法重用。開閉原則的產生就是爲了解決這些問題,它可以指導咱們如何創建穩定靈活的系統,它推崇的是已經設計完成的模塊應該從不改變。當需求變化時,能夠經過添加新代碼擴展這個模塊的行爲,而別去更改那些能夠工做的舊代碼。
那麼,如何作到對擴展開放、對修改封閉呢?其實,抽象是關鍵。咱們都知道,抽象的靈活性好、適應性廣,只要抽象定義合理,基本能夠保持軟件架構的穩定,因此咱們能夠用抽象來構建框架。而易變的細節,咱們用從抽象派生的實現類來進行擴展,當軟件須要發生變化時,咱們只須要根據需求從新派生一個實現類來擴展就能夠了。固然前提是咱們的抽象要合理,要對需求的變動有前瞻性和預見性才行。那麼,總結爲一句話就是:用抽象構建框架,用實現擴展細節。
本文總共講解了七個主要的設計原則:依賴倒置原則讓咱們針對接口編程,只依賴於抽象,不依賴實現,由於依賴抽象易於擴展;合成複用原則建議咱們優先使用組合或聚合來實現代碼的複用,也是由於合成複用耦合度低,能夠提升擴展性;里氏替換原則指導咱們如何正確地使用繼承,所以擴展的時候纔不會產生不一致的結果;單一職責原則強調一個類只負責一個職責,以提升類的擴展性和可維護性;接口隔離原則強調接口的設計要精簡,避免接口污染;迪米特法則告訴咱們要儘可能作到高內聚低耦合;開閉原則推崇對擴展開放,對修改封閉,是其餘設計原則的總綱。
掃描如下二維碼便可關注訂閱號。