我們在實(shí)際的項目中使用各個原則時需要審時度勢,不要抓住一個原則不放,每個原則的優(yōu)點(diǎn)都是有限度的,并不是放之四海而皆準(zhǔn)的真理,所以別為了遵循一個原則而放棄了一個項目的終極目標(biāo):投產(chǎn)上線和盈利。
—— 《設(shè)計模式之禪》
雖然目前在實(shí)際開發(fā)過程中并沒有"機(jī)會"使用太多的設(shè)計模式,但是“聽聞”許多優(yōu)秀的開源項目將設(shè)計模式用得神乎其神而自己算是看得云里霧里,竊以為要擺脫重復(fù)性低層次的編程就必須對設(shè)計模式有足夠的理解,最近在讀秦小波的《設(shè)計模式之禪》,受益良多,拾人牙慧,筆記于此。
設(shè)計模式有6個原則,本文介紹前三個原則,分別是 單一職責(zé)原則,里氏替換原則,依賴倒置原則。
1. 單一職責(zé)原則(Single Responsibility Principle)
含義:There should never be more than one reason for a class to change;即應(yīng)該有且僅有一個原因引起類的變更
單一職責(zé)原則最難劃分的就是職責(zé),一個職責(zé)一個接口,但問題是”職責(zé)“沒有一個量化的標(biāo)準(zhǔn),一個類到底要負(fù)責(zé)哪些職責(zé)?這些職責(zé)該怎么細(xì)化?細(xì)化后是否都要有一個接口或類?這些都需要從實(shí)際的項目去考慮 —— 收益成本比率
例子

- 這個類圖,將用戶信息和用戶信息糅合到了一個接口里,“有兩種原因會引起類的變化”,明顯不符合單一職責(zé)原則

將用戶信息抽取成一個BO(Business Object 業(yè)務(wù)對象),將行為抽取成一個Biz(Business Logic 業(yè)務(wù)邏輯),一個接口負(fù)責(zé)用戶信息的收集和反饋,一個接口負(fù)責(zé)用戶行為,兩個接口負(fù)責(zé)不同的職責(zé),獨(dú)立變化。
對于接口,我們在設(shè)計的時候一定要做到單一,但是對于實(shí)現(xiàn)類就需要多方面考慮了。生搬硬套單一職責(zé)原則會引起類的劇增,給維護(hù)帶來非常多的麻煩,而且過分細(xì)分類的職責(zé)也會認(rèn)為地增加系統(tǒng)的復(fù)雜性。本來一個類可以實(shí)現(xiàn)的行為硬要拆分成兩個類,然后再使用聚合或者組合的方式耦合在一起,人為制造了系統(tǒng)的復(fù)雜性。所以原則是死的,人是活的,這句話很有道理。
一般實(shí)踐是:接口一定要做到單一職責(zé),類的設(shè)計盡量做到只有一個原因引起變化
2. 里式替換原則(Liskov Substitution Principle)
-
里式替換原則有兩種說法
- 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的行為沒有發(fā)生變化,那么類型S是類型T的子類型)
- Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it .(所有引用基類的地方必須能夠透明地使用其子類的對象。)
通俗的講就是,父類能夠出現(xiàn)的地方子類也能夠出現(xiàn),而且子類替換掉父類不會引起任何錯誤或者異常,調(diào)用者根本不關(guān)心是父類還是子類。
在C++ 或者Java這類高級語言來說,則形如
Father* o1 = new Son1();
o1 = new Son2();
其中Son1 和 Son2 是 Father類的子類,Sonx類能夠無痕地傳入到表面類型為Father類的指針中。
-
這個原則包含了四層含義
-
- 子類必須完全實(shí)現(xiàn)父類的方法
- 如果子類不能完整地實(shí)現(xiàn)父類的方法,或者父類的某些方法在子類中已經(jīng)發(fā)生”畸變“,則建議斷開父子繼承關(guān)系,采用依賴、聚合、組合等關(guān)系代替繼承
-
- 子類有自己的個性
- 表面父類類型可以傳入子類,但是表面子類類型則不可以傳入父類;我看到一個例子很好理解,蘋果是水果,香蕉是水果,有水果的地方,可以傳入蘋果、香蕉;但是有香蕉的地方,不能傳入水果,因?yàn)樗灰欢ㄊ窍憬?/li>
- 其實(shí)也就是說,向下轉(zhuǎn)型(downcast)是不安全的
-
- 覆蓋或者實(shí)現(xiàn)父類的方法時輸入?yún)?shù)可以被放大
- 先了解一下前置條件和后置條件
- 前置條件:輸入必須滿足的條件
- 后置條件:輸出必須滿足的條件
- 覆寫(override)要求方法名和參數(shù)都完全相同,而重載(overload)則只要求方法名相同,當(dāng)你在子類中“覆寫”父類的一個方法時,如果你“覆寫”時將參數(shù)的類型范圍縮小了(即使你的參數(shù)個數(shù)還是跟父類相同,如父類中參數(shù)類型是map,而子類中參數(shù)類型是hashmap),這時候其實(shí)變成了重載,因此放大了參數(shù)范圍
-
- 覆寫或?qū)崿F(xiàn)父類的方法時輸出結(jié)果可以被縮小
- 即子類覆寫之后,后置條件必須比父類要窄(父類方法返回S,子類方法返回T,T是S的子類)
-
-
里式替換原則的目的是增強(qiáng)程序的健壯性,版本升級時也可以保持非常好的兼容性。即使增加子類,原有的子類還可以繼續(xù)運(yùn)行。
- —— 在實(shí)際項目中,每個子類對應(yīng)不同的業(yè)務(wù)含義,使用父類作為參數(shù),傳遞不同的子類完成不同的業(yè)務(wù)邏輯
3. 依賴倒置原則(Dependence Inversion Principle)
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 abstractions.
-
有三層含義
- 高層模塊不應(yīng)該依賴底層模塊,兩者都應(yīng)該依賴其抽象;
- 抽象不應(yīng)該依賴細(xì)節(jié);
- 細(xì)節(jié)應(yīng)該依賴抽象;
-
什么是高層模塊、低層模塊?
- 每個邏輯的實(shí)現(xiàn)都是由原子邏輯組成,不可分割的原子邏輯就是低層模塊,原子邏輯的再組裝就是高層模塊【當(dāng)然,高和低是相對的】
-
什么是抽象、細(xì)節(jié)?
- 在形如C++、Java這類高級語言中,抽象就是指接口或者抽象類,兩者都是不能直接被實(shí)例化的
- 細(xì)節(jié)就是實(shí)現(xiàn)類,實(shí)現(xiàn)接口或者繼承抽象類而產(chǎn)生的類就是細(xì)節(jié)
-
在高級語言的表現(xiàn)就是
- 模塊間的依賴通過抽象發(fā)生,實(shí)現(xiàn)類之間不發(fā)生直接的依賴關(guān)系,其依賴關(guān)系是通過接口或者抽象類產(chǎn)生的
- 接口或者抽象類不依賴實(shí)現(xiàn)類
- 實(shí)現(xiàn)類依賴接口或抽象類
說白了就是 面向接口編程,這也是OOD(Object-Oriented Design 面向?qū)ο笤O(shè)計)的精髓
舉個例子

- 實(shí)現(xiàn)類司機(jī)類和奔馳類高度耦合,如果想新增寶馬類汽車,需要修改司機(jī)類,十分不穩(wěn)定

模塊(司機(jī)類和各種車類)之間的依賴通過抽象(接口)發(fā)生,當(dāng)需要添加另一種車類型時(變更發(fā)生),只需要實(shí)現(xiàn)接口ICar即可,司機(jī)類不能修改任何東西,比較穩(wěn)定。
一個變量有兩種類型:表面類型和實(shí)際類型;表面類型是在定義的時候聲明的類型,實(shí)際類型是對象的類型
-
來看看一個客戶端使用場景
54726719-304F-482B-9517-BA830024F98A.png -
Client 屬于高層業(yè)務(wù)邏輯,它對低層模塊的依賴都建立在抽象上
- —— 這意味著,大部分類變量的表現(xiàn)都是抽象類類型,而實(shí)際的類型由實(shí)際實(shí)例化的對象決定?!具@也暗合 里式替換原則】
依賴倒置原則的本質(zhì)就是通過抽象(接口或者抽象類)使得各個類或模塊的實(shí)現(xiàn)彼此獨(dú)立,不相互影響,實(shí)現(xiàn)模塊間的松耦合
最佳實(shí)踐
- 每個類盡量都有接口或抽象類,或者兩者皆有
- 變量的表面類型盡量是接口或者抽象類 (一般工具類除外)
- 任何類都不應(yīng)該從具體類派生 【當(dāng)然這并不是絕對的,一般不超過兩層的繼承都是可以忍受的】
- 盡量不要覆寫基類的已實(shí)現(xiàn)方法 【類間的依賴是抽象,覆寫了基類方法,對依賴的穩(wěn)定性會產(chǎn)生一定的影響】
-
什么是倒置
- 先了解什么是正置:依賴正置就是類間的依賴是實(shí)實(shí)在在的實(shí)現(xiàn)類的依賴,也就是面向?qū)崿F(xiàn)編程 —— 我要開奔馳車就依賴奔馳車,我要用筆記本電腦就直接依賴筆記本電腦,這也符合人的思維模式
- 而我們編程的時候,使用抽象間的依賴,替代了人們傳統(tǒng)思維中的事物間的依賴,“倒置”就是從這里產(chǎn)生的
