1、單一職責(zé)原則(SRP)
定義:就一個類而言,應(yīng)該僅有一個引起它變化的原因
為什么需要單一職責(zé)呢?如果一個類承擔(dān)的職責(zé)過多,就等于把這些職責(zé)耦合在一起了,一個職責(zé)的變化可能會引起其它職責(zé)的變化,當(dāng)變化發(fā)生時,設(shè)計會遭到意想不到的變化。
我們看看下面簡單的類圖,UserDiscount類具有兩個方法,一個是獲取等級類型,一個是計算折扣價格。
有兩個不同的類在使用UserDiscount,Order需要獲取用戶等級和計算價格;User只需要獲取用戶等級,但不需要計算價格,這個設(shè)計違反類SRP,如果其中一個使用類的改變導(dǎo)致UserDiscount改變,這樣會導(dǎo)致其它使用類也需要變更、測試、部署等問題。我們需要拆分兩個職責(zé)類,如下圖:
但是,如果類的變化總是導(dǎo)致這兩個職責(zé)的同時變化,那么就不必分離它們,實際上,分離它們可能會導(dǎo)致復(fù)雜性增加。或者說,變化的軸線僅當(dāng)變化實際發(fā)生時才具有真正意義。如果沒有征兆,那么去應(yīng)用SRP或者其它原則都是不可取的。
結(jié)論:SRP是最簡單的職責(zé)之一,但是也比較難正確運用的職責(zé),在開發(fā)中,會自然地把職責(zé)結(jié)合在一起,畢竟有些職責(zé)需要耦合在一起的,而難以拆分并增加復(fù)雜性。
2、開放封閉原則(OCP)
定義:軟件實體(類、模塊、函數(shù)等等)應(yīng)該是可以擴展的,但是不可以修改的
- 對于擴展是開放的:模塊行為是可以擴展的,當(dāng)應(yīng)用需求改變時,我們可以對模塊進行擴展,使其滿足那些改變的行為。
- 對于修改是封閉的:對模塊擴展時,不必改動模塊的源代碼
下面來看個播放MP3的例子,MP3和Player都是具體類,MP3直接使用Player播放音樂,但是如果需要播放音頻,那么就需要重新修改Player而導(dǎo)致MP3也需要修改。
下面我們修改下例子而遵循OCP原則
這個設(shè)計中,IPlayer是一個接口,MP3和Video繼承該接口,今后想增加其它類型的播放只需要繼承IPlayer就行,無需修改MP3或Video類。
但實際開放中,無論模塊多么封閉,都會存在一些無法對之封閉的現(xiàn)象,那就需要有策略的去對待這個問題,模塊應(yīng)該對哪種變化封閉而做出選型,必須先猜測最有可能發(fā)生變化的情況,然后構(gòu)造出抽象來隔離。
結(jié)論:遵循OOP可以帶來靈活性、可重用性、以及可維護性。然而,對于應(yīng)用程序中每個部分都肆意的進行抽象同樣是不行的,這樣屬于不成熟抽象,我們只需要把頻繁變化的部分進行抽象就行。
3、Liskov替換原則(LSP)
定義:子類型必須能夠替換掉它們的基類型
舉個例子,函數(shù)a使用的參數(shù)是基類B,但是C類繼承基類B,但把C做為參數(shù)傳給了函數(shù)a而導(dǎo)致其發(fā)生錯誤,這樣就是違反了LSP原則。主要體現(xiàn)在下面四個方面:
- 子類必須實現(xiàn)父類的抽象方法,但不得重寫(覆蓋)父類的非抽象(已實現(xiàn))方法。
- 子類中可以增加自己特有的方法。
- 當(dāng)子類覆蓋或?qū)崿F(xiàn)父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入?yún)?shù)更寬松。
- 當(dāng)子類的方法實現(xiàn)父類的抽象方法時,方法的后置條件(即方法的返回值)要比父類更嚴(yán)格。
下面來看下簡單類圖,違反來SRP原則,定義了一個Rectangle和一個繼承自Rectangle的Square,看著是非常符合邏輯的,但是我們重新設(shè)置Rectangle的寬度,會導(dǎo)致Square的寬度也會變動,導(dǎo)致Square出錯。
改變一下不符合SRP,我們再定義一個他們共同的父類Graphics,然后讓Rectangle和Square都繼承自這個父類。在基類Graphics類中沒有賦值方法,因此重設(shè)寬高不可能適用于Graphics類型,而只能適用于不同的具體子類Rectangle和Aquare,因此里氏替換原則不可能被破壞。并且下面的設(shè)計也符合OCP原則。
結(jié)論:使用LSP,使得程序具有更多的可維護性、可重用性以及健壯性。而LSP是使OCP成為可能的主要原則之一,子類型的可替換性才使得基類類型的模塊在無需修改的情況下可以擴展。
4、依賴倒置原則(DIP)
定義:高層模塊不應(yīng)該依賴于低層模塊,二者應(yīng)該依賴于抽象;抽象不應(yīng)該依賴于細(xì)節(jié),細(xì)節(jié)應(yīng)該依賴于抽象。
下面來看下簡單例子,用戶有多個用戶等級類UseOrdinary和UserDiamond,而UserTypeService使用等級類進行相關(guān)的邏輯處理,今后如果增強其它用戶等級,就需要修改UserTypeService,這樣違反類DIP,高層策略沒有和低層實現(xiàn)分離,抽象沒有和具體細(xì)節(jié)分離,沒有這種分離,高層策略就自動地依賴于低層策略,抽象就自動地依賴于具體細(xì)節(jié)。
我們變更下具體的實現(xiàn)方式,抽象出UseType接口,UseOrdinary和UserDiamond繼承該接口,而UserTypeService使用了UseType,不管今后增加什么用戶等級都無需修改UserTypeService,并對于具體的實現(xiàn)類我們是不管的,只要接口的行為不發(fā)生變化,增加新的用戶等級后,上層服務(wù)不用做任何的修改。這樣設(shè)計降低了層與層之間的耦合,能很好地適應(yīng)需求的變化,大大提高了代碼的可維護性。
結(jié)論:設(shè)置倒置的依賴關(guān)系結(jié)構(gòu),使得細(xì)節(jié)和策略都依賴于抽象,屬于面向?qū)ο笤O(shè)計;如果依賴關(guān)系不倒置,屬于過程化設(shè)計。
5、接口隔離原則(ISP)
定義:不應(yīng)該強迫繼承類依賴于它們不使用的接口方法,類間的依賴關(guān)系應(yīng)該建立在最小的接口上
使用者依賴了那些它們不使用的方法,就面臨著這些未使用的方法改變而帶來的變更,無意中導(dǎo)致了它們之間的耦合,下面來看下簡單示例,MatchingHandler是一個匹配接口,包含匹配系統(tǒng)ID(handleSystemId)和處理聯(lián)賽ID(detectLeagueId),MatchMatching和LeagueMatching繼承了該接口,但MatchMatching不需要處理處理聯(lián)賽ID,也繼承了該方法,這樣方法改變而帶來的變更。
我們在來看下變更后的簡單類圖,新增了LeagueMatchingHandler(detectLeagueId),LeagueMatching繼承了該接口,detectLeagueId方法的變更不會導(dǎo)致MatchMatching也需要變更,只會影響到LeagueMatching。
結(jié)論:胖類是這個類過于臃腫,可能會導(dǎo)致使用者產(chǎn)生不正常的耦合關(guān)系,該類的修改也會導(dǎo)致使用者的修改。使用接口分解,使用者只需要使用特定的接口,并解除了和胖類的耦合關(guān)系。
6、迪米特原則(LOD)
定義:類之間盡可能少與其他實體發(fā)生相互作用
在開發(fā)中,我們經(jīng)常提到高內(nèi)聚低耦合,使各個模塊之間的耦合盡量的低,才能提高代碼的復(fù)用率,耦合的方式很多,依賴、關(guān)聯(lián)、組合、聚合等。其中,我們稱出現(xiàn)成員變量、方法參數(shù)、方法返回值中的類為低耦合,而出現(xiàn)在局部變量中的類則高耦合。也就是說,陌生的類最好不要作為局部變量的形式出現(xiàn)在類的內(nèi)部。下面我們來看下例子,定義了Match,Team和Player,Match都引用了Team和Player,Team又引用了Player,這樣違反了LOD,導(dǎo)致了Match跟Player耦合增加。
下面我們來變更下引用,Match只需要引用了Team就行,無需在引用Palyer,因為Team已經(jīng)引用了Player。這樣Match可以打印出相關(guān)選手了。
結(jié)論:LOD的初衷是降低類之間的耦合,由于每個類都減少了不必要的依賴,因此的確可以降低耦合關(guān)系,但這樣必須會產(chǎn)生一個中介類,由這個中介類來處理類之間的通信,過多的中介類會導(dǎo)致系統(tǒng)復(fù)雜度增大而難以維護。設(shè)計的時候需要權(quán)衡,保持結(jié)構(gòu)清晰和高內(nèi)聚低耦合
|