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