[TOC]
單一設(shè)計原則---(專人專事)
單一職責原則的定義是:應(yīng)該有且僅有一個原因引起類的變更。
優(yōu)化:
重新拆封成兩個接口,IUserBO負責用戶的屬性,簡單地說,IUserBO的職責就是收集和 反饋用戶的屬性信息;IUserBiz負責用戶的行為,完成用戶信息的維護和變更。
簡單理解就是 get set 一起,其他操作封裝成biz(biz是Business的縮寫,實際上就是控制層(業(yè)務(wù)邏輯層)),當然不局限于這種類型的對象。
注意 單一職責原則提出了一個編寫程序的標準,用“職責”或“變化原因”來衡量接口或 類設(shè)計得是否優(yōu)良,但是“職責”和“變化原因”都是不可度量的,因項目而異,因環(huán)境而異
單一職責適用于接口、類,同時也適用于方法,什么意思呢?一個方法盡可能做一件事 情,比如一個方法修改用戶密碼,不要把這個方法放到“修改用戶信息”方法中,這個方法的 顆粒度很粗。
如果要修改用戶名稱,就調(diào)用changeUserName方法;要修改家庭地址, 就調(diào)用changeHomeAddress方法;要修改單位電話,就調(diào)用changeOfficeTel方法。每個方法的 職責非常清晰明確,不僅開發(fā)簡單,而且日后的維護也非常容易,大家可以逐漸養(yǎng)成這樣的 習慣。
這個單一原則重在理解,不能認死理,拆分太嚴重也會導致類或者方法數(shù)過多,具體情況具體分析吧。
里氏替換原則---(繼承規(guī)范)
主要是為良好的繼承定義了一個規(guī)范。
這個規(guī)范就是:所有引用基類的地方必須能透明地使用其子類的對象(不會改變?nèi)魏芜壿嫞?br>
通俗點講,只要父類能出現(xiàn)的地方子類就可以出現(xiàn),而且 替換為子類也不會產(chǎn)生任何錯誤或異常,使用者可能根本就不需要知道是父類還是子類。但 是,反過來就不行了,有子類出現(xiàn)的地方,父類未必就能適應(yīng)。
更正宗的定義:如果對每一個類型為S的對象o1,都有類型為T的對 象o2,使得以T定義的所有程序P在所有的對象o1都代換成o2時,程序P的行為沒有發(fā)生變 化,那么類型S是類型T的子類型。
說明:這是給繼承定義的一種良好的規(guī)范,現(xiàn)實中可能會出現(xiàn)不符合這種原則的代碼,所以這是規(guī)范,并不是所有都是這樣的。
細分里氏替換原則四種含義:
- 子類必須完全實現(xiàn)父類的方法(這里的實現(xiàn)應(yīng)該是要保證都有方法體的意思)
比如說父類是個槍,定義了一個方法“射擊”,那么任意子類(玩具槍、狙擊槍、步槍)都應(yīng)該能調(diào)用射擊這個方法。 - 子類可以有自己的個性
當然子類可以增加一些方法或者復寫一些方法 - 覆蓋或?qū)崿F(xiàn)父類的方法時輸入?yún)?shù)可以被放大
范例:
public class Father {
public Collection doSomething(HashMap map){
System.out.println("父類被執(zhí)行...");
return map.values();
}
}
public class Son extends Father {
//放大輸入?yún)?shù)類型
public Collection doSomething(Map map){
System.out.println("子類被執(zhí)行...");
return map.values();
}
}
請注意粗體部分,與父類的方法名相同,但又不是覆寫(Override)父類的方法。你加 個@Override試試看,會報錯的,為什么呢?方法名雖然相同,但方法的輸入?yún)?shù)不同,就 不是覆寫,那這是什么呢?是重載(Overload)!不用大驚小怪的,不在一個類就不能是重 載了?繼承是什么意思,子類擁有父類的所有屬性和方法,方法名相同,輸入?yún)?shù)類型又不 相同,當然是重載了。
父類使用場景:
public class Client {
public static void invoker(){
//父類存在的地方,子類就應(yīng)該能夠存在
Father f = new Father();
HashMap map = new HashMap();
f.doSomething(map);
}
public static void main(String[] args) {
invoker();
}
}
運行結(jié)果:父類被執(zhí)行...
根據(jù)里氏替換原則在這里使用子類替換:
public class Client {
public static void invoker(){
//父類存在的地方,子類就應(yīng)該能夠存在
Son f =new Son();
HashMap map = new HashMap();
f.doSomething(map);
}
public static void main(String[] args) {
invoker();
}
}
運行結(jié)果:父類被執(zhí)行...
運行結(jié)果還是一樣,看明白是怎么回事了嗎?父類方法的輸入?yún)?shù)是HashMap類型,子 類的輸入?yún)?shù)是Map類型,也就是說子類的輸入?yún)?shù)類型的范圍擴大了,子類代替父類傳遞 到調(diào)用者中,子類的方法永遠都不會被執(zhí)行。這是正確的,如果你想讓子類的方法運行,就 必須覆寫父類的方法。
對調(diào)參數(shù):
public class Father {
public Collection doSomething(Map map){
System.out.println("父類被執(zhí)行...");
return map.values();
}
}
public class Son extends Father {
//放大輸入?yún)?shù)類型
public Collection doSomething(HashMap map){
System.out.println("子類被執(zhí)行...");
return map.values();
}
}
如果依然使用上面的使用場景運行結(jié)果就會變成這樣:
運行結(jié)果:父類被執(zhí)行...
更換子類
運行結(jié)果:子類被執(zhí)行...
這就不正常了,子類在沒有覆寫父類的方法的前提下,子類方法被執(zhí)行了,這會引起業(yè)務(wù) 邏輯混亂 ,“歪曲”了父類的意圖,引起一堆意想不到的業(yè)務(wù)邏輯混亂,所以子類中方法的前置條 件必須與超類中被覆寫的方法的前置條件相同或者更寬松。
- 覆寫或?qū)崿F(xiàn)父類的方法時輸出結(jié)果可以被縮小
這是什么意思呢,父類的一個方法的返回值是一個類型F,子類的相同方法(重載或覆 寫)的返回值為S,那么里氏替換原則就要求S必須小于等于F,也就是說,要么S和F是同一 個類型,要么S是F的子類,為什么呢?分兩種情況,如果是覆寫,父類和子類的同名方法的 輸入?yún)?shù)是相同的,兩個方法的范圍值S小于等于F,這是覆寫的要求,這才是重中之重,子 類覆寫父類的方法,天經(jīng)地義。如果是重載,則要求方法的輸入?yún)?shù)類型或數(shù)量不相同,在 里氏替換原則要求下,就是子類的輸入?yún)?shù)寬于或等于父類的輸入?yún)?shù),也就是說你寫的這 個方法是不會被調(diào)用的,參考上面講的前置條件。
綜合3、4條可以總結(jié)一句話:子類入?yún)㈩愋涂梢苑糯蠓秶梢允歉溉雲(yún)⒌母割悾敵鼋Y(jié)果要縮小范圍(可是父出參的子類)。
理解起來比較困難。
依賴倒置原則---(面向接口編程)
- 高層模塊不應(yīng)該依賴低層模塊,兩者都應(yīng)該依賴其抽象;
- 抽象不應(yīng)該依賴細節(jié);
- 細節(jié)應(yīng)該依賴抽象。
高層模塊和低層模塊容易理解,每一個邏輯的實現(xiàn)都是由原子邏輯組成的,不可分割的 原子邏輯就是低層模塊,原子邏輯的再組裝就是高層模塊。那什么是抽象?什么又是細節(jié) 呢?在Java語言中,抽象就是指接口或抽象類,兩者都是不能直接被實例化的;細節(jié)就是實 現(xiàn)類,實現(xiàn)接口或繼承抽象類而產(chǎn)生的類就是細節(jié),其特點就是可以直接被實例化,也就是 可以加上一個關(guān)鍵字new產(chǎn)生一個對象。
依賴倒置原則在Java語言中的表現(xiàn)就是: - 模塊間的依賴通過抽象發(fā)生,實現(xiàn)類之間不發(fā)生直接的依賴關(guān)系,其依賴關(guān)系是通過 接口或抽象類產(chǎn)生的;
- 接口或抽象類不依賴于實現(xiàn)類;
- 實現(xiàn)類依賴接口或抽象類。
更加精簡的定義就是“面向接口編程”——OOD(Object-Oriented Design,面向?qū)ο笤O(shè) 計)的精髓之一。
舉個例子:
司機開動奔馳車:
這樣設(shè)計的話突然來個寶馬,司機沒有對應(yīng)開寶馬的方法,就不能執(zhí)行了。
司機類和奔馳車類之間是緊耦合的關(guān)系,其導致的結(jié)果就是系統(tǒng)的可維護性大大降低。
對上面的例子進行優(yōu)化,引入依賴倒置 原則后的類圖如圖3-2所示
建立兩個接口:IDriver和ICar,分別定義了司機和汽車的各個職能,司機就是駕駛汽 車,必須實現(xiàn)drive()方法
在業(yè)務(wù)場景中,我們貫徹“抽象不應(yīng)該依賴細節(jié)”,也就是我們認為抽象(ICar接口)不 依賴BMW和Benz兩個實現(xiàn)類(細節(jié)),因此在高層次的模塊中應(yīng)用都是抽象
代碼如下:
public interface ICar {
//是汽車就應(yīng)該能跑
public void run();
}
public class Benz implements ICar{
//汽車肯定會跑
public void run(){
System.out.println("奔馳汽車開始運行...");
}
}
public class BMW implements ICar{
//寶馬車當然也可以開動了
public void run(){
System.out.println("寶馬汽車開始運行...");
}
}
public class Driver implements IDriver{
//司機的主要職責就是駕駛汽車
public void drive(ICar car){
car.run();
}
}
- 接口和抽象類都是屬于抽象的,有了抽象才可能依賴倒置。
- 變量的表面類型盡量是接口或者是抽象類
講了這么多,估計大家對“倒置”這個詞還是有點不理解,那到底什么是“倒置”呢?我們 先說“正置”是什么意思,依賴正置就是類間的依賴是實實在在的實現(xiàn)類間的依賴,也就是面 向?qū)崿F(xiàn)編程,這也是正常人的思維方式,我要開奔馳車就依賴奔馳車,我要使用筆記本電腦 就直接依賴筆記本電腦,而編寫程序需要的是對現(xiàn)實世界的事物進行抽象,抽象的結(jié)果就是 有了抽象類和接口,然后我們根據(jù)系統(tǒng)設(shè)計的需要產(chǎn)生了抽象間的依賴,代替了人們傳統(tǒng)思 維中的事物間的依賴,“倒置”就是從這里產(chǎn)生的。
接口隔離原則---(定義接口規(guī)范)
- 客戶端不應(yīng)該依 賴它不需要的接口
依賴它需要的接口,客 戶端需要什么接口就提供什么接口,把不需要的接口剔除掉,那就需要對接口進行細化,保 證其純潔性
- 類間的依賴關(guān)系應(yīng)該建立在最小的接口上
它要求是最小 的接口,也是要求接口細化,接口純潔,與第一個定義如出一轍,只是一個事物的兩種不同 描述。
總結(jié)上面兩句話:
- 建立單一接口,不要建立臃腫龐大的接口。再通 俗一點講:接口盡量細化,同時接口中的方法盡量少。
** 看到這里大家有可能要疑惑了,這與 單一職責原則不是相同的嗎?錯,接口隔離原則與單一職責的審視角度是不相同的,單一職 責要求的是類和接口職責單一,注重的是職責,這是業(yè)務(wù)邏輯上的劃分,而接口隔離原則要 求接口的方法盡量少。例如一個接口的職責可能包含10個方法,這10個方法都放在一個接口 中,并且提供給多個模塊訪問,各個模塊按照規(guī)定的權(quán)限來訪問,在系統(tǒng)外通過文檔約 束“不使用的方法不要訪問”,按照單一職責原則是允許的,按照接口隔離原則是不允許的, 因為它要求“盡量使用多個專門的接口”。專門的接口指什么?就是指提供給每個模塊的都應(yīng) 該是單一接口,提供給幾個模塊就應(yīng)該有幾個接口,而不是建立一個龐大的臃腫的接口,容 納所有的客戶端訪問。**
實際應(yīng)用上面就是一個接口里面不要寫太多方法,如果確實需要很多方法的話,應(yīng)該盡量根據(jù)實際需求進行拆分,拆分成多個接口,按需實現(xiàn)。
例如:對美女的定義 面貌、身材和氣質(zhì) 定義了如下接口
然而每個人的審美觀不同,并不是所有的人都認為美女都是這三種條件的
比如唐朝 身材就認為胖點的好
所以接口要進行拆分,按需進行實現(xiàn)
接口是我們設(shè)計時對外 提供的契約,通過分散定義多個接口,可以預防未來變更的擴散,提高系統(tǒng)的靈活性和可維 護性。
根據(jù)接口隔離原則拆分接口時,首先必須滿足單一職責原則。
迪米特法則---(低耦合)
對類的低耦合提出了明確的要求
- 只和朋友交流
老師想讓體育委員確認一下全班女生來齊沒有,就對他 說:“你去把全班女生清一下。
場景類:
首先確定Teacher類有幾個朋友類,它僅有一個朋友類—— GroupLeader。為什么Girl不是朋友類呢?Teacher也對它產(chǎn)生了依賴關(guān)系呀!朋友類的定義是 這樣的:出現(xiàn)在成員變量、方法的輸入輸出參數(shù)中的類稱為成員朋友類,而出現(xiàn)在方法體內(nèi) 部的類不屬于朋友類,而Girl這個類就是出現(xiàn)在commond方法體內(nèi),因此不屬于Teacher類的 朋友類。迪米特法則告訴我們一個類只和朋友類交流,但是我們剛剛定義的commond方法卻 與Girl類有了交流,聲明了一個List動態(tài)數(shù)組,也就是與一個陌生的類Girl有了交流, 這樣就破壞了Teacher的健壯性。方法是類的一個行為,類竟然不知道自己的行為與其他類 產(chǎn)生依賴關(guān)系,這是不允許的,嚴重違反了迪米特法則。
所以應(yīng)該修改調(diào)整一下:
場景類:
對程序進行了簡單的修改,把Teacher中對List的初始化移動到了場景類中,同時 在GroupLeader中增加了對Girl的注入,避開了Teacher類對陌生類Girl的訪問,降低了系統(tǒng)間 的耦合,提高了系統(tǒng)的健壯性。
- 迪米特法則要求類“羞澀”一點,盡量不要對外公布太多的public方法和非靜態(tài)的 public變量,盡量內(nèi)斂,多使用private、package-private、protected等訪問權(quán)限。
比如說把大象裝冰箱需要三步,任何一步失敗都會導致接下來的動作無法執(zhí)行,我們應(yīng)該封裝一個把大象裝冰箱的方法(涵蓋這三步)開放出去,而不應(yīng)該把這三步都開放出去。
- 迪米特法則的核心觀念就是類間解耦,弱耦合,只有弱耦合了以后,類的復用率才可以 提高。其要求的結(jié)果就是產(chǎn)生了大量的中轉(zhuǎn)或跳轉(zhuǎn)類,導致系統(tǒng)的復雜性提高,同時也為維 護帶來了難度。讀者在采用迪米特法則時需要反復權(quán)衡,既做到讓結(jié)構(gòu)清晰,又做到高內(nèi)聚 低耦合。
開閉原則---(開放擴展,關(guān)閉修改)
- 定義:一個軟件實體如類、模塊和函數(shù)應(yīng)該對擴展開放,對修改關(guān)閉。
開閉原則的定義已經(jīng)非常明確地告訴我們:軟件實體應(yīng)該對擴展開放,對修改關(guān)閉,其 含義是說一個軟件實體應(yīng)該通過擴展來實現(xiàn)變化,而不是通過修改已有的代碼來實現(xiàn)變化
- 比如說原來賣書是原件賣,現(xiàn)在突然要打折了,如果改變原有邏輯可能會導致出現(xiàn)各種問題,工作量也大,應(yīng)該找一種安全的方案小范圍改動,比如繼承
- 再比如說,平常工作遇到一個類需要增加個新功能,在原有類的基礎(chǔ)上改動一下就可以解決,當時覺著沒問題,后來可能就會引發(fā)一系列問題,因為這個類已經(jīng)被很多其他類引用著,直接修改的話非常容易引起一些意想不到的問題。這時候就應(yīng)該使用擴展方式來實現(xiàn)我們想要的新功能,比如可以新增方法,或者使用繼承,這樣擴展,既可以保證我們對新需求的實現(xiàn),又可以避免直接修改方法帶來的一系列問題。