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