Reference
設(shè)計模式之六大原則
作者:海子
出處:http://www.cnblogs.com/dolphin0520/
本博客中未標(biāo)明轉(zhuǎn)載的文章歸作者海子和博客園共有,歡迎轉(zhuǎn)載,但未經(jīng)作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接笤喳,否則保留追究法律責(zé)任的權(quán)利站蝠。
單一職責(zé)原則 Single Responsibility Principle, SRP
單一職責(zé)原則是最簡單的面向?qū)ο笤O(shè)計原則校赤,它用于控制類的粒度大小倒戏。
單一職責(zé)原則是實現(xiàn)高內(nèi)聚、低耦合的指導(dǎo)方針癌佩,它是最簡單但又最難運用的原則木缝,需要設(shè)計人員發(fā)現(xiàn)類的不同職責(zé)并將其分離,而發(fā)現(xiàn)類的多重職責(zé)需要設(shè)計人員具有較強(qiáng)的分析設(shè)計能力和相關(guān)實踐經(jīng)驗围辙。
定義
不要存在多于一個導(dǎo)致類變更的原因我碟。通俗的說,即一個類只負(fù)責(zé)一項職責(zé) / 一個類只負(fù)責(zé)一個功能領(lǐng)域中的相應(yīng)職責(zé)姚建,或者可以定義為:就一個類而言矫俺,應(yīng)該只有一個引起它變化的原因 / 應(yīng)該有且僅有一個原因引起類的變更
問題由來
在軟件系統(tǒng)中,一個類(大到模塊掸冤,小到方法)承擔(dān)的職責(zé)越多厘托,它被復(fù)用的可能性就越小,而且一個類承擔(dān)的職責(zé)過多稿湿,就相當(dāng)于將這些職責(zé)耦合在一起铅匹,當(dāng)其中一個職責(zé)變化時,可能會影響其他職責(zé)的運作饺藤。
類T負(fù)責(zé)兩個不同的職責(zé):職責(zé)P1包斑,職責(zé)P2。當(dāng)由于職責(zé)P1需求發(fā)生改變而需要修改類T時涕俗,有可能會導(dǎo)致原本運行正常的職責(zé)P2功能發(fā)生故障舰始。
解決方案
降低類與類之間的耦合。
將職責(zé)進(jìn)行分離咽袜,將不同的職責(zé)封裝在不同的類中丸卷,即將不同的變化原因封裝在不同的類中,如果多個職責(zé)總是同時發(fā)生改變則可將它們封裝在同一類中询刹。
分析
到單一職責(zé)原則谜嫉,很多人都會不屑一顧。因為它太簡單了凹联。稍有經(jīng)驗的程序員即使從來沒有讀過設(shè)計模式沐兰、從來沒有聽說過單一職責(zé)原則,在設(shè)計軟件時也會自覺的遵守這一重要原則蔽挠,因為這是常識住闯。在軟件編程中,誰也不希望因為修改了一個功能導(dǎo)致其他的功能發(fā)生故障澳淑。而避免出現(xiàn)這一問題的方法便是遵循單一職責(zé)原則比原。雖然單一職責(zé)原則如此簡單,并且被認(rèn)為是常識杠巡,但是即便是經(jīng)驗豐富的程序員寫出的程序量窘,也會有違背這一原則的代碼存在。為什么會出現(xiàn)這種現(xiàn)象呢氢拥?因為有職責(zé)擴(kuò)散蚌铜。所謂職責(zé)擴(kuò)散锨侯,就是因為某種原因,職責(zé)P被分化為粒度更細(xì)的職責(zé)P1和P2冬殃。
比如:類T只負(fù)責(zé)一個職責(zé)P囚痴,這樣設(shè)計是符合單一職責(zé)原則的。后來由于某種原因审葬,也許是需求變更了渡讼,也許是程序的設(shè)計者境界提高了,需要將職責(zé)P細(xì)分為粒度更細(xì)的職責(zé)P1耳璧,P2成箫,這時如果要使程序遵循單一職責(zé)原則,需要將類T也分解為兩個類T1和T2旨枯,分別負(fù)責(zé)P1蹬昌、P2兩個職責(zé)。但是在程序已經(jīng)寫好的情況下攀隔,這樣做簡直太費時間了皂贩。所以,簡單的修改類T昆汹,用它來負(fù)責(zé)兩個職責(zé)是一個比較不錯的選擇明刷,雖然這樣做有悖于單一職責(zé)原則。(這樣做的風(fēng)險在于職責(zé)擴(kuò)散的不確定性满粗,因為我們不會想到這個職責(zé)P辈末,在未來可能會擴(kuò)散為P1,P2映皆,P3挤聘,P4……Pn。所以記住捅彻,在職責(zé)擴(kuò)散到我們無法控制的程度之前组去,立刻對代碼進(jìn)行重構(gòu)。)
例子
重構(gòu)步淹,職責(zé)分解
Sunny軟件公司開發(fā)人員針對某CRM(Customer Relationship Management从隆,客戶關(guān)系管理)系統(tǒng)中客戶信息圖形統(tǒng)計模塊提出了如圖所示初始設(shè)計方案:
CustomerDataChart類中的方法說明如下:getConnection()方法用于連接數(shù)據(jù)庫,findCustomers()用于查詢所有的客戶信息缭裆,createChart()用于創(chuàng)建圖表键闺,displayChart()用于顯示圖表。
CustomerDataChart類承擔(dān)了太多的職責(zé)幼驶,既包含與數(shù)據(jù)庫相關(guān)的方法艾杏,又包含與圖表生成和顯示相關(guān)的方法。如果在其他類中也需要連接數(shù)據(jù)庫或者使用findCustomers()方法查詢客戶信息盅藻,則難以實現(xiàn)代碼的重用购桑。無論是修改數(shù)據(jù)庫連接方式還是修改圖表顯示方式都需要修改該類,它不止一個引起它變化的原因氏淑,違背了單一職責(zé)原則勃蜘。因此需要對該類進(jìn)行拆分,使其滿足單一職責(zé)原則假残,類CustomerDataChart可拆分為如下三個類:
- DBUtil:負(fù)責(zé)連接數(shù)據(jù)庫缭贡,包含數(shù)據(jù)庫連接方法getConnection();
- CustomerDAO:負(fù)責(zé)操作數(shù)據(jù)庫中的Customer表辉懒,包含對Customer表的增刪改查等方法阳惹,如findCustomers();
- CustomerDataChart:負(fù)責(zé)圖表的生成和顯示眶俩,包含方法createChart()和displayChart()莹汤。
現(xiàn)使用單一職責(zé)原則對其進(jìn)行重構(gòu)。
職責(zé)擴(kuò)散
class Animal{
public void breathe(String animal){
System.out.println(animal+"呼吸空氣");
}
}
public class Client{
public static void main(String[] args){
Animal animal = new Animal();
animal.breathe("牛");
animal.breathe("羊");
animal.breathe("豬");
}
}
運行結(jié)果:
牛呼吸空氣
羊呼吸空氣
豬呼吸空氣
程序上線后颠印,發(fā)現(xiàn)問題了纲岭,并不是所有的動物都呼吸空氣的,比如魚就是呼吸水的线罕。修改時如果遵循單一職責(zé)原則止潮,需要將Animal類細(xì)分為陸生動物類Terrestrial,水生動物Aquatic钞楼,代碼如下:
class Terrestrial{
public void breathe(String animal){
System.out.println(animal+"呼吸空氣");
}
}
class Aquatic{
public void breathe(String animal){
System.out.println(animal+"呼吸水");
}
}
public class Client{
public static void main(String[] args){
Terrestrial terrestrial = new Terrestrial();
terrestrial.breathe("牛");
terrestrial.breathe("羊");
terrestrial.breathe("豬");
Aquatic aquatic = new Aquatic();
aquatic.breathe("魚");
}
}
運行結(jié)果:
牛呼吸空氣
羊呼吸空氣
豬呼吸空氣
魚呼吸水
我們會發(fā)現(xiàn)如果這樣修改花銷是很大的喇闸,除了將原來的類分解之外,還需要修改客戶端询件。而直接修改類Animal來達(dá)成目的雖然違背了單一職責(zé)原則仅偎,但花銷卻小的多,代碼如下:
class Animal{
public void breathe(String animal){
if("魚".equals(animal)){
System.out.println(animal+"呼吸水");
}else{
System.out.println(animal+"呼吸空氣");
}
}
}
public class Client{
public static void main(String[] args){
Animal animal = new Animal();
animal.breathe("牛");
animal.breathe("羊");
animal.breathe("豬");
animal.breathe("魚");
}
}
可以看到雳殊,這種修改方式要簡單的多橘沥。但是卻存在著隱患:有一天需要將魚分為呼吸淡水的魚和呼吸海水的魚,則又需要修改Animal類的breathe方法夯秃,而對原有代碼的修改會對調(diào)用"豬""牛""羊"等相關(guān)功能帶來風(fēng)險座咆,也許某一天你會發(fā)現(xiàn)程序運行的結(jié)果變?yōu)?牛呼吸水"了。這種修改方式直接在代碼級別上違背了單一職責(zé)原則仓洼,雖然修改起來最簡單介陶,但隱患卻是最大的。還有一種修改方式:
class Animal{
public void breathe(String animal){
System.out.println(animal+"呼吸空氣");
}
public void breathe2(String animal){
System.out.println(animal+"呼吸水");
}
}
public class Client{
public static void main(String[] args){
Animal animal = new Animal();
animal.breathe("牛");
animal.breathe("羊");
animal.breathe("豬");
animal.breathe2("魚");
}
}
可以看到色建,這種修改方式?jīng)]有改動原來的方法哺呜,而是在類中新加了一個方法,這樣雖然也違背了單一職責(zé)原則箕戳,但在方法級別上卻是符合單一職責(zé)原則的某残,因為它并沒有動原來方法的代碼国撵。這三種方式各有優(yōu)缺點,那么在實際編程中玻墅,采用哪一中呢介牙?其實這真的比較難說,需要根據(jù)實際情況來確定澳厢。我的原則是:
只有邏輯足夠簡單环础,才可以在代碼級別上「添加
if - else
」違反單一職責(zé)原則;只有類中方法數(shù)量足夠少剩拢,才可以在方法級別上「添加function
」違反單一職責(zé)原則线得;
例如本文所舉的這個例子,它太簡單了徐伐,它只有一個方法贯钩,所以,無論是在代碼級別上違反單一職責(zé)原則呵晨,還是在方法級別上違反魏保,都不會造成太大的影響。實際應(yīng)用中的類都要復(fù)雜的多摸屠,一旦發(fā)生職責(zé)擴(kuò)散而需要修改類時谓罗,除非這個類本身非常簡單,否則還是遵循單一職責(zé)原則的好季二。
優(yōu)點
- 可以降低類的復(fù)雜度檩咱,一個類只負(fù)責(zé)一項職責(zé),其邏輯肯定要比負(fù)責(zé)多項職責(zé)簡單的多胯舷;
- 提高類的可讀性刻蚯,提高系統(tǒng)的可維護(hù)性;
- 變更引起的風(fēng)險降低桑嘶,變更是必然的炊汹,如果單一職責(zé)原則遵守的好,當(dāng)修改一個功能時逃顶,可以顯著降低對其他功能的影響讨便。
需要說明的一點是單一職責(zé)原則不只是面向?qū)ο缶幊趟枷胨赜械模灰悄K化的程序設(shè)計以政,都適用單一職責(zé)原則霸褒。
需注意
單一職責(zé)原則提出了一個編寫程序的標(biāo)準(zhǔn),用“職責(zé)”或“變化原因”來衡量接口或類設(shè)計得是否優(yōu)良盈蛮,但是“職責(zé)”和“變化原因”都是不可以度量的废菱,因項目和環(huán)境而異。
開閉原則 Open-Closed Principle, OCP
開閉原則是面向?qū)ο蟮目蓮?fù)用設(shè)計的第一塊基石,它是最重要的面向?qū)ο笤O(shè)計原則殊轴。
定義
一個軟件實體(如類衰倦、模塊和函數(shù))應(yīng)當(dāng)對擴(kuò)展開放,對修改關(guān)閉梳凛。即軟件實體應(yīng)盡量在不修改原有代碼的情況下進(jìn)行擴(kuò)展耿币。
軟件實體可以指一個軟件模塊梳杏、一個由多個類組成的局部結(jié)構(gòu)或一個獨立的類韧拒。
盡量通過擴(kuò)展軟件實體來解決需求變化,而不是通過修改已有的代碼來完成變化
問題由來
任何軟件都需要面臨一個很重要的問題十性,即它們的需求會隨時間的推移而發(fā)生變化叛溢。當(dāng)軟件系統(tǒng)需要面對新的需求時,我們應(yīng)該盡量保證系統(tǒng)的設(shè)計框架是穩(wěn)定的劲适。
在軟件的生命周期內(nèi)楷掉,因為變化、升級和維護(hù)等原因需要對軟件原有代碼進(jìn)行修改時霞势,可能會給舊代碼中引入錯誤烹植,也可能會使我們不得不對整個功能進(jìn)行重構(gòu),并且需要原有代碼經(jīng)過重新測試愕贡。
如果一個軟件設(shè)計符合開閉原則草雕,那么可以非常方便地對系統(tǒng)進(jìn)行擴(kuò)展,而且在擴(kuò)展時無須修改現(xiàn)有代碼固以,使得軟件系統(tǒng)在擁有適應(yīng)性和靈活性的同時具備較好的穩(wěn)定性和延續(xù)性墩虹。隨著軟件規(guī)模越來越大,軟件壽命越來越長憨琳,軟件維護(hù)成本越來越高诫钓,設(shè)計滿足開閉原則的軟件系統(tǒng)也變得越來越重要。
解決方案
用抽象構(gòu)建框架篙螟,用實現(xiàn)擴(kuò)展細(xì)節(jié)菌湃。
為了滿足開閉原則,需要對系統(tǒng)進(jìn)行抽象化設(shè)計遍略,抽象化是開閉原則的關(guān)鍵惧所。可以為系統(tǒng)定義一個相對穩(wěn)定的抽象層墅冷,而將不同的實現(xiàn)行為移至具體的實現(xiàn)層中完成纯路。在很多面向?qū)ο缶幊陶Z言中都提供了接口、抽象類等機(jī)制寞忿,可以通過它們定義系統(tǒng)的抽象層驰唬,再通過具體類來進(jìn)行擴(kuò)展。如果需要修改系統(tǒng)的行為,無須對抽象層進(jìn)行任何改動叫编,只需要增加新的具體類來實現(xiàn)新的業(yè)務(wù)功能即可辖佣,實現(xiàn)在不修改已有代碼的基礎(chǔ)上擴(kuò)展系統(tǒng)的功能,達(dá)到開閉原則的要求搓逾。
分析
抽象化是開閉原則的關(guān)鍵卷谈。
開閉原則是面向?qū)ο笤O(shè)計中最基礎(chǔ)的設(shè)計原則,它指導(dǎo)我們?nèi)绾谓⒎€(wěn)定靈活的系統(tǒng)霞篡。
開閉原則可能是設(shè)計模式六項原則中定義最模糊的一個了世蔗,它只告訴我們對擴(kuò)展開放,對修改關(guān)閉朗兵,可是到底如何才能做到對擴(kuò)展開放污淋,對修改關(guān)閉,并沒有明確的告訴我們余掖。以前寸爆,如果有人告訴我"你進(jìn)行設(shè)計的時候一定要遵守開閉原則",我會覺的他什么都沒說盐欺,但貌似又什么都說了赁豆。因為開閉原則真的太虛了。
其實冗美,遵循設(shè)計模式其他5大原則魔种,以及使用23種設(shè)計模式的目的就是遵循開閉原則。也就是說墩衙,只要我們其他5項原則遵守的好了务嫡,設(shè)計出的軟件自然是符合開閉原則的,這個開閉原則更像是其他五項原則遵守程度的"平均得分"漆改,其他5項原則遵守的好心铃,平均分自然就高,說明軟件設(shè)計開閉原則遵守的好挫剑;如果其他5項原則遵守的不好去扣,則說明開閉原則遵守的不好。
開閉原則無非就是想表達(dá)這樣一層意思:用抽象構(gòu)建框架樊破,用實現(xiàn)擴(kuò)展細(xì)節(jié)愉棱。因為抽象靈活性好,適應(yīng)性廣哲戚,只要抽象的合理奔滑,可以基本保持軟件架構(gòu)的穩(wěn)定。而軟件中易變的細(xì)節(jié)顺少,我們用從抽象派生的實現(xiàn)類來進(jìn)行擴(kuò)展朋其,當(dāng)軟件需要發(fā)生變化時王浴,我們只需要根據(jù)需求重新派生一個實現(xiàn)類來擴(kuò)展就可以了。當(dāng)然前提是我們的抽象要合理梅猿,要對需求的變更有前瞻性和預(yù)見性才行氓辣。
說到這里,再回想一下其他說的5項原則袱蚓,恰恰是告訴我們用抽象構(gòu)建框架钞啸,用實現(xiàn)擴(kuò)展細(xì)節(jié)的注意事項而已:單一職責(zé)原則告訴我們實現(xiàn)類要職責(zé)單一;里氏替換原則告訴我們不要破壞繼承體系喇潘;依賴倒置原則告訴我們要面向接口編程体斩;接口隔離原則告訴我們在設(shè)計接口的時候要精簡單一;迪米特法則告訴我們要降低耦合响蓉。而開閉原則是總綱硕勿,他告訴我們要對擴(kuò)展開放哨毁,對修改關(guān)閉枫甲。
最后說明一下如何去遵守這六個原則。對這六個原則的遵守并不是是和否的問題扼褪,而是多和少的問題想幻,也就是說,我們一般不會說有沒有遵守话浇,而是說遵守程度的多少脏毯。 任何事都是過猶不及,設(shè)計模式的六個設(shè)計原則也是一樣幔崖,制定這六個原則的目的并不是要我們刻板的遵守他們食店,而需要根據(jù)實際情況靈活運用。對他們的遵守程度只要在一個合理的范圍內(nèi)赏寇,就算是良好的設(shè)計吉嫩。我們用一幅圖來說明一下。
圖中的每一條維度各代表一項原則嗅定,我們依據(jù)對這項原則的遵守程度在維度上畫一個點自娩,則如果對這項原則遵守的合理的話,這個點應(yīng)該落在紅色的同心圓內(nèi)部渠退;如果遵守的差忙迁,點將會在小圓內(nèi)部;如果過度遵守碎乃,點將會落在大圓外部姊扔。一個良好的設(shè)計體現(xiàn)在圖中,應(yīng)該是六個頂點都在同心圓中的六邊形梅誓。
在上圖中恰梢,設(shè)計1晨川、設(shè)計2屬于良好的設(shè)計,他們對六項原則的遵守程度都在合理的范圍內(nèi)删豺;設(shè)計3共虑、設(shè)計4設(shè)計雖然有些不足,但也基本可以接受呀页;設(shè)計5則嚴(yán)重不足妈拌,對各項原則都沒有很好的遵守;而設(shè)計6則遵守過渡了蓬蝶,設(shè)計5和設(shè)計6都是迫切需要重構(gòu)的設(shè)計尘分。
到這里,設(shè)計模式的六大原則就寫完了丸氛。主要參考書籍有《設(shè)計模式》《設(shè)計模式之禪》《大話設(shè)計模式》以及網(wǎng)上一些零散的文章培愁,但主要內(nèi)容主要還是我本人對這六個原則的感悟。寫出來的目的一方面是對這六項原則系統(tǒng)地整理一下缓窜,一方面也與廣大的網(wǎng)友分享定续,因為設(shè)計模式對編程人員來說,的確非常重要禾锤。正如有句話叫做一千個讀者眼中有一千個哈姆雷特私股,如果大家對這六項原則的理解跟我有所不同,歡迎留言恩掷,大家共同探討倡鲸。
例子
Sunny軟件公司開發(fā)的CRM系統(tǒng)可以顯示各種類型的圖表,如餅狀圖和柱狀圖等黄娘,為了支持多種圖表顯示方式峭状,原始設(shè)計方案如圖所示:
在ChartDisplay類的display()方法中存在如下代碼片段:
if (type.equals("pie")) {
PieChart chart = new PieChart();
chart.display();
}
else if (type.equals("bar")) {
BarChart chart = new BarChart();
chart.display();
}
在該代碼中,如果需要增加一個新的圖表類逼争,如折線圖LineChart优床,則需要修改ChartDisplay類的display()方法的源代碼,增加新的判斷邏輯氮凝,違反了開閉原則羔巢。
現(xiàn)對該系統(tǒng)進(jìn)行重構(gòu),使之符合開閉原則罩阵。
在本實例中竿秆,由于在ChartDisplay類的display()方法中針對每一個圖表類編程,因此增加新的圖表類不得不修改源代碼稿壁∮母郑可以通過抽象化的方式對系統(tǒng)進(jìn)行重構(gòu),使之增加新的圖表類時無須修改源代碼傅是,滿足開閉原則匪燕。
使用依賴倒置(原則)蕾羊, 構(gòu)造注入
具體做法如下:
- 增加一個抽象圖表類AbstractChart,將各種具體圖表類作為其子類帽驯;
- ChartDisplay類針對抽象圖表類進(jìn)行編程龟再,由客戶端來決定使用哪種具體圖表。
我們引入了抽象圖表類AbstractChart尼变,且ChartDisplay針對抽象圖表類進(jìn)行編程利凑,并通過setChart()方法由客戶端來設(shè)置實例化的具體圖表對象,在ChartDisplay的display()方法中調(diào)用chart對象的display()方法顯示圖表嫌术。如果需要增加一種新的圖表哀澈,如折線圖LineChart,只需要將LineChart也作為AbstractChart的子類度气,在客戶端向ChartDisplay中注入一個LineChart對象即可割按,無須修改現(xiàn)有類庫的源代碼。