做后臺產(chǎn)品的時候,為了避免設(shè)計地凌亂,想了解一些系統(tǒng)設(shè)計模式思想,于是找高內(nèi)聚低耦合相關(guān)的文章。這篇文章是摘自網(wǎng)友的。
總體來說設(shè)計模式分為三大類:
創(chuàng)建型模式,共五種:工廠方法模式、抽象工廠模式、單例模式、建造者模式、原型模式。 結(jié)構(gòu)型模式,共七種:適配器模式、裝飾器模式、代理模式、外觀模式、橋接模式、組合模式、享元模式。 行為型模式,共十一種:策略模式、模板方法模式、觀察者模式、迭代子模式、責任鏈模式、命令模式、備忘錄模式、狀態(tài)模式、訪問者模式、中介者模式、解釋器模式。
六大原則
單一職責原則Single Responsibility Principle
定義:一個類或者一個接口,最好只負責一項職責。
問題由來:類T負責兩個不同的職責P1和P2。由于職責P1需要發(fā)生改變而需要修改T類時,有可能導(dǎo)致原來運行正常的職責P2功能發(fā)生故障。
解決方法:遵循單一職責原則。分別建立兩個類T1和T2,使類T1負責職責P1,類T2負責職責P2。這樣,當修改類T1也不會影響職責P2;同理,當修改類T2時不會影響職責P1。
有時候也會有違背這一原則的代碼存在。因為有職責擴散,就是因為某種原因,職責P被分化為粒度更細的職責P1和P2。比如:類T只負責一個職責P,這樣設(shè)計是符合單一職責原則的。后來由于某種原因,也許是需求變更了,也許是程序的設(shè)計者境界提高了,需要將職責P細分為粒度更細的職責P1,P2,這時如果要使程序遵循單一職責原則,需要將類T也分解為兩個類T1和T2,分別負責P1、P2兩個職責。但是在程序已經(jīng)寫好的情況下,這樣做簡直太費時間了。所以,簡單的修改類T,用它來負責兩個職責是一個比較不錯的選擇,雖然這樣做有悖于單一職責原則。這樣做的風險在于職責擴散的不確定性,因為我們不會想到這個職責P,在未來可能會擴散為P1,P2,P3,P4,Pn。所以記住,在職責擴散到我們無法控制的程度之前,立刻對代碼進行重構(gòu)。
我的原則是:只有邏輯足夠簡單,才可以在代碼級別上違反單一職責原則;只有類中方法數(shù)量足夠少,才可以在方法級別上違反單一職責原則;
遵循單一職責原的優(yōu)點有:類的復(fù)雜性將降低,簡單明細的代碼將使可讀性將大大提高,自然而然可維護性亦將同步提高。變更引起的風險降低,變更是必然的,如果單一職責原則遵守的好,當修改一個功能時,可以顯著降低對其他功能的影響。
里氏替換原則Liskov Substitution Principle
肯定有不少人跟我剛看到這項原則的時候一樣,對這個原則的名字充滿疑惑。其實原因就是這項原則最早是在1988年,由麻省理工學(xué)院的一位姓里的女士(Barbara Liskov)提出來的。
里氏替換原則的核心精神是:在使用基類的的地方可以任意使用其子類,能保證子類完美替換基類;這一精神其實是對繼承機制約束規(guī)范的體現(xiàn)。在父類和子類的具體實現(xiàn)中,嚴格控制繼承層次中的關(guān)系特征,以保證用子類替換基類時,程序行為不發(fā)生問題,且能正常進行下去。
里氏替換原則主要發(fā)力點是繼承基礎(chǔ)上的抽象和多態(tài),具體就是子類必須實現(xiàn)父類的方法,是重寫;這里要注意重寫(Override)與重載(Overload)的區(qū)分,即使參數(shù)的數(shù)據(jù)范圍發(fā)生變化,也能將重寫變成重載!而你原本只是想把所繼承的方法完善的具體點兒!如果是這樣的話絕對會引起以后業(yè)務(wù)邏輯的混亂。
里氏替換原則是關(guān)于繼承機制的設(shè)計原則,違反里氏替換原則將會使繼承變的一塌糊涂;而遵循里氏替換原則能夠保證系統(tǒng)具有良好的的拓展性,我們可以隨時根據(jù)需要增改不同的子類,這將大大增強程序的健壯性,讓版本的升級可以做到非常好的兼容;同時基于多態(tài)的抽象機制,能夠很好的減少代碼冗余,避免運行期的類型判別等;而在項目的實施中不同的子類對應(yīng)著不同的業(yè)務(wù),使用父類做參數(shù),不同子類可以輪番上陣,必然強大!
定義2:所有引用基類的地方必須能透明地使用其子類的對象。
問題由來:有一功能P1,由類A完成。現(xiàn)需要將功能P1進行擴展,擴展后的功能為P,其中P由原有功能P1與新功能P2組成。新功能P由類A的子類B來完成,則子類B在完成新功能P2的同時,有可能會導(dǎo)致原有功能P1發(fā)生故障。
解決方案:當使用繼承時,遵循里氏替換原則。類B繼承類A時,除添加新的方法完成新增功能P2外,盡量不要重寫父類A的方法,也盡量不要重載父類A的方法。
繼承包含這樣一層含義:父類中凡是已經(jīng)實現(xiàn)好的方法(相對于抽象方法而言),實際上是在設(shè)定一系列的規(guī)范和契約,雖然它不強制要求所有的子類必須遵從這些契約,但是如果子類對這些非抽象方法任意修改,就會對整個繼承體系造成破壞。而里氏替換原則就是表達了這一層含義。
繼承作為面向?qū)ο笕筇匦灾唬诮o程序設(shè)計帶來巨大便利的同時,也帶來了弊端。比如使用繼承會給程序帶來侵入性,程序的可移植性降低,增加了對象間的耦合性,如果一個類被其他的類所繼承,則當這個類需要修改時,必須考慮到所有的子類,并且父類修改后,所有涉及到子類的功能都有可能會產(chǎn)生故障。
如果非要重寫父類的方法,比較通用的做法是:原來的父類和子類都繼承一個更通俗的基類,原有的繼承關(guān)系去掉,采用依賴、聚合,組合等關(guān)系代替。
里氏替換原則通俗的來講就是:子類可以擴展父類的功能,但不能改變父類原有的功能。它包含以下4層含義:
子類可以實現(xiàn)父類的抽象方法,但不能覆蓋父類的非抽象方法。 子類中可以增加自己特有的方法。 當子類的方法重載父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入?yún)?shù)更寬松。 當子類的方法實現(xiàn)父類的抽象方法時,方法的后置條件(即方法的返回值)要比父類更嚴格。
看上去很不可思議,因為我們會發(fā)現(xiàn)在自己編程中常常會違反里氏替換原則,程序照樣跑的好好的。所以大家都會產(chǎn)生這樣的疑問,假如我非要不遵循里氏替換原則,你寫的代碼出問題的幾率將會大大增加。
依賴倒置原則Dependence Inversion Principle
定義:高層模塊不應(yīng)該依賴低層模塊,二者都應(yīng)該依賴其抽象;抽象不應(yīng)該依賴細節(jié);細節(jié)應(yīng)該依賴抽象。其核心思想是:依賴于抽象。
問題由來:類A直接依賴類B,假如要將類A改為依賴類C,則必須通過修改類A的代碼來達成。這種場景下,類A一般是高層模塊,負責復(fù)雜的業(yè)務(wù)邏輯;類B和類C是低層模塊,負責基本的原子操作;假如修改類A,會給程序帶來不必要的風險。
解決方案:將類A修改為依賴接口I,類B和類C各自實現(xiàn)接口I,類A通過接口I間接與類B或者類C發(fā)生聯(lián)系,則會大大降低修改類A的幾率。
依賴倒置原則基于這樣一個事實:相對于細節(jié)的多變性,抽象的東西要穩(wěn)定的多。以抽象為基礎(chǔ)搭建起來的架構(gòu)比以細節(jié)為基礎(chǔ)搭建起來的架構(gòu)要穩(wěn)定的多。
在java中,抽象指的是接口或者抽象類,細節(jié)就是具體的實現(xiàn)類,使用接口或者抽象類的目的是制定好規(guī)范和契約,而不去涉及任何具體的操作,把展現(xiàn)細節(jié)的任務(wù)交給他們的實現(xiàn)類去完成。
依賴倒置原則的核心思想是面向接口編程,我們依舊用一個例子來說明面向接口編程比相對于面向?qū)崿F(xiàn)編程好在什么地方。場景是這樣的,母親給孩子講故事,只要給她一本書,她就可以照著書給孩子講故事了。
傳遞依賴關(guān)系有三種方式,以上的例子中使用的方法是接口傳遞,另外還有兩種傳遞方式:構(gòu)造方法傳遞和setter方法傳遞,相信用過Spring框架的,對依賴的傳遞方式一定不會陌生。
在實際編程中,我們一般需要做到如下3點:
低層模塊盡量都要有抽象類或接口,或者兩者都有。 變量的聲明類型盡量是抽象類或接口。 使用繼承時遵循里氏替換原則。
依賴倒置原則的核心就是要我們面向接口編程,理解了面向接口編程,也就理解了依賴倒置。
接口隔離原則
定義:客戶端不應(yīng)該依賴它不需要的接口;一個類對另一個類的依賴應(yīng)該建立在最小的接口上。 否則將會造成接口污染。類A通過接口I依賴類B,類C通過接口I依賴類D,如果接口I對于類A和類B來說不是最小接口,則類B和類D必須去實現(xiàn)他們不需要的方法。
解決方案:將臃腫的接口I拆分為獨立的幾個接口,類A和類C分別與他們需要的接口建立依賴關(guān)系。也就是采用接口隔離原則。
接口隔離原則的含義是:建立單一接口,不要建立龐大臃腫的接口,盡量細化接口,接口中的方法盡量少。也就是說,我們要為各個類建立專用的接口,而不要試圖去建立一個很龐大的接口供所有依賴它的類去調(diào)用。本文例子中,將一個龐大的接口變更為3個專用的接口所采用的就是接口隔離原則。在程序設(shè)計中,依賴幾個專用的接口要比依賴一個綜合的接口更靈活。接口是設(shè)計時對外部設(shè)定的契約,通過分散定義多個接口,可以預(yù)防外來變更的擴散,提高系統(tǒng)的靈活性和可維護性。
說到這里,很多人會覺的接口隔離原則跟之前的單一職責原則很相似,其實不然。其一,單一職責原則原注重的是職責;而接口隔離原則注重對接口依賴的隔離。其二,單一職責原則主要是約束類,其次才是接口和方法,它針對的是程序中的實現(xiàn)和細節(jié);而接口隔離原則主要約束接口,主要針對抽象,針對程序整體框架的構(gòu)建。
采用接口隔離原則對接口進行約束時,要注意以下幾點:
接口盡量小,但是要有限度。對接口進行細化可以提高程序設(shè)計靈活性是不掙的事實,但是如果過小,則會造成接口數(shù)量過多,使設(shè)計復(fù)雜化。所以一定要適度。為依賴接口的類定制服務(wù),只暴露給調(diào)用的類它需要的方法,它不需要的方法則隱藏起來。只有專注地為一個模塊提供定制服務(wù),才能建立最小的依賴關(guān)系。提高內(nèi)聚,減少對外交互。使接口用最少的方法去完成最多的事情。
運用接口隔離原則,一定要適度,接口設(shè)計的過大或過小都不好。設(shè)計接口的時候,只有多花些時間去思考和籌劃,才能準確地實踐這一原則。
現(xiàn)實中,如何把握接口越小越好,這個度很難界定,顆粒度小固然靈活,但同時會造成結(jié)構(gòu)的復(fù)雜化,以下有幾個把握規(guī)則可以參考:
一個接口只服務(wù)于一個子模塊或業(yè)務(wù)邏輯,服務(wù)定制; 通過業(yè)務(wù)邏輯壓縮接口中的public方法,讓接口看起來精悍; 已經(jīng)被污染了的接口,盡量修改,如果變更風險太大,則用適配器模式進行轉(zhuǎn)化處理; 根據(jù)具體的業(yè)務(wù),深入了解邏輯,用心感知去控制設(shè)計思路。
具體如何實施接口隔離,主要有兩種方法:
1. 委托分離,通過增加一個新的接口類型來委托客戶的請求,隔離客戶和接口的直接依賴,注意這同時也會增加系統(tǒng)的開銷;
2. 多重繼承分離,通過接口的多重繼承來實現(xiàn)客戶的需求,這種方式相對較好。具體的使用,視情況而定。
迪米特法則Demeter Principle
定義:一個對象應(yīng)該對其他對象保持最少的了解。其核心精神是:不和陌生人說話,通俗之意是一個對象對自己需要耦合關(guān)聯(lián)調(diào)用的類應(yīng)該知道的更少。這樣會導(dǎo)致類之間的耦合度降低,每個類都盡量減少對其他類的依賴,因此,這也很容易使得系統(tǒng)的功能模塊相互獨立,之間不存在很強的依賴關(guān)系。
問題由來:類與類之間的關(guān)系越密切,耦合度越大,當一個類發(fā)生改變時,對另一個類的影響也越大。
解決方案:盡量降低類與類之間的耦合。
自從我們接觸編程開始,就知道了軟件編程的總的原則:低耦合,高內(nèi)聚。無論是面向過程編程還是面向?qū)ο缶幊蹋挥惺垢鱾€模塊之間的耦合盡量的低,才能提高代碼的復(fù)用率。低耦合的優(yōu)點不言而喻,但是怎么樣編程才能做到低耦合呢?那正是迪米特法則要去完成的。迪米特法則又叫最少知道原則,最早是在1987年由美國Northeastern University的Ian Holland提出。通俗的來講,就是一個類對自己依賴的類知道的越少越好。也就是說,對于被依賴的類來說,無論邏輯多么復(fù)雜,都盡量地的將邏輯封裝在類的內(nèi)部,對外除了提供的public方法,不對外泄漏任何信息。迪米特法則還有一個更簡單的定義:只與直接的朋友通信。首先來解釋一下什么是直接的朋友:每個對象都會與其他對象有耦合關(guān)系,只要兩個對象之間有耦合關(guān)系,我們就說這兩個對象之間是朋友關(guān)系。耦合的方式很多,依賴、關(guān)聯(lián)、組合、聚合等。其中,我們稱出現(xiàn)成員變量、方法參數(shù)、方法返回值中的類為直接的朋友,而出現(xiàn)在局部變量中的類則不是直接的朋友。也就是說,陌生的類最好不要作為局部變量的形式出現(xiàn)在類的內(nèi)部。
舉一個例子:有一個集團公司,下屬單位有分公司和直屬部門,現(xiàn)在要求打印出所有下屬單位的員工ID。先來看一下違反迪米特法則的設(shè)計。
開閉原則Open Close Principle
定義:一個軟件實體如類、模塊和函數(shù)應(yīng)該對擴展開放,對修改關(guān)閉。
開閉原則無非就是想表達這樣一層意思:用抽象構(gòu)建框架,用實現(xiàn)擴展細節(jié)。因為抽象靈活性好,適應(yīng)性廣,只要抽象的合理,可以基本保持軟件架構(gòu)的穩(wěn)定。而軟件中易變的細節(jié),我們用從抽象派生的實現(xiàn)類來進行擴展,當軟件需要發(fā)生變化時,我們只需要根據(jù)需求重新派生一個實現(xiàn)類來擴展就可以了。當然前提是我們的抽象要合理,要對需求的變更有前瞻性和預(yù)見性才行。
說到這里,再回想一下前面說的5項原則,恰恰是告訴我們用抽象構(gòu)建框架,用實現(xiàn)擴展細節(jié)的注意事項而已:
單一職責原則告訴我們實現(xiàn)類要職責單一; 里氏替換原則告訴我們不要破壞繼承體系; 依賴倒置原則告訴我們要面向接口編程; 接口隔離原則告訴我們在設(shè)計接口的時候要精簡單一; 迪米特法則告訴我們要降低耦合。 而開閉原則是總綱,他告訴我們要對擴展開放,對修改關(guān)閉。
最后說明一下如何去遵守這六個原則。對這六個原則的遵守并不是是和否的問題,而是多和少的問題,也就是說,我們一般不會說有沒有遵守,而是說遵守程度的多少。任何事都是過猶不及,設(shè)計模式的六個設(shè)計原則也是一樣,制定這六個原則的目的并不是要我們刻板的遵守他們,而需要根據(jù)實際情況靈活運用。對他們的遵守程度只要在一個合理的范圍內(nèi),就算是良好的設(shè)計。