[TOC]
單一設計原則---(專人專事)
單一職責原則的定義是:應該有且僅有一個原因引起類的變更锦针。
優(yōu)化:
重新拆封成兩個接口窜锯,IUserBO負責用戶的屬性欠雌,簡單地說夭谤,IUserBO的職責就是收集和 反饋用戶的屬性信息港粱;IUserBiz負責用戶的行為,完成用戶信息的維護和變更呕臂。
簡單理解就是 get set 一起破托,其他操作封裝成biz(biz是Business的縮寫,實際上就是控制層(業(yè)務邏輯層))歧蒋,當然不局限于這種類型的對象炼团。
注意 單一職責原則提出了一個編寫程序的標準,用“職責”或“變化原因”來衡量接口或 類設計得是否優(yōu)良疏尿,但是“職責”和“變化原因”都是不可度量的,因項目而異易桃,因環(huán)境而異
單一職責適用于接口褥琐、類,同時也適用于方法晤郑,什么意思呢敌呈?一個方法盡可能做一件事 情,比如一個方法修改用戶密碼造寝,不要把這個方法放到“修改用戶信息”方法中磕洪,這個方法的 顆粒度很粗。
如果要修改用戶名稱诫龙,就調用changeUserName方法析显;要修改家庭地址, 就調用changeHomeAddress方法签赃;要修改單位電話谷异,就調用changeOfficeTel方法分尸。每個方法的 職責非常清晰明確,不僅開發(fā)簡單歹嘹,而且日后的維護也非常容易箩绍,大家可以逐漸養(yǎng)成這樣的 習慣。
這個單一原則重在理解尺上,不能認死理材蛛,拆分太嚴重也會導致類或者方法數(shù)過多,具體情況具體分析吧怎抛。
里氏替換原則---(繼承規(guī)范)
主要是為良好的繼承定義了一個規(guī)范卑吭。
這個規(guī)范就是:所有引用基類的地方必須能透明地使用其子類的對象(不會改變任何邏輯)
通俗點講,只要父類能出現(xiàn)的地方子類就可以出現(xiàn)抽诉,而且 替換為子類也不會產生任何錯誤或異常陨簇,使用者可能根本就不需要知道是父類還是子類。但 是迹淌,反過來就不行了河绽,有子類出現(xiàn)的地方,父類未必就能適應唉窃。
更正宗的定義:如果對每一個類型為S的對象o1耙饰,都有類型為T的對 象o2,使得以T定義的所有程序P在所有的對象o1都代換成o2時纹份,程序P的行為沒有發(fā)生變 化苟跪,那么類型S是類型T的子類型。
說明:這是給繼承定義的一種良好的規(guī)范蔓涧,現(xiàn)實中可能會出現(xiàn)不符合這種原則的代碼件已,所以這是規(guī)范,并不是所有都是這樣的元暴。
細分里氏替換原則四種含義:
- 子類必須完全實現(xiàn)父類的方法(這里的實現(xiàn)應該是要保證都有方法體的意思)
比如說父類是個槍篷扩,定義了一個方法“射擊”,那么任意子類(玩具槍茉盏、狙擊槍鉴未、步槍)都應該能調用射擊這個方法。 - 子類可以有自己的個性
當然子類可以增加一些方法或者復寫一些方法 - 覆蓋或實現(xiàn)父類的方法時輸入參數(shù)可以被放大
范例:
public class Father {
public Collection doSomething(HashMap map){
System.out.println("父類被執(zhí)行...");
return map.values();
}
}
public class Son extends Father {
//放大輸入參數(shù)類型
public Collection doSomething(Map map){
System.out.println("子類被執(zhí)行...");
return map.values();
}
}
請注意粗體部分鸠姨,與父類的方法名相同铜秆,但又不是覆寫(Override)父類的方法。你加 個@Override試試看讶迁,會報錯的连茧,為什么呢?方法名雖然相同,但方法的輸入參數(shù)不同梅屉,就 不是覆寫值纱,那這是什么呢?是重載(Overload)坯汤!不用大驚小怪的虐唠,不在一個類就不能是重 載了?繼承是什么意思惰聂,子類擁有父類的所有屬性和方法疆偿,方法名相同,輸入參數(shù)類型又不 相同搓幌,當然是重載了杆故。
父類使用場景:
public class Client {
public static void invoker(){
//父類存在的地方,子類就應該能夠存在
Father f = new Father();
HashMap map = new HashMap();
f.doSomething(map);
}
public static void main(String[] args) {
invoker();
}
}
運行結果:父類被執(zhí)行...
根據里氏替換原則在這里使用子類替換:
public class Client {
public static void invoker(){
//父類存在的地方溉愁,子類就應該能夠存在
Son f =new Son();
HashMap map = new HashMap();
f.doSomething(map);
}
public static void main(String[] args) {
invoker();
}
}
運行結果:父類被執(zhí)行...
運行結果還是一樣处铛,看明白是怎么回事了嗎?父類方法的輸入參數(shù)是HashMap類型拐揭,子 類的輸入參數(shù)是Map類型撤蟆,也就是說子類的輸入參數(shù)類型的范圍擴大了,子類代替父類傳遞 到調用者中堂污,子類的方法永遠都不會被執(zhí)行家肯。這是正確的,如果你想讓子類的方法運行盟猖,就 必須覆寫父類的方法讨衣。
對調參數(shù):
public class Father {
public Collection doSomething(Map map){
System.out.println("父類被執(zhí)行...");
return map.values();
}
}
public class Son extends Father {
//放大輸入參數(shù)類型
public Collection doSomething(HashMap map){
System.out.println("子類被執(zhí)行...");
return map.values();
}
}
如果依然使用上面的使用場景運行結果就會變成這樣:
運行結果:父類被執(zhí)行...
更換子類
運行結果:子類被執(zhí)行...
這就不正常了,子類在沒有覆寫父類的方法的前提下式镐,子類方法被執(zhí)行了反镇,這會引起業(yè)務 邏輯混亂 ,“歪曲”了父類的意圖娘汞,引起一堆意想不到的業(yè)務邏輯混亂愿险,所以子類中方法的前置條 件必須與超類中被覆寫的方法的前置條件相同或者更寬松。
- 覆寫或實現(xiàn)父類的方法時輸出結果可以被縮小
這是什么意思呢价说,父類的一個方法的返回值是一個類型F,子類的相同方法(重載或覆 寫)的返回值為S风秤,那么里氏替換原則就要求S必須小于等于F鳖目,也就是說,要么S和F是同一 個類型缤弦,要么S是F的子類领迈,為什么呢?分兩種情況,如果是覆寫狸捅,父類和子類的同名方法的 輸入參數(shù)是相同的衷蜓,兩個方法的范圍值S小于等于F,這是覆寫的要求尘喝,這才是重中之重磁浇,子 類覆寫父類的方法,天經地義朽褪。如果是重載置吓,則要求方法的輸入參數(shù)類型或數(shù)量不相同,在 里氏替換原則要求下缔赠,就是子類的輸入參數(shù)寬于或等于父類的輸入參數(shù)衍锚,也就是說你寫的這 個方法是不會被調用的,參考上面講的前置條件嗤堰。
綜合3戴质、4條可以總結一句話:子類入參類型可以放大范圍(可以是父入參的父類),輸出結果要縮小范圍(可是父出參的子類)踢匣。
理解起來比較困難告匠。
依賴倒置原則---(面向接口編程)
- 高層模塊不應該依賴低層模塊,兩者都應該依賴其抽象符糊;
- 抽象不應該依賴細節(jié)凫海;
- 細節(jié)應該依賴抽象。
高層模塊和低層模塊容易理解男娄,每一個邏輯的實現(xiàn)都是由原子邏輯組成的行贪,不可分割的 原子邏輯就是低層模塊,原子邏輯的再組裝就是高層模塊模闲。那什么是抽象建瘫?什么又是細節(jié) 呢?在Java語言中尸折,抽象就是指接口或抽象類啰脚,兩者都是不能直接被實例化的;細節(jié)就是實 現(xiàn)類实夹,實現(xiàn)接口或繼承抽象類而產生的類就是細節(jié)橄浓,其特點就是可以直接被實例化,也就是 可以加上一個關鍵字new產生一個對象亮航。
依賴倒置原則在Java語言中的表現(xiàn)就是: - 模塊間的依賴通過抽象發(fā)生荸实,實現(xiàn)類之間不發(fā)生直接的依賴關系,其依賴關系是通過 接口或抽象類產生的缴淋;
- 接口或抽象類不依賴于實現(xiàn)類准给;
- 實現(xiàn)類依賴接口或抽象類泄朴。
更加精簡的定義就是“面向接口編程”——OOD(Object-Oriented Design,面向對象設 計)的精髓之一露氮。
舉個例子:
司機開動奔馳車:
這樣設計的話突然來個寶馬祖灰,司機沒有對應開寶馬的方法,就不能執(zhí)行了畔规。
司機類和奔馳車類之間是緊耦合的關系局扶,其導致的結果就是系統(tǒng)的可維護性大大降低。
對上面的例子進行優(yōu)化油讯,引入依賴倒置 原則后的類圖如圖3-2所示
建立兩個接口:IDriver和ICar详民,分別定義了司機和汽車的各個職能,司機就是駕駛汽 車陌兑,必須實現(xiàn)drive()方法
在業(yè)務場景中沈跨,我們貫徹“抽象不應該依賴細節(jié)”,也就是我們認為抽象(ICar接口)不 依賴BMW和Benz兩個實現(xiàn)類(細節(jié))兔综,因此在高層次的模塊中應用都是抽象
代碼如下:
public interface ICar {
//是汽車就應該能跑
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)類間的依賴纠吴,也就是面 向實現(xiàn)編程,這也是正常人的思維方式慧瘤,我要開奔馳車就依賴奔馳車戴已,我要使用筆記本電腦 就直接依賴筆記本電腦,而編寫程序需要的是對現(xiàn)實世界的事物進行抽象锅减,抽象的結果就是 有了抽象類和接口糖儡,然后我們根據系統(tǒng)設計的需要產生了抽象間的依賴,代替了人們傳統(tǒng)思 維中的事物間的依賴怔匣,“倒置”就是從這里產生的握联。
接口隔離原則---(定義接口規(guī)范)
- 客戶端不應該依 賴它不需要的接口
依賴它需要的接口,客 戶端需要什么接口就提供什么接口每瞒,把不需要的接口剔除掉金闽,那就需要對接口進行細化,保 證其純潔性
- 類間的依賴關系應該建立在最小的接口上
它要求是最小 的接口剿骨,也是要求接口細化呐矾,接口純潔,與第一個定義如出一轍懦砂,只是一個事物的兩種不同 描述蜒犯。
總結上面兩句話:
- 建立單一接口,不要建立臃腫龐大的接口荞膘。再通 俗一點講:接口盡量細化罚随,同時接口中的方法盡量少。
** 看到這里大家有可能要疑惑了羽资,這與 單一職責原則不是相同的嗎淘菩?錯,接口隔離原則與單一職責的審視角度是不相同的屠升,單一職 責要求的是類和接口職責單一潮改,注重的是職責,這是業(yè)務邏輯上的劃分腹暖,而接口隔離原則要 求接口的方法盡量少汇在。例如一個接口的職責可能包含10個方法,這10個方法都放在一個接口 中脏答,并且提供給多個模塊訪問糕殉,各個模塊按照規(guī)定的權限來訪問,在系統(tǒng)外通過文檔約 束“不使用的方法不要訪問”殖告,按照單一職責原則是允許的阿蝶,按照接口隔離原則是不允許的, 因為它要求“盡量使用多個專門的接口”黄绩。專門的接口指什么羡洁?就是指提供給每個模塊的都應 該是單一接口,提供給幾個模塊就應該有幾個接口爽丹,而不是建立一個龐大的臃腫的接口筑煮,容 納所有的客戶端訪問。**
實際應用上面就是一個接口里面不要寫太多方法习劫,如果確實需要很多方法的話咆瘟,應該盡量根據實際需求進行拆分,拆分成多個接口诽里,按需實現(xiàn)袒餐。
例如:對美女的定義 面貌、身材和氣質 定義了如下接口
然而每個人的審美觀不同谤狡,并不是所有的人都認為美女都是這三種條件的
比如唐朝 身材就認為胖點的好
所以接口要進行拆分灸眼,按需進行實現(xiàn)
接口是我們設計時對外 提供的契約,通過分散定義多個接口墓懂,可以預防未來變更的擴散焰宣,提高系統(tǒng)的靈活性和可維 護性。
根據接口隔離原則拆分接口時捕仔,首先必須滿足單一職責原則匕积。
迪米特法則---(低耦合)
對類的低耦合提出了明確的要求
- 只和朋友交流
老師想讓體育委員確認一下全班女生來齊沒有盈罐,就對他 說:“你去把全班女生清一下。
場景類:
首先確定Teacher類有幾個朋友類闪唆,它僅有一個朋友類—— GroupLeader盅粪。為什么Girl不是朋友類呢?Teacher也對它產生了依賴關系呀悄蕾!朋友類的定義是 這樣的:出現(xiàn)在成員變量票顾、方法的輸入輸出參數(shù)中的類稱為成員朋友類,而出現(xiàn)在方法體內 部的類不屬于朋友類帆调,而Girl這個類就是出現(xiàn)在commond方法體內奠骄,因此不屬于Teacher類的 朋友類。迪米特法則告訴我們一個類只和朋友類交流番刊,但是我們剛剛定義的commond方法卻 與Girl類有了交流含鳞,聲明了一個List動態(tài)數(shù)組,也就是與一個陌生的類Girl有了交流撵枢, 這樣就破壞了Teacher的健壯性民晒。方法是類的一個行為,類竟然不知道自己的行為與其他類 產生依賴關系锄禽,這是不允許的潜必,嚴重違反了迪米特法則。
所以應該修改調整一下:
場景類:
對程序進行了簡單的修改沃但,把Teacher中對List的初始化移動到了場景類中磁滚,同時 在GroupLeader中增加了對Girl的注入,避開了Teacher類對陌生類Girl的訪問宵晚,降低了系統(tǒng)間 的耦合垂攘,提高了系統(tǒng)的健壯性。
- 迪米特法則要求類“羞澀”一點淤刃,盡量不要對外公布太多的public方法和非靜態(tài)的 public變量晒他,盡量內斂,多使用private逸贾、package-private陨仅、protected等訪問權限。
比如說把大象裝冰箱需要三步铝侵,任何一步失敗都會導致接下來的動作無法執(zhí)行灼伤,我們應該封裝一個把大象裝冰箱的方法(涵蓋這三步)開放出去,而不應該把這三步都開放出去咪鲜。
- 迪米特法則的核心觀念就是類間解耦狐赡,弱耦合,只有弱耦合了以后疟丙,類的復用率才可以 提高颖侄。其要求的結果就是產生了大量的中轉或跳轉類鸟雏,導致系統(tǒng)的復雜性提高,同時也為維 護帶來了難度览祖。讀者在采用迪米特法則時需要反復權衡崔慧,既做到讓結構清晰,又做到高內聚 低耦合穴墅。
開閉原則---(開放擴展,關閉修改)
- 定義:一個軟件實體如類温自、模塊和函數(shù)應該對擴展開放玄货,對修改關閉。
開閉原則的定義已經非常明確地告訴我們:軟件實體應該對擴展開放悼泌,對修改關閉松捉,其 含義是說一個軟件實體應該通過擴展來實現(xiàn)變化,而不是通過修改已有的代碼來實現(xiàn)變化
- 比如說原來賣書是原件賣馆里,現(xiàn)在突然要打折了隘世,如果改變原有邏輯可能會導致出現(xiàn)各種問題,工作量也大鸠踪,應該找一種安全的方案小范圍改動丙者,比如繼承
- 再比如說,平常工作遇到一個類需要增加個新功能营密,在原有類的基礎上改動一下就可以解決械媒,當時覺著沒問題,后來可能就會引發(fā)一系列問題评汰,因為這個類已經被很多其他類引用著纷捞,直接修改的話非常容易引起一些意想不到的問題。這時候就應該使用擴展方式來實現(xiàn)我們想要的新功能被去,比如可以新增方法主儡,或者使用繼承,這樣擴展惨缆,既可以保證我們對新需求的實現(xiàn)糜值,又可以避免直接修改方法帶來的一系列問題。