接口设计六大原则
六大設(shè)計(jì)原則
六大設(shè)計(jì)原則主要是指:
- 單一職責(zé)原則(Single Responsibility Principle);
- 開(kāi)閉原則(Open Closed Principle);
- 里氏替換原則(Liskov Substitution Principle);
- 迪米特法則(Law of Demeter),又叫“最少知道法則”;
- 接口隔離原則(Interface Segregation Principle);
- 依賴倒置原則(Dependence Inversion Principle)。
把這 6 個(gè)原則的首字母(里氏替換原則和迪米特法則的首字母重復(fù),只取一個(gè))聯(lián)合起來(lái)就是:SOLID(穩(wěn)定的),其代表的含義也就是把這 6 個(gè)原則結(jié)合使用的好處:建立穩(wěn)定、靈活、健壯的設(shè)計(jì)。
?
?
單一職責(zé)原則
單一職責(zé)原則的定義是:應(yīng)該有且僅有一個(gè)原因引起類的變更。
沒(méi)錯(cuò),單一職責(zé)原則就這一句話,不懂沒(méi)關(guān)系,我們舉個(gè)例子。
我們以打電話為例,電話通話的時(shí)候有 4 個(gè)過(guò)程發(fā)生:撥號(hào)、通話、回應(yīng)、掛機(jī)。那我們寫(xiě)一個(gè)接口,類圖如下:
?
?
代碼為:
?
?
我們看這個(gè)接口有沒(méi)有問(wèn)題?相信大部分同學(xué)會(huì)覺(jué)得沒(méi)問(wèn)題,因?yàn)槠匠N覀兙褪沁@么寫(xiě)的。沒(méi)錯(cuò),這個(gè)接口接近于完美,注意,是“接近”。單一職責(zé)原則要求一個(gè)接口或一個(gè)類只能有一個(gè)原因引起變化,也就是一個(gè)接口或者類只能有一個(gè)職責(zé),它就負(fù)責(zé)一件事情,看看上面的接口只負(fù)責(zé)一件事情嗎?明顯不是。
IPhone這個(gè)接口包含了兩個(gè)職責(zé):協(xié)議管理和數(shù)據(jù)傳送。dial 和 hangup 這兩個(gè)方法實(shí)現(xiàn)的是協(xié)議管理,分別負(fù)責(zé)撥號(hào)接通和掛機(jī),chat 方法實(shí)現(xiàn)的是數(shù)據(jù)傳送。不管是協(xié)議接通的變化還是輸出傳送的變化,都會(huì)引起這個(gè)接口的變化。所以,IPhone這個(gè)接口并不符合單一職責(zé)原則。若要讓IPhone滿足單一職責(zé)原則,我們就要對(duì)其進(jìn)行拆分,拆分后的類圖如下:
?
?
這樣設(shè)計(jì)就完美了,一個(gè)類實(shí)現(xiàn)了兩個(gè)接口,把兩個(gè)職責(zé)融合在一個(gè)類中。你會(huì)覺(jué)得這個(gè)Phone有兩個(gè)原因引起變化了啊,是的,但是別忘了我們是面向接口編程,我們對(duì)外公布的是接口而不是實(shí)現(xiàn)類。
另外,單一職責(zé)原則不僅適用于接口和類,也適用于方法。一個(gè)方法盡可能只做一件事,比如一個(gè)修改用戶密碼的方法,不要把這個(gè)方法放到“修改用戶信息”方法中。
單一職責(zé)的好處
1. 類的復(fù)雜性降低,實(shí)現(xiàn)什么職責(zé)都有清晰明確的定義;
2. 可讀性高,復(fù)雜性降低,可讀性自然就提高了;
3. 可維護(hù)性提高,可讀性提高了,那自然更容易維護(hù)了;
4. 變更引起的風(fēng)險(xiǎn)降低,變更是必不可少的,如果接口的單一職責(zé)做得好,一個(gè)接口修改只對(duì)相應(yīng)的實(shí)現(xiàn)類有影響,對(duì)其他的接口無(wú)影響,這對(duì)系統(tǒng)的擴(kuò)展性、維護(hù)性都有非常大的幫助。
里氏替換原則
在面向?qū)ο蟮恼Z(yǔ)言中,繼承是必不可少的、非常優(yōu)秀的語(yǔ)言機(jī)制,它有如下優(yōu)點(diǎn):
有優(yōu)點(diǎn)就必然存在缺點(diǎn):
為了讓“利”的因素發(fā)揮最大的作用,同時(shí)減少“弊”帶來(lái)的麻煩,引入了里氏替換原則(LSP)。
歷史替換原則最正宗的定義是:如果對(duì)每一個(gè)類型為S的對(duì)象o1,都有類型為T的對(duì)象o2,使得以T定義的所有程序P在所有的對(duì)象o1都代替o2時(shí),程序P的行為沒(méi)有發(fā)生變化,那么類型S是類型T的子類型。
通俗點(diǎn)講,就是只要父類能出現(xiàn)的地方,子類就可以出現(xiàn),而且替換為子類也不會(huì)產(chǎn)生任何錯(cuò)誤或異常。
里氏替換原則為良好的繼承定義了一個(gè)規(guī)范,一句簡(jiǎn)單的定義包含了4層含義。
1. 子類必須完全實(shí)現(xiàn)父類的方法。
我們?cè)谧鱿到y(tǒng)設(shè)計(jì)的時(shí)候,經(jīng)常會(huì)定義一個(gè)接口或抽象類,然后編碼實(shí)現(xiàn),調(diào)用類則直接傳入接口或抽象類,其實(shí)這里就已經(jīng)使用了里氏替換原則。我們以打CS舉例,來(lái)描述一下里面用到的槍。類圖如下:
?
?
槍的主要職責(zé)是射擊,如何射擊在各個(gè)具體的子類中實(shí)現(xiàn),在士兵類Soldier中定義了一個(gè)方法 killEnemy,使用槍來(lái)kill敵人,具體用什么槍,調(diào)用的時(shí)候才知道。
AbstractGun類源碼如下:
?
?
手槍、步槍、機(jī)槍的實(shí)現(xiàn)類代碼如下:
?
?
?
?
?
?
士兵類的源碼為:
?
?
注意,士兵類的killEnemy方法中使用的gun是抽象的,具體時(shí)間什么槍需要由客戶端(Client)調(diào)用Soldier的構(gòu)造方法傳參確定。
客戶端Client源碼如下:
?
?
注意:在類中調(diào)用其他類時(shí)務(wù)必要使用父類或接口,如果不能使用父類或接口,則說(shuō)明類的設(shè)計(jì)已經(jīng)違背了LSP原則。
2. 孩子類可以有自己的個(gè)性。
孩子類當(dāng)然可以有自己的屬性和方法了,也正因如此,在子類出現(xiàn)的地方,父類未必就可以代替。
還是以上面的關(guān)于槍支的例子為例,步槍有 AK47、SKS狙擊步槍等型號(hào),把這兩個(gè)型號(hào)的槍引入后的Rifle的子類圖如下:
?
?
SKS狙擊步槍可以配一個(gè)8倍鏡進(jìn)行遠(yuǎn)程瞄準(zhǔn),相對(duì)于父類步槍,這就是SKS的個(gè)性。源碼如下:
?
?
狙擊手Snipper類的源碼如下:
?
?
狙擊手因?yàn)橹荒苁褂镁褤魳?#xff0c;所以,狙擊手類中持有的槍只能是狙擊類型的,如果換成父類步槍Rifle,則傳遞進(jìn)來(lái)的可能就不是狙擊槍,而是AK47了,而AK47是沒(méi)有zoomOut方法的,所以肯定是不行的。這也驗(yàn)證了里氏替換原則的那一句話:有子類出現(xiàn)的地方,父類未必就可以代替。
3. 覆蓋或?qū)崿F(xiàn)父類的方法時(shí),輸入?yún)?shù)可以被放大。
來(lái)看一個(gè)例子,我們先定義一個(gè)Father類:
?
?
然后定義一個(gè)子類:
?
?
子類方法與父類方法同名,但又不是覆寫(xiě)父類的方法。你加個(gè)@Override看看,會(huì)報(bào)錯(cuò)的。像這種方法名相同,方法參數(shù)不同,叫做方法的重載。你可能會(huì)有疑問(wèn):重載不是只能在當(dāng)前類內(nèi)部重載嗎?因?yàn)镾on繼承了Father,Son就有了Father的所有屬性和方法,自然就有了Father的doSomething這個(gè)方法,所以,這里就構(gòu)成了重載。
接下來(lái)看場(chǎng)景類:
?
?
根據(jù)里氏替換原則,父類出現(xiàn)的地方子類就可以出現(xiàn),我們把上面的父類替換為子類:
?
?
我們發(fā)現(xiàn)運(yùn)行結(jié)果是一樣的。為什么會(huì)這樣呢?因?yàn)樽宇怱on繼承了Father,就擁有了doSomething(HashMap map)這個(gè)方法,不過(guò)由于Son沒(méi)有重寫(xiě)這個(gè)方法,當(dāng)調(diào)用Son的這個(gè)方法的時(shí)候,就會(huì)自動(dòng)調(diào)用其父類的這個(gè)方法。所以兩次的結(jié)果是一致的。
舉個(gè)反例,如果父類的輸入?yún)?shù)類型大于子類的輸入?yún)?shù)類型,會(huì)出現(xiàn)什么問(wèn)題呢?我們直接看代碼執(zhí)行結(jié)果即可輕松看出問(wèn)題:
擴(kuò)大父類方法入?yún)?#xff1a;
?
?
縮小子類方法入?yún)?#xff1a;
?
?
場(chǎng)景類:
?
?
根據(jù)里氏替換原則,有父類的地方就可以有子類,我們把Father替換為Son看看結(jié)果:
?
?
兩次運(yùn)行結(jié)果不一致,違反了里氏替換原則,所以子類中方法的入?yún)㈩愋捅仨毰c父類中被覆寫(xiě)的方法的入?yún)㈩愋拖嗤蚋鼘捤伞?/strong>
4. 覆蓋或?qū)崿F(xiàn)父類的方法時(shí),輸出結(jié)果可以被縮小。
這句話的意思就是,父類的一個(gè)方法的返回值是類型T,子類的相同方法(重載或重寫(xiě))的返回值為類型S,那么里氏替換原則就要求S必須小于等于T。為什么呢?因?yàn)橹貙?xiě)父類方法,父類和子類的同名方法的輸入?yún)?shù)是相同的,兩個(gè)方法的范圍值S小于等于T,這時(shí)重寫(xiě)父類方法的要求。
依賴倒置原則
依賴倒置原則在Java語(yǔ)言中的表現(xiàn)是:
1. 模塊間的依賴通過(guò)抽象發(fā)生,實(shí)現(xiàn)類之間不直接發(fā)生依賴關(guān)系,其依賴關(guān)系是通過(guò)接口或抽象類產(chǎn)生的;
2. 接口或抽象類不依賴于實(shí)現(xiàn)類;
3. 實(shí)現(xiàn)類依賴接口或抽象類。
說(shuō)白了,就是“面向接口編程”。
依賴倒置原則可以減少類間的耦合性,提高系統(tǒng)的穩(wěn)定性,降低并行開(kāi)發(fā)引起的風(fēng)險(xiǎn),提高代碼的可讀性和可維護(hù)性。
我們以汽車和司機(jī)舉例,畫(huà)出類圖:
?
?
奔馳車源代碼:
?
?
司機(jī)源代碼:
?
?
客戶端源代碼:
?
?
通過(guò)以上的代碼,完成了司機(jī)開(kāi)動(dòng)奔馳車的場(chǎng)景。可以看到,這個(gè)場(chǎng)景并沒(méi)有引用依賴倒置原則,司機(jī)Driver類直接依賴奔馳車Benz類,這樣會(huì)有什么隱患呢?試想,后期業(yè)務(wù)變動(dòng),司機(jī)又買了一輛寶馬車,源代碼如下:
?
?
由于司機(jī)現(xiàn)在只有開(kāi)奔馳的方法,所以他是開(kāi)不了寶馬的。一個(gè)拿有C駕照的司機(jī)能開(kāi)奔馳,不能開(kāi)寶馬?太不合理了。所以,這就暴露出上面的設(shè)計(jì)問(wèn)題了。我們對(duì)上面的功能重新設(shè)計(jì),首先新建兩個(gè)接口。
汽車接口ICar:
?
?
司機(jī)接口IDriver:
?
?
IDriver中,通過(guò)傳入ICar接口實(shí)現(xiàn)了抽象之間的依賴關(guān)系。
接下來(lái)創(chuàng)建汽車實(shí)現(xiàn)類:奔馳和寶馬。
?
?
然后創(chuàng)建司機(jī)實(shí)現(xiàn)類:
?
?
最后是場(chǎng)景類調(diào)用:
?
?
Client屬于高層業(yè)務(wù)邏輯,它對(duì)低層模塊的依賴都建立在抽象上,driver的表面類型是IDriver,benz的表面類型是ICar。
依賴倒置原則的使用建議:
(1)每個(gè)類盡量都有接口或抽象類,或者接口和抽象類兩者都具備。
(2)變量的表面類型盡量是接口或抽象類。
(3)任何類都不應(yīng)該從具體類派生。
(4)盡量不要重寫(xiě)基類的方法。如果基類是一個(gè)抽象類,而且這個(gè)方法已經(jīng)實(shí)現(xiàn)了,子類盡量不要重寫(xiě)。
(5)結(jié)合里氏替換原則使用。
接口隔離原則
接口隔離原則就是客戶端不應(yīng)該依賴它不需要的接口,或者說(shuō)類間的依賴關(guān)系應(yīng)該建立在最小的接口上。
我們以搜索美女為例,設(shè)計(jì)了如下的類圖:
?
?
源代碼如下。美女及其實(shí)現(xiàn)類:
?
?
搜索程序及其子類源代碼如下:
?
?
最后是場(chǎng)景調(diào)用類:
?
?
上面實(shí)現(xiàn)了一個(gè)搜索美女的小程序。我們想象這個(gè)程序有沒(méi)有問(wèn)題?IPettyGirl接口是否做到了最優(yōu)化?并沒(méi)有。
每個(gè)人的審美觀不一樣,張三認(rèn)為顏值高就是美女,即使身材和氣質(zhì)一般;李四認(rèn)為身材好就行,不在乎顏值和氣質(zhì);而王五則認(rèn)為顏值和身材都是外在,只要有氣質(zhì),那就是美女。這時(shí),IPettyGirl接口就滿足不了了,因?yàn)镮PettyGirl的要求是顏值、身材、氣質(zhì)兼具才是美女。所以為了滿足各種人的口味,我們需要重新設(shè)計(jì)接口的結(jié)構(gòu)。把IPettyGirl拆分為3個(gè)接口,分別表示顏值高、身材好、氣質(zhì)佳。修改后的類圖如下:
?
?
源代碼如下。美女及其實(shí)現(xiàn)類:
?
?
搜索類及其子類如下:
?
?
通過(guò)重構(gòu)以后,不管以后需要顏值美女,還是需要身材美女,抑或氣質(zhì)美女,都可以保持接口的穩(wěn)定性。
以上把一個(gè)臃腫的接口拆分為三個(gè)獨(dú)立的接口所依賴的原則就是接口隔離原則。接口隔離原則是對(duì)接口進(jìn)行規(guī)范約束。
迪米特法則
迪米特法則(LoD)也叫最少知道法則:一個(gè)對(duì)象應(yīng)該對(duì)其他對(duì)象有最少的了解。
1.只和朋友交流
迪米特法則還有一個(gè)英文解釋是:Only talk to your immediate friends(只和直接的朋友交流)。每個(gè)對(duì)象都必然會(huì)與其他對(duì)象耦合,兩個(gè)對(duì)象的耦合就成為朋友關(guān)系。下面我們通過(guò)體育課老師讓班長(zhǎng)清點(diǎn)女生人數(shù)為例講解。
首先設(shè)計(jì)程序的類圖:
?
?
編碼實(shí)現(xiàn):
?
?
場(chǎng)景類:
?
?
程序開(kāi)發(fā)完了,我們首先看下Teacher類有幾個(gè)朋友類,首先要知道朋友類的定義:出現(xiàn)在成員變量、方法的輸入輸出參數(shù)中的類稱為成員朋友類。所以Teacher類只有一個(gè)GroupLeader朋友類。根據(jù)迪米特法則,一個(gè)類只能和朋友類交流,上面的Teacher類內(nèi)部卻與非朋友類Girl發(fā)生了交流,這就不符合迪米特法則,破壞了程序的健壯性。
我們對(duì)類圖做下修改:
?
?
修改后的代碼:
?
?
再看場(chǎng)景類調(diào)用:
?
?
總之,就是類與類之間的關(guān)系是建立在類間的,而不是方法間,因此一個(gè)方法盡量不引入一個(gè)類中不存在的對(duì)象。
2.朋友間也是有距離的
我們?cè)陂_(kāi)發(fā)中經(jīng)常有這種場(chǎng)景:調(diào)用一個(gè)或多個(gè)類,先執(zhí)行第一個(gè)方法,然后是第二個(gè)方法,根據(jù)返回結(jié)果再看是否執(zhí)行第三個(gè)方法。我們以安裝某個(gè)軟件為例,其類圖為:
?
?
代碼如下:
?
?
場(chǎng)景類:
?
?
程序很簡(jiǎn)單,但也存在一些問(wèn)題:Wizard類把太多方法暴露給InstallSoftware類了,兩者的朋友關(guān)系太親密了,耦合關(guān)系變的異常牢固,如果要把Wizard中first方法的返回值改為Boolean類型,則要同時(shí)修改InstallSoftware類,增加了風(fēng)險(xiǎn)。因此,這種耦合是不合適的,我們需要對(duì)其優(yōu)化。重構(gòu)后的類圖如下:
?
?
代碼如下。導(dǎo)向類:
?
?
我們把安裝步驟改為私有方法,只向外暴露一個(gè)安裝方法,這樣,即使修改步驟的邏輯,也只是對(duì)Wizard自己有影響,只需要修改自己的安裝方法邏輯即可,其他類不會(huì)受到影響。
安裝類:
?
?
一個(gè)類公開(kāi)的public屬性或方法越多,修改時(shí)涉及的面也就越大,變更引起的風(fēng)險(xiǎn)擴(kuò)散也就越大。所以,我們開(kāi)發(fā)中盡量不要對(duì)外公布太多public方法和非靜態(tài)的public變量,盡量?jī)?nèi)斂。
3.是自己的就是自己的
在實(shí)際開(kāi)發(fā)中經(jīng)常會(huì)出現(xiàn)這樣一種情況:一個(gè)方法放在吧本類中也可以,放在其他類中也沒(méi)有錯(cuò)。那這時(shí),我們只需要堅(jiān)持一個(gè)原則:如果一個(gè)方法放在本類中,既不增加類間關(guān)系,也對(duì)本類不產(chǎn)生負(fù)面影響,那就放置在本類中。
總之,迪米特法則的核心觀念就是類間解耦,弱耦合,只有弱耦合了以后,類的復(fù)用率才可以提升上去。
開(kāi)閉原則
開(kāi)閉原則是指一個(gè)軟件實(shí)體如類、模塊和函數(shù)應(yīng)該對(duì)擴(kuò)展開(kāi)放,對(duì)修改關(guān)閉。也就是說(shuō)一個(gè)軟件實(shí)體應(yīng)該通過(guò)擴(kuò)展來(lái)實(shí)現(xiàn)變化,而不是通過(guò)修改已有的代碼來(lái)實(shí)現(xiàn)變化。我們以書(shū)店銷售書(shū)籍為例來(lái)說(shuō)明什么是開(kāi)閉原則。
其類圖如下:
?
?
書(shū)籍及其實(shí)現(xiàn)類代碼如下:
?
?
書(shū)店類代碼:
?
?
項(xiàng)目開(kāi)發(fā)完了,開(kāi)始正常賣書(shū)了。假如到了雙十一,要搞打折活動(dòng),上面的功能是不支持的,所以需要修改程序。有三種方法可以解決這個(gè)問(wèn)題:
(1)修改接口
在IBook接口里新增getOffPrice()方法,專門用于進(jìn)行打折,所有的實(shí)現(xiàn)類都實(shí)現(xiàn)該方法。但這樣修改的后果就是,實(shí)現(xiàn)類NovelBook要修改,書(shū)店類BookStore中的main方法也要修改,同時(shí),IBook作為接口應(yīng)該是穩(wěn)定且可靠的,不應(yīng)該經(jīng)常發(fā)生變化,因此,該方案被否定。
(2)修改實(shí)現(xiàn)類
修改NovelBook類中的方法,直接在getPrice()方法中實(shí)現(xiàn)打折處理,這個(gè)方法可以是可以,但如果采購(gòu)書(shū)籍的人員要看價(jià)格怎么辦,由于該方法已經(jīng)進(jìn)行了打折處理,因此采購(gòu)人員看到的也是打折后的價(jià)格,會(huì)因信息不對(duì)稱出現(xiàn)決策失誤的情況。因此,該方案也不是一個(gè)最優(yōu)的方案。
(3)通過(guò)擴(kuò)展實(shí)現(xiàn)變化
增加一個(gè)子類OffNovelBook,覆寫(xiě)getPrice方法,高層次的模塊(也就是BookStore中static靜態(tài)塊中)通過(guò)OffNovelBook類產(chǎn)生新的對(duì)象,完成業(yè)務(wù)變化對(duì)系統(tǒng)的最小開(kāi)發(fā)。這樣修改也少,風(fēng)險(xiǎn)也小,修改后的類圖如下:
?
?
OffNovelBook源碼如下:
?
?
然后修改BookStore中的書(shū)籍類為OffNovelBook:
?
?
為什么要用開(kāi)閉原則
1. 開(kāi)閉原則非常著名,只要是做面向?qū)ο缶幊痰?#xff0c;在開(kāi)發(fā)時(shí)都會(huì)提及開(kāi)閉原則。
2. 開(kāi)閉原則是最基礎(chǔ)的一個(gè)原則,前面介紹的5個(gè)原則都是開(kāi)閉原則的具體形態(tài),而開(kāi)閉原則才是其精神領(lǐng)袖。
3. 開(kāi)閉原則提高了復(fù)用性,以及可維護(hù)性。
總結(jié)六大設(shè)計(jì)原則
1. 單一職責(zé)原則:一個(gè)類或接口只承擔(dān)一個(gè)職責(zé)。
2. 里氏替換原則:在繼承類時(shí),務(wù)必重寫(xiě)(override)父類中所有的方法,尤其需要注意父類的protected方法(它們往往是讓你重寫(xiě)的),子類盡量不要暴露自己的public方法供外界調(diào)用。
3. 依賴倒置原則:高層模塊不應(yīng)該依賴于低層模塊,而應(yīng)該依賴于抽象。抽象不應(yīng)依賴于細(xì)節(jié),細(xì)節(jié)應(yīng)依賴于抽象。
4. 接口隔離原則:不要對(duì)外暴露沒(méi)有實(shí)際意義的接口。
5. 迪米特法則:盡量減少對(duì)象之間的交互,從而減小類之間的耦合。
6. 開(kāi)閉原則:對(duì)軟件實(shí)體的改動(dòng),最好用擴(kuò)展而非修改的方式。
總結(jié)
- 上一篇: Asp.net 中 Eval 调用后台函
- 下一篇: java敏感词过滤算法