一,、單一職責(zé)原則(Single Responsibility Principle)
定義:一個(gè)類(lèi)只負(fù)責(zé)一個(gè)功能領(lǐng)域中的相應(yīng)職責(zé),或者可以定義為:就一個(gè)類(lèi)而言,,應(yīng)該只有一個(gè)引起它變化的原因,。
問(wèn)題由來(lái):類(lèi)T負(fù)責(zé)兩個(gè)不同的職責(zé):職責(zé)P1,職責(zé)P2,。當(dāng)由于職責(zé)P1需求發(fā)生改變而需要修改類(lèi)T時(shí),,有
可能會(huì)導(dǎo)致原本運(yùn)行正常的職責(zé)P2功能發(fā)生故障。
單一職責(zé)原則告訴我們:一個(gè)類(lèi)不能太“累”,!在軟件系統(tǒng)中,,一個(gè)類(lèi)(大到模塊,小到方法)承擔(dān)的職責(zé)越多,,它被復(fù)用的可能性就越小,,而且一個(gè)類(lèi)承擔(dān)的職責(zé)過(guò)多,就相當(dāng)于將這些職責(zé)耦合在一起,,當(dāng)其中一個(gè)職責(zé)變化時(shí),,可能會(huì)影響其他職責(zé)的運(yùn)作,因此要將這些職責(zé)進(jìn)行分離,,將不同的職責(zé)封裝在不同的類(lèi)中,,即將不同的變化原因封裝在不同的類(lèi)中,如果多個(gè)職責(zé)總是同時(shí)發(fā)生改變則可將它們封裝在同一類(lèi)中,。
單一職責(zé)原則是實(shí)現(xiàn)高內(nèi)聚,、低耦合的指導(dǎo)方針,它是最簡(jiǎn)單但又最難運(yùn)用的原則,,需要設(shè)計(jì)人員發(fā)現(xiàn)類(lèi)的不同職責(zé)并將其分離,,而發(fā)現(xiàn)類(lèi)的多重職責(zé)需要設(shè)計(jì)人員具有較強(qiáng)的分析設(shè)計(jì)能力和相關(guān)實(shí)踐經(jīng)驗(yàn)。
SRP在類(lèi)或接口中的使用:
/**
上面的的類(lèi)圖對(duì)應(yīng)的接口入下
*/
public interface IPhone{
//撥通電話
public void dial(String phoneNumber);
//通話
public void chat(Object o);
//掛斷電話
public void hangup();
}
在看到這個(gè)接口的時(shí)候,,我們都會(huì)認(rèn)為這樣的設(shè)計(jì)是沒(méi)有問(wèn)題的,,撥通電話,通話,,掛斷電話寫(xiě)在同一個(gè)接口里面并沒(méi)有什么錯(cuò),。但是,我們仔細(xì)分析,,這個(gè)接口真的沒(méi)有問(wèn)題嗎,?單一職責(zé)原則要求一個(gè)接口或類(lèi)只有一個(gè)原因引起變化,也就是說(shuō)一個(gè)接口或一個(gè)類(lèi)只有一個(gè)原則,,它就只負(fù)責(zé)一件事,。 但我們分析上面這個(gè)接口,卻發(fā)現(xiàn)它包含了兩個(gè)職責(zé):一個(gè)時(shí)協(xié)議管理,,一個(gè)是數(shù)據(jù)傳送,。dial()和hangup()兩個(gè)方法實(shí)現(xiàn)的是協(xié)議管理,,分別是撥通電話和掛機(jī)。chat()實(shí)現(xiàn)的是數(shù)據(jù)傳送,,把我們說(shuō)的話轉(zhuǎn)換成模擬信號(hào)或數(shù)字信號(hào)傳遞給對(duì)方,,然后再把對(duì)方傳遞過(guò)來(lái)的信號(hào)還原成我們聽(tīng)得懂的語(yǔ)言。這里的協(xié)議接通和數(shù)據(jù)傳送的變化都會(huì)引起該接口或?qū)崿F(xiàn)類(lèi)的變化,。我們想一想,,這兩個(gè)職責(zé)會(huì)相互影響嗎?不管是什么協(xié)議,,協(xié)議接通只負(fù)責(zé)將電話接通就行,,而數(shù)據(jù)傳輸只需要傳輸數(shù)據(jù),不必要去管協(xié)議是如何接通的,。所以通過(guò)分析,,IPhone接口包含了兩個(gè)職責(zé),而且這兩個(gè)職責(zé)的變化不互相影響,,這就可以考慮分成兩個(gè)接口,。
籠統(tǒng)地講:是否需要拆分取決于變化:
當(dāng)變化發(fā)生,只影響其中一個(gè)職責(zé),,那就需要拆分
如果變化都影響到這兩個(gè)職責(zé),,那就不需要拆分。
SRP也適用于方法:
其實(shí),,單一職責(zé)原則不僅適用于類(lèi),,接口,同樣適用于方法中,。這要舉一個(gè)例子了,,比如我們做項(xiàng)目的時(shí)候會(huì)遇到修改用戶信息這樣的功能模塊,我們一般的想法是將用戶的所有數(shù)據(jù)都接收過(guò)來(lái),,比如用戶名,,信息,密碼,,家庭地址等等,,然后統(tǒng)一封裝到一個(gè)User對(duì)象中提交到數(shù)據(jù)庫(kù),我們一般都是這么干的,,就如下面這樣:
其實(shí)這樣的方法是不可取的,因?yàn)槁氊?zé)不明確,,方法不明確,,你到底是要修改密碼,還是修改用戶名,,還是修改地址,,還是都要修改,?這樣職責(zé)不明確的話在與其他項(xiàng)目成員溝通的時(shí)候會(huì)產(chǎn)生很多麻煩,正確的設(shè)計(jì)如下:
循單一職責(zé)原的優(yōu)點(diǎn)有:
1.可以降低類(lèi)的復(fù)雜度,,一個(gè)類(lèi)只負(fù)責(zé)一項(xiàng)職責(zé),,其邏輯肯定要比負(fù)責(zé)多項(xiàng)職責(zé)簡(jiǎn)單的多;
2. 提高類(lèi)的可讀性,,提高系統(tǒng)的可維護(hù)性,;
3.變更引起的風(fēng)險(xiǎn)降低,變更是必然的,,如果單一職責(zé)原則遵守的好,,當(dāng)修改一個(gè)功能時(shí),可以顯著降低對(duì)其他功能的影響,。
二.開(kāi)閉原則(Open-Closed Principle, OCP)
定義:一個(gè)軟件實(shí)體應(yīng)當(dāng)對(duì)擴(kuò)展開(kāi)放,,對(duì)修改關(guān)閉。即軟件實(shí)體應(yīng)盡量在不修改原有代碼的情況下進(jìn)行擴(kuò)展
問(wèn)題由來(lái):任何軟件都需要面臨一個(gè)很重要的問(wèn)題,,即它們的需求會(huì)隨時(shí)間的推移而發(fā)生變化,。因?yàn)樽兓?jí)和維護(hù)等原因,,如果需要對(duì)軟件原有代碼進(jìn)行修改,,可能會(huì)給舊代碼引入錯(cuò)誤,也有可能會(huì)使我們不得不對(duì)整個(gè)功能進(jìn)行重構(gòu),,并且需要原有代碼經(jīng)過(guò)重新測(cè)試,,所以當(dāng)軟件需要變化時(shí),盡量通過(guò)擴(kuò)展軟件實(shí)體的行為來(lái)實(shí)現(xiàn)變化,,而不是通過(guò)修改已有的代碼來(lái)實(shí)現(xiàn)使我們需要的,。
為了滿足開(kāi)閉原則,需要對(duì)系統(tǒng)進(jìn)行抽象化設(shè)計(jì),,抽象化是開(kāi)閉原則的關(guān)鍵,。在Java、C#等編程語(yǔ)言中,,可以為系統(tǒng)定義一個(gè)相對(duì)穩(wěn)定的抽象層,,而將不同的實(shí)現(xiàn)行為移至具體的實(shí)現(xiàn)層中完成。如果需要修改系統(tǒng)的行為,,無(wú)須對(duì)抽象層進(jìn)行任何改動(dòng),,只需要增加新的具體類(lèi)來(lái)實(shí)現(xiàn)新的業(yè)務(wù)功能即可,實(shí)現(xiàn)在不修改已有代碼的基礎(chǔ)上擴(kuò)展系統(tǒng)的功能,,達(dá)到開(kāi)閉原則的要求,。
舉例:
實(shí)現(xiàn)畫(huà)圖表的功能,如餅狀圖和柱狀圖等,為了支持多種圖表顯示方式,,原始設(shè)計(jì)方案如圖下圖所示:
在ChartDisplay類(lèi)的display()方法中存在如下代碼片段:
......
if (type.equals("pie")) {
PieChart chart = new PieChart();
chart.display();
}
else if (type.equals("bar")) {
BarChart chart = new BarChart();
chart.display();
}
......
在該代碼中,,如果需要增加一個(gè)新的圖表類(lèi),如折線圖LineChart,,則需要修改ChartDisplay類(lèi)的display()方法的源代碼,,增加新的判斷邏輯,違反了開(kāi)閉原則,。
現(xiàn)對(duì)該系統(tǒng)進(jìn)行重構(gòu),,使之符合開(kāi)閉原則。
(1) 增加一個(gè)抽象圖表類(lèi)AbstractChart,,將各種具體圖表類(lèi)作為其子類(lèi),;
(2) ChartDisplay類(lèi)針對(duì)抽象圖表類(lèi)進(jìn)行編程,由客戶端來(lái)決定使用哪種具體圖表,。
重構(gòu)后結(jié)構(gòu)如圖2所示:
我們引入了抽象圖表類(lèi)AbstractChart,,且ChartDisplay針對(duì)抽象圖表類(lèi)進(jìn)行編程,并通過(guò)setChart()方法由客戶端來(lái)設(shè)置實(shí)例化的具體圖表對(duì)象,,在ChartDisplay的display()方法中調(diào)用chart對(duì)象的display()方法顯示圖表,。如果需要增加一種新的圖表,如折線圖LineChart,,只需要將LineChart也作為AbstractChart的子類(lèi),,在客戶端向ChartDisplay中注入一個(gè)LineChart對(duì)象即可,無(wú)須修改現(xiàn)有類(lèi)庫(kù)的源代碼,。
為什么使用開(kāi)閉原則:
第一:開(kāi)閉原則非常有名,,只要是面向?qū)ο缶幊蹋陂_(kāi)發(fā)時(shí)都會(huì)強(qiáng)調(diào)開(kāi)閉原則
第二:開(kāi)閉原則是最基礎(chǔ)的設(shè)計(jì)原則,,其它的五個(gè)設(shè)計(jì)原則都是開(kāi)閉原則的具體形態(tài),,也就是說(shuō)其它的五個(gè)設(shè)計(jì)原則是指導(dǎo)設(shè)計(jì)的工具和方法,而開(kāi)閉原則才是其精神領(lǐng)袖,。依照J(rèn)ava語(yǔ)言的稱(chēng)謂,,開(kāi)閉原則是抽象類(lèi),而其它的五個(gè)原則是具體的實(shí)現(xiàn)類(lèi),。
第三:開(kāi)閉原則可以提高復(fù)用性
在面向?qū)ο蟮脑O(shè)計(jì)中,,所有的邏輯都是從原子邏輯組合而來(lái),而不是在一個(gè)類(lèi)中獨(dú)立實(shí)現(xiàn)一套業(yè)務(wù)邏輯,。只有這樣的代碼才可以復(fù)用,,邏輯粒度越小,被復(fù)用的可能性越大,。為什么要復(fù)用呢,?復(fù)用可以減少代碼的重復(fù),,避免相同的邏輯分散在多個(gè)角落,,減少維護(hù)人員的工作量以及系統(tǒng)變化時(shí)產(chǎn)生bug的機(jī)會(huì),。怎么才能提高復(fù)用率呢?設(shè)計(jì)者需要縮小邏輯粒度,,直到一個(gè)邏輯不可以分為止,。
第四:開(kāi)閉原則可以提高維護(hù)性
一款軟件量產(chǎn)后,維護(hù)人員的工作不僅僅對(duì)數(shù)據(jù)進(jìn)行維護(hù),,還可能要對(duì)程序進(jìn)行擴(kuò)展,,維護(hù)人員最樂(lè)意的事是擴(kuò)展一個(gè)類(lèi),而不是修改一個(gè)類(lèi),。讓維護(hù)人員讀懂原有代碼,,再進(jìn)行修改,是一件非常痛苦的事情,,不要讓他在原有的代碼海洋中游蕩后再修改,,那是對(duì)維護(hù)人員的折磨和摧殘。
第五:面向?qū)ο箝_(kāi)發(fā)的要求
萬(wàn)物皆對(duì)象,,我們要把所有的事物抽象成對(duì)象,,然后針對(duì)對(duì)象進(jìn)行操作,但是萬(wàn)物皆發(fā)展變化,,有變化就要有策略去應(yīng)對(duì),,怎么快速應(yīng)對(duì)呢?這就需要在設(shè)計(jì)之初考慮到盡可能多變化的因素,,然后留下接口,,等待“可能”轉(zhuǎn)變?yōu)?ldquo;現(xiàn)實(shí)”。
如何使用開(kāi)閉原則
第一:抽象約束
抽象是對(duì)一組事物的通用描述,,沒(méi)有具體的實(shí)現(xiàn),,也就表示它可以有非常多的可能性,可以跟隨需求的變化而變化,。因此,,通過(guò)接口或抽象類(lèi)可以約束一組可能變化的行為,并且能夠?qū)崿F(xiàn)對(duì)擴(kuò)展開(kāi)放,,其包含三層含義:
1.通過(guò)接口或抽象類(lèi)約束擴(kuò)散,,對(duì)擴(kuò)展進(jìn)行邊界限定,不允許出現(xiàn)在接口或抽象類(lèi)中不存在的public方法,。
2.參數(shù)類(lèi)型,,引用對(duì)象盡量使用接口或抽象類(lèi),而不是實(shí)現(xiàn)類(lèi),,這主要是實(shí)現(xiàn)里氏替換原則的一個(gè)要求
3.抽象層盡量保持穩(wěn)定,,一旦確定就不要修改
第二:元數(shù)據(jù)(metadata)控件模塊行為
編程是一個(gè)很苦很累的活,,那怎么才能減輕壓力呢?答案是盡量使用元數(shù)據(jù)來(lái)控制程序的行為,,減少重復(fù)開(kāi)發(fā),。什么是元數(shù)據(jù)?用來(lái)描述環(huán)境和數(shù)據(jù)的數(shù)據(jù),,通俗的說(shuō)就是配置參數(shù),,參數(shù)可以從文件中獲得,也可以從數(shù)據(jù)庫(kù)中獲得,。
第三:制定項(xiàng)目章程
在一個(gè)團(tuán)隊(duì)中,,建立項(xiàng)目章程是非常重要的,因?yàn)檎鲁淌撬虚_(kāi)發(fā)人員都必須遵守的約定,,對(duì)項(xiàng)目來(lái)說(shuō),,約定優(yōu)于配置。這比通過(guò)接口或抽象類(lèi)進(jìn)行約束效率更高,,而擴(kuò)展性一點(diǎn)也沒(méi)有減少
第四:封裝變化
對(duì)變化封裝包含兩層含義:
(1)將相同的變化封裝到一個(gè)接口或抽象類(lèi)中
(2)將不同的變化封裝到不同的接口或抽象類(lèi)中,,不應(yīng)該有兩個(gè)不同的變化出現(xiàn)在同一個(gè)接口或抽象類(lèi)中。 封裝變化,,也就是受保護(hù)的變化,,找出預(yù)計(jì)有變化或不穩(wěn)定的點(diǎn),我們?yōu)檫@些變化點(diǎn)創(chuàng)建穩(wěn)定的接口,。
三,、里氏代換原則(Liskov Substitution Principle, LSP)
定義:里氏代換原則(Liskov Substitution Principle, LSP):所有引用基類(lèi)(父類(lèi))的地方必須能透明地使用其子類(lèi)的對(duì)象。
繼承優(yōu)點(diǎn)
代碼共享,,減少創(chuàng)建類(lèi)的工作量,,每個(gè)子類(lèi)都擁有父類(lèi)的方法和屬性;提高代碼的重用性,;子類(lèi)可以形似父類(lèi),,但又異于父類(lèi);提高代碼的可擴(kuò)展性,,實(shí)現(xiàn)父類(lèi)的方法就可以“為所欲為”了;提高產(chǎn)品或項(xiàng)目的開(kāi)放性,。
繼承缺點(diǎn)
繼承是侵入性的。只要繼承,,就必須擁有父類(lèi)的所有屬性和方法,;降低代碼的靈活性。子類(lèi)必須擁有父類(lèi)的屬性和方法,;
增強(qiáng)了耦合性,。當(dāng)父類(lèi)的常量、變量和方法被修改時(shí),,必需要考慮子類(lèi)的修改,,而且在缺乏規(guī)范的環(huán)境下,,這種修改可能帶來(lái)非常糟糕的結(jié)果;大片的代碼需要重構(gòu),。
克服繼承的缺點(diǎn)——里氏替換原則
從整體上來(lái)看,,利大于弊。
里氏代換原則告訴我們,,在軟件中將一個(gè)基類(lèi)對(duì)象替換成它的子類(lèi)對(duì)象,,程序?qū)⒉粫?huì)產(chǎn)生任何錯(cuò)誤和異常,反過(guò)來(lái)則不成立,,如果一個(gè)軟件實(shí)體使用的是一個(gè)子類(lèi)對(duì)象的話,那么它不一定能夠使用基類(lèi)對(duì)象,。例如:我喜歡動(dòng)物,,那我一定喜歡狗,因?yàn)楣肥莿?dòng)物的子類(lèi),;但是我喜歡狗,,不能據(jù)此斷定我喜歡動(dòng)物,因?yàn)槲也⒉幌矚g老鼠,,雖然它也是動(dòng)物,。
里氏代換原則是實(shí)現(xiàn)開(kāi)閉原則的重要方式之一,由于使用基類(lèi)對(duì)象的地方都可以使用子類(lèi)對(duì)象,,因此在程序中盡量使用基類(lèi)類(lèi)型來(lái)對(duì)對(duì)象進(jìn)行定義,,而在運(yùn)行時(shí)再確定其子類(lèi)類(lèi)型,用子類(lèi)對(duì)象來(lái)替換父類(lèi)對(duì)象,。
在使用里氏代換原則時(shí)需要注意如下幾個(gè)問(wèn)題:
(1)子類(lèi)的所有方法必須在父類(lèi)中聲明,,或子類(lèi)必須實(shí)現(xiàn)父類(lèi)中聲明的所有方法。根據(jù)里氏代換原則,,為了保證系統(tǒng)的擴(kuò)展性,,在程序中通常使用父類(lèi)來(lái)進(jìn)行定義,如果一個(gè)方法只存在子類(lèi)中,,在父類(lèi)中不提供相應(yīng)的聲明,,則無(wú)法在以父類(lèi)定義的對(duì)象中使用該方法。
(2) 我們?cè)谶\(yùn)用里氏代換原則時(shí),,盡量把父類(lèi)設(shè)計(jì)為抽象類(lèi)或者接口,,讓子類(lèi)繼承父類(lèi)或?qū)崿F(xiàn)父接口,并實(shí)現(xiàn)在父類(lèi)中聲明的方法,,運(yùn)行時(shí),,子類(lèi)實(shí)例替換父類(lèi)實(shí)例,我們可以很方便地?cái)U(kuò)展系統(tǒng)的功能,,同時(shí)無(wú)須修改原有子類(lèi)的代碼,,增加新的功能可以通過(guò)增加一個(gè)新的子類(lèi)來(lái)實(shí)現(xiàn),。里氏代換原則是開(kāi)閉原則的具體實(shí)現(xiàn)手段之一。
(3) Java語(yǔ)言中,,在編譯階段,,Java編譯器會(huì)檢查一個(gè)程序是否符合里氏代換原則,這是一個(gè)與實(shí)現(xiàn)無(wú)關(guān)的,、純語(yǔ)法意義上的檢查,,但Java編譯器的檢查是有局限的
系統(tǒng)需要提供一個(gè)發(fā)送Email的功能,客戶(Customer)可以分為VIP客戶(VIPCustomer)和普通客戶(CommonCustomer)兩類(lèi),,原始設(shè)計(jì)方案如圖所示:
在對(duì)系統(tǒng)進(jìn)行進(jìn)一步分析后發(fā)現(xiàn),,無(wú)論是普通客戶還是VIP客戶,發(fā)送郵件的過(guò)程都是相同的,,也就是說(shuō)兩個(gè)send()方法中的代碼重復(fù),,而且在本系統(tǒng)中還將增加新類(lèi)型的客戶。為了讓系統(tǒng)具有更好的擴(kuò)展性,,同時(shí)減少代碼重復(fù),,使用里氏代換原則對(duì)其進(jìn)行重構(gòu)。
在本實(shí)例中,,可以考慮增加一個(gè)新的抽象客戶類(lèi)Customer,,而將CommonCustomer和VIPCustomer類(lèi)作為其子類(lèi),郵件發(fā)送類(lèi)EmailSender類(lèi)針對(duì)抽象客戶類(lèi)Customer編程,,根據(jù)里氏代換原則,,能夠接受基類(lèi)對(duì)象的地方必然能夠接受子類(lèi)對(duì)象,因此將EmailSender中的send()方法的參數(shù)類(lèi)型改為Customer,,如果需要增加新類(lèi)型的客戶,,只需將其作為Customer類(lèi)的子類(lèi)即可。重構(gòu)后的結(jié)構(gòu)如圖所示:
里氏代換原則是實(shí)現(xiàn)開(kāi)閉原則的重要方式之一,。在本實(shí)例中,,在傳遞參數(shù)時(shí)使用基類(lèi)對(duì)象,除此以外,,在定義成員變量,、定義局部變量、確定方法返回類(lèi)型時(shí)都可使用里氏代換原則,。針對(duì)基類(lèi)編程,,在程序運(yùn)行時(shí)再確定具體子類(lèi)。
四,、依賴(lài)倒置原則(Dependence Inversion Principle,,DIP)
定義:
高層模塊不應(yīng)該依賴(lài)低層模塊,兩者都應(yīng)該依賴(lài)其抽象,;抽象不應(yīng)該依賴(lài)細(xì)節(jié),,細(xì)節(jié)應(yīng)該依賴(lài)抽象,,其核心思想是:要面向接口編程,不要面向?qū)崿F(xiàn)編程,。
依賴(lài)倒轉(zhuǎn)原則要求我們?cè)诔绦虼a中傳遞參數(shù)時(shí)或在關(guān)聯(lián)關(guān)系中,,盡量引用層次高的抽象層類(lèi),即使用接口和抽象類(lèi)進(jìn)行變量類(lèi)型聲明,、參數(shù)類(lèi)型聲明,、方法返回類(lèi)型聲明,以及數(shù)據(jù)類(lèi)型的轉(zhuǎn)換等,,而不要用具體類(lèi)來(lái)做這些事情,。為了確保該原則的應(yīng)用,一個(gè)具體類(lèi)應(yīng)當(dāng)只實(shí)現(xiàn)接口或抽象類(lèi)中聲明過(guò)的方法,,而不要給出多余的方法,,否則將無(wú)法調(diào)用到在子類(lèi)中增加的新方法。
在引入抽象層后,,系統(tǒng)將具有很好的靈活性,在程序中盡量使用抽象層進(jìn)行編程,,而將具體類(lèi)寫(xiě)在配置文件中,,這樣一來(lái),如果系統(tǒng)行為發(fā)生變化,,只需要對(duì)抽象層進(jìn)行擴(kuò)展,,并修改配置文件,而無(wú)須修改原有系統(tǒng)的源代碼,,在不修改的情況下來(lái)擴(kuò)展系統(tǒng)的功能,,滿足開(kāi)閉原則的要求。
在實(shí)現(xiàn)依賴(lài)倒轉(zhuǎn)原則時(shí),,我們需要針對(duì)抽象層編程,,而將具體類(lèi)的對(duì)象通過(guò)依賴(lài)注入(DependencyInjection, DI)的方式注入到其他對(duì)象中,依賴(lài)注入是指當(dāng)一個(gè)對(duì)象要與其他對(duì)象發(fā)生依賴(lài)關(guān)系時(shí),,通過(guò)抽象來(lái)注入所依賴(lài)的對(duì)象,。常用的注入方式有三種,分別是:構(gòu)造注入,,設(shè)值注入(Setter注入)和接口注入,。構(gòu)造注入是指通過(guò)構(gòu)造函數(shù)來(lái)傳入具體類(lèi)的對(duì)象,設(shè)值注入是指通過(guò)Setter方法來(lái)傳入具體類(lèi)的對(duì)象,,而接口注入是指通過(guò)在接口中聲明的業(yè)務(wù)方法來(lái)傳入具體類(lèi)的對(duì)象,。這些方法在定義時(shí)使用的是抽象類(lèi)型,在運(yùn)行時(shí)再傳入具體類(lèi)型的對(duì)象,,由子類(lèi)對(duì)象來(lái)覆蓋父類(lèi)對(duì)象,。
依賴(lài)倒置原則的作用
(1)依賴(lài)倒置原則可以降低類(lèi)間的耦合性,。
(2)依賴(lài)倒置原則可以提高系統(tǒng)的穩(wěn)定性。
(3)依賴(lài)倒置原則可以減少并行開(kāi)發(fā)引起的風(fēng)險(xiǎn),。
(4)依賴(lài)倒置原則可以提高代碼的可讀性和可維護(hù)性,。
依賴(lài)倒置原則的實(shí)現(xiàn)方法
依賴(lài)倒置原則的目的是通過(guò)要面向接口的編程來(lái)降低類(lèi)間的耦合性,所以我們?cè)趯?shí)際編程中只要遵循以下4點(diǎn),,就能在項(xiàng)目中滿足這個(gè)規(guī)則,。
(1)每個(gè)類(lèi)盡量提供接口或抽象類(lèi),或者兩者都具備,。
(2)變量的聲明類(lèi)型盡量是接口或者是抽象類(lèi),。
(3)任何類(lèi)都不應(yīng)該從具體類(lèi)派生。
(4)使用繼承時(shí)盡量遵循里氏替換原則
例子:
現(xiàn)需要將存儲(chǔ)在TXT或Excel文件中的客戶信息轉(zhuǎn)存到數(shù)據(jù)庫(kù)中,,因此需要進(jìn)行數(shù)據(jù)格式轉(zhuǎn)換,。在客戶數(shù)據(jù)操作類(lèi)中將調(diào)用數(shù)據(jù)格式轉(zhuǎn)換類(lèi)的方法實(shí)現(xiàn)格式轉(zhuǎn)換和數(shù)據(jù)庫(kù)插入操作,初始設(shè)計(jì)方案結(jié)構(gòu)如圖所示:
在編碼實(shí)現(xiàn)圖所示結(jié)構(gòu)時(shí),,Sunny軟件公司開(kāi)發(fā)人員發(fā)現(xiàn)該設(shè)計(jì)方案存在一個(gè)非常嚴(yán)重的問(wèn)題,,由于每次轉(zhuǎn)換數(shù)據(jù)時(shí)數(shù)據(jù)來(lái)源不一定相同,因此需要更換數(shù)據(jù)轉(zhuǎn)換類(lèi),,如有時(shí)候需要將TXTDataConvertor改為ExcelDataConvertor,,此時(shí),需要修改CustomerDAO的源代碼,,而且在引入并使用新的數(shù)據(jù)轉(zhuǎn)換類(lèi)時(shí)也不得不修改CustomerDAO的源代碼,,系統(tǒng)擴(kuò)展性較差,違反了開(kāi)閉原則,,現(xiàn)需要對(duì)該方案進(jìn)行重構(gòu),。
在本實(shí)例中,由于CustomerDAO針對(duì)具體數(shù)據(jù)轉(zhuǎn)換類(lèi)編程,,因此在增加新的數(shù)據(jù)轉(zhuǎn)換類(lèi)或者更換數(shù)據(jù)轉(zhuǎn)換類(lèi)時(shí)都不得不修改CustomerDAO的源代碼,。我們可以通過(guò)引入抽象數(shù)據(jù)轉(zhuǎn)換類(lèi)解決該問(wèn)題,在引入抽象數(shù)據(jù)轉(zhuǎn)換類(lèi)DataConvertor之后,,CustomerDAO針對(duì)抽象類(lèi)DataConvertor編程,,而將具體數(shù)據(jù)轉(zhuǎn)換類(lèi)名存儲(chǔ)在配置文件中,符合依賴(lài)倒轉(zhuǎn)原則,。根據(jù)里氏代換原則,,程序運(yùn)行時(shí),具體數(shù)據(jù)轉(zhuǎn)換類(lèi)對(duì)象將替換DataConvertor類(lèi)型的對(duì)象,,程序不會(huì)出現(xiàn)任何問(wèn)題,。更換具體數(shù)據(jù)轉(zhuǎn)換類(lèi)時(shí)無(wú)須修改源代碼,只需要修改配置文件;如果需要增加新的具體數(shù)據(jù)轉(zhuǎn)換類(lèi),,只要將新增數(shù)據(jù)轉(zhuǎn)換類(lèi)作為DataConvertor的子類(lèi)并修改配置文件即可,,原有代碼無(wú)須做任何修改,滿足開(kāi)閉原則,。重構(gòu)后的結(jié)構(gòu)如圖所示:
在上述重構(gòu)過(guò)程中,,我們使用了開(kāi)閉原則、里氏代換原則和依賴(lài)倒轉(zhuǎn)原則,,在大多數(shù)情況下,,這三個(gè)設(shè)計(jì)原則會(huì)同時(shí)出現(xiàn),開(kāi)閉原則是目標(biāo),,里氏代換原則是基礎(chǔ),,依賴(lài)倒轉(zhuǎn)原則是手段,它們相輔相成,,相互補(bǔ)充,,目標(biāo)一致,只是分析問(wèn)題時(shí)所站角度不同而已,。
五,、接口隔離原則(Interface Segregation Principle, ISP)
定義:使用多個(gè)專(zhuān)門(mén)的接口,而不使用單一的總接口,,即客戶端不應(yīng)該依賴(lài)那些它不需要的接口,。
根據(jù)接口隔離原則,當(dāng)一個(gè)接口太大時(shí),,我們需要將它分割成一些更細(xì)小的接口,使用該接口的客戶端僅需知道與之相關(guān)的方法即可,。每一個(gè)接口應(yīng)該承擔(dān)一種相對(duì)獨(dú)立的角色,,不干不該干的事,該干的事都要干,。這里的“接口”往往有兩種不同的含義:一種是指一個(gè)類(lèi)型所具有的方法特征的集合,,僅僅是一種邏輯上的抽象;另外一種是指某種語(yǔ)言具體的“接口”定義,,有嚴(yán)格的定義和結(jié)構(gòu),,比如Java語(yǔ)言中的interface。對(duì)于這兩種不同的含義,,ISP的表達(dá)方式以及含義都有所不同:
(1) 當(dāng)把“接口”理解成一個(gè)類(lèi)型所提供的所有方法特征的集合的時(shí)候,,這就是一種邏輯上的概念,接口的劃分將直接帶來(lái)類(lèi)型的劃分,??梢园呀涌诶斫獬山巧粋€(gè)接口只能代表一個(gè)角色,每個(gè)角色都有它特定的一個(gè)接口,,此時(shí),,這個(gè)原則可以叫做“角色隔離原則”。
(2) 如果把“接口”理解成狹義的特定語(yǔ)言的接口,,那么ISP表達(dá)的意思是指接口僅僅提供客戶端需要的行為,,客戶端不需要的行為則隱藏起來(lái),應(yīng)當(dāng)為客戶端提供盡可能小的單獨(dú)的接口,,而不要提供大的總接口,。在面向?qū)ο缶幊陶Z(yǔ)言中,實(shí)現(xiàn)一個(gè)接口就需要實(shí)現(xiàn)該接口中定義的所有方法,,因此大的總接口使用起來(lái)不一定很方便,,為了使接口的職責(zé)單一,需要將大接口中的方法根據(jù)其職責(zé)不同分別放在不同的小接口中,,以確保每個(gè)接口使用起來(lái)都較為方便,,并都承擔(dān)某一單一角色。接口應(yīng)該盡量細(xì)化,,同時(shí)接口中的方法應(yīng)該盡量少,,每個(gè)接口中只包含一個(gè)客戶端(如子模塊或業(yè)務(wù)邏輯類(lèi))所需的方法即可,這種機(jī)制也稱(chēng)為“定制服務(wù)”,,即為不同的客戶端提供寬窄不同的接口,。
接口隔離原則和單一職責(zé)都是為了提高類(lèi)的內(nèi)聚性、降低它們之間的耦合性,,體現(xiàn)了封裝的思想,,但兩者是不同的:
(1)單一職責(zé)原則注重的是職責(zé),而接口隔離原則注重的是對(duì)接口依賴(lài)的隔離,。
(2)單一職責(zé)原則主要是約束類(lèi),,它針對(duì)的是程序中的實(shí)現(xiàn)和細(xì)節(jié);接口隔離原則主要約束接口,,主要針對(duì)抽象和程序整體框架的構(gòu)建,。
接口隔離原則的優(yōu)點(diǎn)
接口隔離原則是為了約束接口、降低類(lèi)對(duì)接口的依賴(lài)性,,遵循接口隔離原則有以下 5 個(gè)優(yōu)點(diǎn),。
(1)將臃腫龐大的接口分解為多個(gè)粒度小的接口,可以預(yù)防外來(lái)變更的擴(kuò)散,,提高系統(tǒng)的靈活性和可維護(hù)性,。
(2)接口隔離提高了系統(tǒng)的內(nèi)聚性,減少了對(duì)外交互,,降低了系統(tǒng)的耦合性,。
(3)如果接口的粒度大小定義合理,能夠保證系統(tǒng)的穩(wěn)定性;但是,,如果定義過(guò)小,,則會(huì)造成接口數(shù)量過(guò)多,使設(shè)計(jì)復(fù)雜化,;如果定義太大,,靈活性降低,無(wú)法提供定制服務(wù),,給整體項(xiàng)目帶來(lái)無(wú)法預(yù)料的風(fēng)險(xiǎn),。
(4)使用多個(gè)專(zhuān)門(mén)的接口還能夠體現(xiàn)對(duì)象的層次,因?yàn)榭梢酝ㄟ^(guò)接口的繼承,,實(shí)現(xiàn)對(duì)總接口的定義,。
(5)能減少項(xiàng)目工程中的代碼冗余。過(guò)大的大接口里面通常放置許多不用的方法,,當(dāng)實(shí)現(xiàn)這個(gè)接口的時(shí)候,,被迫設(shè)計(jì)冗余的代碼。
接口隔離原則的實(shí)現(xiàn)方法
在具體應(yīng)用接口隔離原則時(shí),,應(yīng)該根據(jù)以下幾個(gè)規(guī)則來(lái)衡量,。
(1)接口盡量小,但是要有限度,。一個(gè)接口只服務(wù)于一個(gè)子模塊或業(yè)務(wù)邏輯,。
(2)為依賴(lài)接口的類(lèi)定制服務(wù)。只提供調(diào)用者需要的方法,,屏蔽不需要的方法,。
(3)了解環(huán)境,拒絕盲從,。每個(gè)項(xiàng)目或產(chǎn)品都有選定的環(huán)境因素,,環(huán)境不同,接口拆分的標(biāo)準(zhǔn)就不同深入了解業(yè)務(wù)邏輯,。
(4)提高內(nèi)聚,減少對(duì)外交互,。使接口用最少的方法去完成最多的事情,。
下面通過(guò)一個(gè)簡(jiǎn)單實(shí)例來(lái)加深對(duì)接口隔離原則的理解:
Sunny軟件公司開(kāi)發(fā)人員針對(duì)某CRM系統(tǒng)的客戶數(shù)據(jù)顯示模塊設(shè)計(jì)了如圖1所示接口,其中方法dataRead()用于從文件中讀取數(shù)據(jù),,方法transformToXML()用于將數(shù)據(jù)轉(zhuǎn)換成XML格式,,方法createChart()用于創(chuàng)建圖表,方法displayChart()用于顯示圖表,,方法createReport()用于創(chuàng)建文字報(bào)表,,方法displayReport()用于顯示文字報(bào)表。
在實(shí)際使用過(guò)程中發(fā)現(xiàn)該接口很不靈活,例如如果一個(gè)具體的數(shù)據(jù)顯示類(lèi)無(wú)須進(jìn)行數(shù)據(jù)轉(zhuǎn)換(源文件本身就是XML格式),,但由于實(shí)現(xiàn)了該接口,,將不得不實(shí)現(xiàn)其中聲明的transformToXML()方法(至少需要提供一個(gè)空實(shí)現(xiàn));如果需要?jiǎng)?chuàng)建和顯示圖表,,除了需實(shí)現(xiàn)與圖表相關(guān)的方法外,,還需要實(shí)現(xiàn)創(chuàng)建和顯示文字報(bào)表的方法,否則程序編譯時(shí)將報(bào)錯(cuò),。
現(xiàn)使用接口隔離原則對(duì)其進(jìn)行重構(gòu),。
在圖中,由于在接口CustomerDataDisplay中定義了太多方法,,即該接口承擔(dān)了太多職責(zé),,一方面導(dǎo)致該接口的實(shí)現(xiàn)類(lèi)很龐大,在不同的實(shí)現(xiàn)類(lèi)中都不得不實(shí)現(xiàn)接口中定義的所有方法,,靈活性較差,,如果出現(xiàn)大量的空方法,將導(dǎo)致系統(tǒng)中產(chǎn)生大量的無(wú)用代碼,,影響代碼質(zhì)量,;另一方面由于客戶端針對(duì)大接口編程,將在一定程序上破壞程序的封裝性,,客戶端看到了不應(yīng)該看到的方法,,沒(méi)有為客戶端定制接口。因此需要將該接口按照接口隔離原則和單一職責(zé)原則進(jìn)行重構(gòu),,將其中的一些方法封裝在不同的小接口中,,確保每一個(gè)接口使用起來(lái)都較為方便,并都承擔(dān)某一單一角色,,每個(gè)接口中只包含一個(gè)客戶端(如模塊或類(lèi))所需的方法即可,。
通過(guò)使用接口隔離原則,本實(shí)例重構(gòu)后的結(jié)構(gòu)如圖所示:
在使用接口隔離原則時(shí),,我們需要注意控制接口的粒度,,接口不能太小,如果太小會(huì)導(dǎo)致系統(tǒng)中接口泛濫,,不利于維護(hù),;接口也不能太大,太大的接口將違背接口隔離原則,,靈活性較差,,使用起來(lái)很不方便。一般而言,,接口中僅包含為某一類(lèi)用戶定制的方法即可,,不應(yīng)該強(qiáng)迫客戶依賴(lài)于那些它們不用的方法,。
六、迪米特法則(Law of Demeter, LoD)
定義:迪米特法則(Law of Demeter, LoD):一個(gè)軟件實(shí)體應(yīng)當(dāng)盡可能少地與其他實(shí)體發(fā)生相互作用,。
迪米特法則(Law of Demeter,,LoD)又叫作最少知識(shí)原則(Least Knowledge Principle,LKP),,產(chǎn)生于 1987 年美國(guó)東北大學(xué)(Northeastern University)的一個(gè)名為迪米特(Demeter)的研究項(xiàng)目,,由伊恩·荷蘭(Ian Holland)提出,被 UML 創(chuàng)始者之一的布奇(Booch)普及,,后來(lái)又因?yàn)樵诮?jīng)典著作《程序員修煉之道》(The Pragmatic Programmer)提及而廣為人知,。
迪米特法則的定義是:只與你的直接朋友交談,不跟“陌生人”說(shuō)話(Talk only to your immediate friends and not to strangers),。其含義是:如果兩個(gè)軟件實(shí)體無(wú)須直接通信,,那么就不應(yīng)當(dāng)發(fā)生直接的相互調(diào)用,可以通過(guò)第三方轉(zhuǎn)發(fā)該調(diào)用,。其目的是降低類(lèi)之間的耦合度,,提高模塊的相對(duì)獨(dú)立性。
迪米特法則中的“朋友”是指:當(dāng)前對(duì)象本身,、當(dāng)前對(duì)象的成員對(duì)象,、當(dāng)前對(duì)象所創(chuàng)建的對(duì)象、當(dāng)前對(duì)象的方法參數(shù)等,,這些對(duì)象同當(dāng)前對(duì)象存在關(guān)聯(lián),、聚合或組合關(guān)系,可以直接訪問(wèn)這些對(duì)象的方法,。
在應(yīng)用迪米特法則時(shí),,一個(gè)對(duì)象只能與直接朋友發(fā)生交互,不要與“陌生人”發(fā)生直接交互,,這樣做可以降低系統(tǒng)的耦合度,,一個(gè)對(duì)象的改變不會(huì)給太多其他對(duì)象帶來(lái)影響。
迪米特法則要求我們?cè)谠O(shè)計(jì)系統(tǒng)時(shí),,應(yīng)該盡量減少對(duì)象之間的交互,,如果兩個(gè)對(duì)象之間不必彼此直接通信,那么這兩個(gè)對(duì)象就不應(yīng)當(dāng)發(fā)生任何直接的相互作用,,如果其中的一個(gè)對(duì)象需要調(diào)用另一個(gè)對(duì)象的某一個(gè)方法的話,,可以通過(guò)第三者轉(zhuǎn)發(fā)這個(gè)調(diào)用。簡(jiǎn)言之,,就是通過(guò)引入一個(gè)合理的第三者來(lái)降低現(xiàn)有對(duì)象之間的耦合度。
在將迪米特法則運(yùn)用到系統(tǒng)設(shè)計(jì)中時(shí),,要注意下面的幾點(diǎn):在類(lèi)的劃分上,,應(yīng)當(dāng)盡量創(chuàng)建松耦合的類(lèi),,類(lèi)之間的耦合度越低,就越有利于復(fù)用,,一個(gè)處在松耦合中的類(lèi)一旦被修改,,不會(huì)對(duì)關(guān)聯(lián)的類(lèi)造成太大波及;在類(lèi)的結(jié)構(gòu)設(shè)計(jì)上,,每一個(gè)類(lèi)都應(yīng)當(dāng)盡量降低其成員變量和成員函數(shù)的訪問(wèn)權(quán)限,;在類(lèi)的設(shè)計(jì)上,只要有可能,,一個(gè)類(lèi)型應(yīng)當(dāng)設(shè)計(jì)成不變類(lèi),;在對(duì)其他類(lèi)的引用上,一個(gè)對(duì)象對(duì)其他對(duì)象的引用應(yīng)當(dāng)降到最低,。
迪米特法則的優(yōu)點(diǎn)
迪米特法則要求限制軟件實(shí)體之間通信的寬度和深度,,正確使用迪米特法則將有以下兩個(gè)優(yōu)點(diǎn)。
降低了類(lèi)之間的耦合度,,提高了模塊的相對(duì)獨(dú)立性,。
由于親合度降低,從而提高了類(lèi)的可復(fù)用率和系統(tǒng)的擴(kuò)展性,。
但是,,過(guò)度使用迪米特法則會(huì)使系統(tǒng)產(chǎn)生大量的中介類(lèi),從而增加系統(tǒng)的復(fù)雜性,,使模塊之間的通信效率降低,。所以,在釆用迪米特法則時(shí)需要反復(fù)權(quán)衡,,確保高內(nèi)聚和低耦合的同時(shí),,保證系統(tǒng)的結(jié)構(gòu)清晰
迪米特法則的實(shí)現(xiàn)方法
從迪米特法則的定義和特點(diǎn)可知,它強(qiáng)調(diào)以下兩點(diǎn):
從依賴(lài)者的角度來(lái)說(shuō),,只依賴(lài)應(yīng)該依賴(lài)的對(duì)象,。
從被依賴(lài)者的角度說(shuō),只暴露應(yīng)該暴露的方法,。
所以,,在運(yùn)用迪米特法則時(shí)要注意以下 6 點(diǎn)。
在類(lèi)的劃分上,,應(yīng)該創(chuàng)建弱耦合的類(lèi),。類(lèi)與類(lèi)之間的耦合越弱,就越有利于實(shí)現(xiàn)可復(fù)用的目標(biāo),。
在類(lèi)的結(jié)構(gòu)設(shè)計(jì)上,,盡量降低類(lèi)成員的訪問(wèn)權(quán)限。
在類(lèi)的設(shè)計(jì)上,,優(yōu)先考慮將一個(gè)類(lèi)設(shè)置成不變類(lèi),。
在對(duì)其他類(lèi)的引用上,,將引用其他對(duì)象的次數(shù)降到最低。
不暴露類(lèi)的屬性成員,,而應(yīng)該提供相應(yīng)的訪問(wèn)器(set 和 get 方法),。
謹(jǐn)慎使用序列化(Serializable)功能。
下面通過(guò)一個(gè)簡(jiǎn)單實(shí)例來(lái)加深對(duì)迪米特法則的理解:
Sunny軟件公司所開(kāi)發(fā)CRM系統(tǒng)包含很多業(yè)務(wù)操作窗口,,在這些窗口中,,某些界面控件之間存在復(fù)雜的交互關(guān)系,一個(gè)控件事件的觸發(fā)將導(dǎo)致多個(gè)其他界面控件產(chǎn)生響應(yīng),,例如,,當(dāng)一個(gè)按鈕(Button)被單擊時(shí),對(duì)應(yīng)的列表框(List),、組合框(ComboBox),、文本框(TextBox)、文本標(biāo)簽(Label)等都將發(fā)生改變,,在初始設(shè)計(jì)方案中,,界面控件之間的交互關(guān)系可簡(jiǎn)化為如圖所示結(jié)構(gòu):
在圖中,由于界面控件之間的交互關(guān)系復(fù)雜,,導(dǎo)致在該窗口中增加新的界面控件時(shí)需要修改與之交互的其他控件的源代碼,,系統(tǒng)擴(kuò)展性較差,也不便于增加和刪除新控件,。
現(xiàn)使用迪米特對(duì)其進(jìn)行重構(gòu),。
在本實(shí)例中,可以通過(guò)引入一個(gè)專(zhuān)門(mén)用于控制界面控件交互的中間類(lèi)(Mediator)來(lái)降低界面控件之間的耦合度,。引入中間類(lèi)之后,,界面控件之間不再發(fā)生直接引用,而是將請(qǐng)求先轉(zhuǎn)發(fā)給中間類(lèi),,再由中間類(lèi)來(lái)完成對(duì)其他控件的調(diào)用,。當(dāng)需要增加或刪除新的控件時(shí),只需修改中間類(lèi)即可,,無(wú)須修改新增控件或已有控件的源代碼,,重構(gòu)后結(jié)構(gòu)如圖所示:
總結(jié)
單一職責(zé)原則告訴我們實(shí)現(xiàn)類(lèi)要職責(zé)單一
里氏替換原則告訴我們不要破壞繼承體系
依賴(lài)倒置原則告訴我們要面向接口編程
接口隔離原則告訴我們?cè)谠O(shè)計(jì)接口的時(shí)候要精簡(jiǎn)單一
迪米特原則告訴我們要降低耦合
開(kāi)閉原則是總綱,告訴我們要對(duì)擴(kuò)展開(kāi)放,,對(duì)修改關(guān)閉