設計模式(Design pattern)是一套被反復使用、多數人知曉的前域、經過分類編目的、代碼設計經驗的總結韵吨。使用設計模式是為了可重用代碼匿垄、讓代碼更容易被他人理解、保證代碼可靠性归粉。 毫無疑問椿疗,設計模式于己于他人于系統(tǒng)都是多贏的,設計模式使代碼編制真正工程化糠悼,設計模式是軟件工程的基石届榄,如同大廈的一塊塊磚石一樣。項目中合理的運用設計模式可以完美的解決很多問題倔喂,每種模式在現在中都有相應的原理來與之對應铝条,每一個模式描述了一個在我們周圍不斷重復發(fā)生的問題,以及該問題的核心解決方案席噩,這也是它能被廣泛應用的原因班缰。
使用模式最好的方式是:“把模式裝進腦子里,然后在你的設計和已有的應用中悼枢,尋找何處可以使用他們埠忘。”以往是代碼復用,現在是經驗復用莹妒。
一名船、設計模式的分類
Gof設計模式Gof設計模式有23個,分為三類:創(chuàng)建型(如何創(chuàng)建對象)旨怠,結構型(如何實現對象的組合)渠驼,行為型(對象如何交互以及怎么分配職責)。
其中創(chuàng)建型有5個:
- 單例模式 Singleton Pattern
- 工廠方法模式 Factory Method Pattern
- 抽象工廠模式 Abstract Factory Pattern
- 原型模式 Prototype Pattern
- 建造者模式 Builder Pattern
結構型有7個:
- 適配器模式 Adapter Pattern
- 橋接模式 Bridge Pattern
- 組合模式 Composite Pattern
- 裝飾模式 Decorator Pattern
- 外觀模式 Fa?ade Pattern
- 享元模式 Flyweight Pattern
- 代理模式 Proxy Pattern
行為型有11個:
- 職責鏈模式 Chain of Responsibility Pattern
- 命令模式 Command Pattern
- 解釋器模式 Interpreter Pattern
- 迭代器模式 Iterator Pattern
- 中介者模式 Mediator Pattern
- 備忘錄模式 Memento Pattern
- 觀察者模式 Observer Pattern
- 狀態(tài)模式 State Pattern
- 策略模式 Strategy Pattern
- 模板方法模式 Template Method Pattern
- 訪問者模式 Visitor Pattern
二运吓、設計模式的六大原則
1渴邦、單一職責原則(Single Responsibility Principle,簡稱SRP )
定義:不要存在多于一個導致類變更的原因拘哨。通俗的說谋梭,即一個類只負責一項職責 ,只有一個引起它變化的原因倦青。
問題由來:類T負責兩個不同的職責:職責P1瓮床,職責P2。當由于職責P1需求發(fā)生改變而需要修改類T時产镐,有可能會導致原本運行正常的職責P2功能發(fā)生故障隘庄。
解決方案:遵循單一職責原則。分別建立兩個類T1癣亚、T2丑掺,使T1完成職責P1功能,T2完成職責P2功能述雾。這樣街州,當修改類T1時,不會使職責P2發(fā)生故障風險玻孟;同理唆缴,當修改T2時,也不會使職責P1發(fā)生故障風險黍翎。
說到單一職責原則面徽,很多人都會不屑一顧。因為它太簡單了匣掸。稍有經驗的程序員即使從來沒有讀過設計模式趟紊、從來沒有聽說過單一職責原則,在設計軟件時也會自覺的遵守這一重要原則碰酝,因為這是常識霎匈。在軟件編程中,誰也不希望因為修改了一個功能導致其他的功能發(fā)生故障砰粹。而避免出現這一問題的方法便是遵循單一職責原則。雖然單一職責原則如此簡單,并且被認為是常識碱璃,但是即便是經驗豐富的程序員寫出的程序弄痹,也會有違背這一原則的代碼存在。為什么會出現這種現象呢嵌器?因為有職責擴散肛真。所謂職責擴散,就是因為某種原因爽航,職責P被分化為粒度更細的職責P1和P2蚓让。
比如:類T只負責一個職責P,這樣設計是符合單一職責原則的讥珍。后來由于某種原因历极,也許是需求變更了,也許是程序的設計者境界提高了衷佃,需要將職責P細分為粒度更細的職責P1趟卸,P2,這時如果要使程序遵循單一職責原則氏义,需要將類T也分解為兩個類T1和T2锄列,分別負責P1、P2兩個職責惯悠。但是在程序已經寫好的情況下邻邮,這樣做簡直太費時間了。所以克婶,簡單的修改類T筒严,用它來負責兩個職責是一個比較不錯的選擇,雖然這樣做有悖于單一職責原則鸠补。(這樣做的風險在于職責擴散的不確定性萝风,因為我們不會想到這個職責P,在未來可能會擴散為P1紫岩,P2规惰,P3,P4……Pn泉蝌。所以記住搂妻,在職責擴散到我們無法控制的程度之前,立刻對代碼進行重構潘拱。)
舉例說明拱撵,用一個類描述動物呼吸這個場景:
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("豬");
}
}
運行結果:
牛呼吸空氣
羊呼吸空氣
豬呼吸空氣
程序上線后,發(fā)現問題了诅愚,并不是所有的動物都呼吸空氣的寒锚,比如魚就是呼吸水的。修改時如果遵循單一職責原則,需要將Animal類細分為陸生動物類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("魚");
}
}
我們會發(fā)現如果這樣修改花銷是很大的,除了將原來的類分解之外喇喉,還需要修改客戶端祖今。而直接修改類Animal來達成目的雖然違背了單一職責原則,但花銷卻小的多拣技,代碼如下:
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方法徐绑,而對原有代碼的修改會對調用“豬”“牛”“羊”等相關功能帶來風險掸绞,也許某一天你會發(fā)現程序運行的結果變?yōu)椤芭:粑绷恕?strong>這種修改方式直接在代碼級別上違背了單一職責原則泵三,雖然修改起來最簡單,但隱患卻是最大的衔掸。還有一種修改方式:
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("魚");
}
}
可以看到烫幕,這種修改方式沒有改動原來的方法,而是在類中新加了一個方法敞映,這樣雖然也違背了單一職責原則较曼,但在方法級別上卻是符合單一職責原則的,因為它并沒有動原來方法的代碼振愿。這三種方式各有優(yōu)缺點捷犹,那么在實際編程中,采用哪一中呢冕末?其實這真的比較難說萍歉,需要根據實際情況來確定。我的原則是:只有邏輯足夠簡單档桃,才可以在代碼級別上違反單一職責原則枪孩;只有類中方法數量足夠少,才可以在方法級別上違反單一職責原則藻肄;
例如本文所舉的這個例子蔑舞,它太簡單了,它只有一個方法嘹屯,所以攻询,無論是在代碼級別上違反單一職責原則,還是在方法級別上違反州弟,都不會造成太大的影響钧栖。實際應用中的類都要復雜的多低零,一旦發(fā)生職責擴散而需要修改類時,除非這個類本身非常簡單拯杠,否則還是遵循單一職責原則的好毁兆。
遵循單一職責原的優(yōu)點有:
- 可以降低類的復雜度,一個類只負責一項職責阴挣,其邏輯肯定要比負責多項職責簡單的多;
- 提高類的可讀性纺腊,提高系統(tǒng)的可維護性畔咧;
- 變更引起的風險降低,變更是必然的揖膜,如果單一職責原則遵守的好誓沸,當修改一個功能時,可以顯著降低對其他功能的影響壹粟。
- 需要說明的一點是單一職責原則不只是面向對象編程思想所特有的拜隧,只要是模塊化的程序設計,都適用單一職責原則趁仙。
2洪添、里氏替換原則(Liskov Substitution Principle,簡稱LSP)
肯定有不少人跟我剛看到這項原則的時候一樣,對這個原則的名字充滿疑惑雀费。其實原因就是這項原則最早是在1988年干奢,由麻省理工學院的一位姓里的女士(Barbara Liskov)提出來的。
定義1:如果對每一個類型為 T1的對象 o1盏袄,都有類型為 T2 的對象o2忿峻,使得以 T1定義的所有程序 P 在所有的對象 o1 都代換成 o2 時,程序 P 的行為沒有發(fā)生變化辕羽,那么類型 T2 是類型 T1 的子類型逛尚。
定義2:所有引用基類的地方必須能透明地使用其子類的對象。簡單來說刁愿,所有使用基類代碼的地方绰寞,如果換成子類對象的時候還能夠正常運行,則滿足這個原則酌毡,否則就是繼承關系有問題克握,應該廢除兩者的繼承關系,這個原則可以用來判斷我們的對象繼承關系是否合理枷踏。
問題由來:有一功能P1菩暗,由類A完成。現需要將功能P1進行擴展旭蠕,擴展后的功能為P停团,其中P由原有功能P1與新功能P2組成旷坦。新功能P由類A的子類B來完成,則子類B在完成新功能P2的同時佑稠,有可能會導致原有功能P1發(fā)生故障秒梅。
解決方案:當使用繼承時,遵循里氏替換原則舌胶。類B繼承類A時捆蜀,除添加新的方法完成新增功能P2外,盡量不要重寫父類A的方法幔嫂,也盡量不要重載父類A的方法辆它。
繼承包含這樣一層含義:父類中凡是已經實現好的方法(相對于抽象方法而言),實際上是在設定一系列的規(guī)范和契約履恩,雖然它不強制要求所有的子類必須遵從這些契約锰茉,但是如果子類對這些非抽象方法任意修改,就會對整個繼承體系造成破壞切心。而里氏替換原則就是表達了這一層含義飒筑。
通常在設計的時候,我們都會優(yōu)先采用組合而不是繼承绽昏,因為繼承雖然減少了代碼协屡,提高了代碼的重用性,但是父類跟子類會有很強的耦合性全谤,破壞了封裝著瓶。
繼承作為面向對象三大特性之一,在給程序設計帶來巨大便利的同時啼县,也帶來了弊端材原。比如使用繼承會給程序帶來侵入性,程序的可移植性降低季眷,增加了對象間的耦合性余蟹,如果一個類被其他的類所繼承,則當這個類需要修改時子刮,必須考慮到所有的子類威酒,并且父類修改后,所有涉及到子類的功能都有可能會產生故障挺峡。
舉例說明繼承的風險葵孤,我們需要完成一個兩數相減的功能,由類A來負責橱赠。
class A{
public int func1(int a, int b){
return a-b;
}
}
public class Client{
public static void main(String[] args){
A a = new A();
System.out.println("100-50="+a.func1(100, 50));
System.out.println("100-80="+a.func1(100, 80));
}
}
運行結果:
100-50=50
100-80=20
后來尤仍,我們需要增加一個新的功能:完成兩數相加,然后再與100求和狭姨,由類B來負責宰啦。即類B需要完成兩個功能:
- 兩數相減苏遥。
- 兩數相加,然后再加100赡模。
由于類A已經實現了第一個功能田炭,所以類B繼承類A后,只需要再完成第二個功能就可以了漓柑,代碼如下:
class B extends A{
public int func1(int a, int b){
return a+b;
}
public int func2(int a, int b){
return func1(a,b)+100;
}
}
public class Client{
public static void main(String[] args){
B b = new B();
System.out.println("100-50="+b.func1(100, 50));
System.out.println("100-80="+b.func1(100, 80));
System.out.println("100+20+100="+b.func2(100, 20));
}
}
類B完成后教硫,運行結果:
100-50=150
100-80=180
100+20+100=220
我們發(fā)現原本運行正常的相減功能發(fā)生了錯誤。原因就是類B在給方法起名時無意中重寫了父類的方法辆布,造成所有運行相減功能的代碼全部調用了類B重寫后的方法栋豫,造成原本運行正常的功能出現了錯誤。在本例中谚殊,引用基類A完成的功能,換成子類B之后蛤铜,發(fā)生了異常嫩絮。在實際編程中,我們常常會通過重寫父類的方法來完成新的功能围肥,這樣寫起來雖然簡單剿干,但是整個繼承體系的可復用性會比較差,特別是運用多態(tài)比較頻繁時穆刻,程序運行出錯的幾率非常大置尔。如果非要重寫父類的方法,比較通用的做法是:原來的父類和子類都繼承一個更通俗的基類氢伟,原有的繼承關系去掉榜轿,采用依賴、聚合朵锣,組合等關系代替谬盐。
里氏替換原則通俗的來講就是:子類可以擴展父類的功能,但不能改變父類原有的功能诚些。它包含以下4層含義:
- 子類可以實現父類的抽象方法飞傀,但不能覆蓋父類的非抽象方法。
- 子類中可以增加自己特有的方法诬烹。
- 當子類的方法重載父類的方法時砸烦,方法的前置條件(即方法的形參)要比父類方法的輸入參數更寬松。
- 當子類的方法實現父類的抽象方法時绞吁,方法的后置條件(即方法的返回值)要比父類更嚴格幢痘。
看上去很不可思議,因為我們會發(fā)現在自己編程中常常會違反里氏替換原則家破,程序照樣跑的好好的雪隧。所以大家都會產生這樣的疑問西轩,假如我非要不遵循里氏替換原則會有什么后果?
后果就是:你寫的代碼出問題的幾率將會大大增加脑沿。
3藕畔、依賴倒置原則(Dependence Inversion Principle,簡稱DIP)
定義:高層模塊不應該依賴低層模塊,二者都應該依賴其抽象庄拇;抽象不應該依賴細節(jié)注服;細節(jié)應該依賴抽象。
說明:高層模塊就是調用端措近,低層模塊就是具體實現類溶弟。抽象就是指接口或抽象類。細節(jié)就是實現類瞭郑。
通俗來講:依賴倒置原則的本質就是通過抽象(接口或抽象類)使個各類或模塊的實現彼此獨立辜御,互不影響,實現模塊間的松耦合屈张。
問題描述:類A直接依賴類B擒权,假如要將類A改為依賴類C,則必須通過修改類A的代碼來達成阁谆。這種場景下碳抄,類A一般是高層模塊,負責復雜的業(yè)務邏輯场绿;類B和類C是低層模塊剖效,負責基本的原子操作;假如修改類A焰盗,會給程序帶來不必要的風險璧尸。
解決方案:將類A修改為依賴接口interface,類B和類C各自實現接口interface熬拒,類A通過接口interface間接與類B或者類C發(fā)生聯系逗宁,則會大大降低修改類A的幾率。
好處:依賴倒置的好處在小型項目中很難體現出來梦湘。但在大中型項目中可以減少需求變化引起的工作量瞎颗。使并行開發(fā)更友好。
問題由來:類A直接依賴類B捌议,假如要將類A改為依賴類C哼拔,則必須通過修改類A的代碼來達成。這種場景下瓣颅,類A一般是高層模塊倦逐,負責復雜的業(yè)務邏輯;類B和類C是低層模塊宫补,負責基本的原子操作檬姥;假如修改類A曾我,會給程序帶來不必要的風險。
解決方案:將類A修改為依賴接口I健民,類B和類C各自實現接口I抒巢,類A通過接口I間接與類B或者類C發(fā)生聯系,則會大大降低修改類A的幾率秉犹。
依賴倒置原則基于這樣一個事實:相對于細節(jié)的多變性蛉谜,抽象的東西要穩(wěn)定的多型诚。以抽象為基礎搭建起來的架構比以細節(jié)為基礎搭建起來的架構要穩(wěn)定的多涵紊。在java中,抽象指的是接口或者抽象類,細節(jié)就是具體的實現類庆亡,使用接口或者抽象類的目的是制定好規(guī)范和契約彰亥,而不去涉及任何具體的操作废酷,把展現細節(jié)的任務交給他們的實現類去完成丰介。
更加精簡的定義就是“面向接口編程”——OOD(Object-Oriented Design,面向對象設計)的精髓之一柜某。
依賴倒置原則的核心思想是面向接口編程,我們依舊用一個例子來說明面向接口編程比相對于面向實現編程好在什么地方监嗜。場景是這樣的溃肪,母親給孩子講故事放仗,只要給她一本書润绎,她就可以照著書給孩子講故事了。代碼如下:
class Book{
public String getContent(){
return "很久很久以前有一個阿拉伯的故事……";
}
}
class Mother{
public void narrate(Book book){
System.out.println("媽媽開始講故事");
System.out.println(book.getContent());
}
}
public class Client{
public static void main(String[] args){
Mother mother = new Mother();
mother.narrate(new Book());
}
}
運行結果:
媽媽開始講故事
很久很久以前有一個阿拉伯的故事……
運行良好,假如有一天莉撇,需求變成這樣:不是給書而是給一份報紙呢蛤,讓這位母親講一下報紙上的故事,報紙的代碼如下:
class Newspaper{
public String getContent(){
return "林書豪38+7領導尼克斯擊敗湖人……";
}
}
這位母親卻辦不到棍郎,因為她居然不會讀報紙上的故事其障,這太荒唐了,只是將書換成報紙涂佃,居然必須要修改Mother才能讀励翼。假如以后需求換成雜志呢?換成網頁呢辜荠?還要不斷地修改Mother汽抚,這顯然不是好的設計。原因就是Mother與Book之間的耦合性太高了伯病,必須降低他們之間的耦合度才行造烁。
我們引入一個抽象的接口IReader。讀物午笛,只要是帶字的都屬于讀物:
interface IReader{
public String getContent();
}
Mother類與接口IReader發(fā)生依賴關系惭蟋,而Book和Newspaper都屬于讀物的范疇,他們各自都去實現IReader接口药磺,這樣就符合依賴倒置原則了告组,代碼修改為:
class Newspaper implements IReader {
public String getContent(){
return "林書豪17+9助尼克斯擊敗老鷹……";
}
}
class Book implements IReader{
public String getContent(){
return "很久很久以前有一個阿拉伯的故事……";
}
}
class Mother{
public void narrate(IReader reader){
System.out.println("媽媽開始講故事");
System.out.println(reader.getContent());
}
}
public class Client{
public static void main(String[] args){
Mother mother = new Mother();
mother.narrate(new Book());
mother.narrate(new Newspaper());
}
}
運行結果:
媽媽開始講故事
很久很久以前有一個阿拉伯的故事……
媽媽開始講故事
林書豪17+9助尼克斯擊敗老鷹……
這樣修改后,無論以后怎樣擴展Client類癌佩,都不需要再修改Mother類了木缝。這只是一個簡單的例子,實際情況中驼卖,代表高層模塊的Mother類將負責完成主要的業(yè)務邏輯氨肌,一旦需要對它進行修改鸿秆,引入錯誤的風險極大酌畜。所以遵循依賴倒置原則可以降低類之間的耦合性,提高系統(tǒng)的穩(wěn)定性卿叽,降低修改程序造成的風險桥胞。
采用依賴倒置原則給多人并行開發(fā)帶來了極大的便利,比如上例中考婴,原本Mother類與Book類直接耦合時贩虾,Mother類必須等Book類編碼完成后才可以進行編碼,因為Mother類依賴于Book類沥阱。修改后的程序則可以同時開工缎罢,互不影響,因為Mother與Book類一點關系也沒有。參與協(xié)作開發(fā)的人越多策精、項目越龐大舰始,采用依賴導致原則的意義就越重大。現在很流行的TDD開發(fā)模式就是依賴倒置原則最成功的應用咽袜。
傳遞依賴關系有三種方式丸卷,以上的例子中使用的方法是接口傳遞,另外還有兩種傳遞方式:構造方法傳遞和setter方法傳遞询刹,相信用過Spring框架的谜嫉,對依賴的傳遞方式一定不會陌生。
依賴的三種寫法
依賴是可以傳遞的凹联,A對象依賴B對象沐兰,B又依賴C,C又依賴D……生生不息匕垫,依賴不止僧鲁,記住一點:只要做到抽象依賴,即使是多層的依賴傳遞也無所畏懼象泵!
對象的依賴關系有三種方式來傳遞寞秃,如下所示。
1.構造函數傳遞依賴對象
在類中通過構造函數聲明依賴對象偶惠,按照依賴注入的說法春寿,這種方式叫做構造函數注入,按照這種方式的注入忽孽,IDriver和Driver的程序修改后如代碼清單3-11所示绑改。
代碼清單3-11 構造函數傳遞依賴對象
public interface IDriver {
//是司機就應該會駕駛汽車
public void drive();
}
public class Driver implements IDriver{
private ICar car;
//構造函數注入
public Driver(ICar _car){
this.car = _car;
}
//司機的主要職責就是駕駛汽車
public void drive(){
this.car.run();
}
}
2.Setter方法傳遞依賴對象
在抽象中設置Setter方法聲明依賴關系,依照依賴注入的說法兄一,這是Setter依賴注入厘线,按照這種方式的注入,IDriver和Driver的程序修改后如代碼清單3-12所示出革。
代碼清單3-12 Setter依賴注入
public interface IDriver {
//車輛型號
public void setCar(ICar car);
//是司機就應該會駕駛汽車
public void drive();
}
public class Driver implements IDriver{
private ICar car;
public void setCar(ICar car){
this.car = car;
}
//司機的主要職責就是駕駛汽車
public void drive(){
this.car.run();
}
}
3.接口聲明依賴對象
在接口的方法中聲明依賴對象造壮,3.2節(jié)的例子就采用了接口聲明依賴的方式,該方法也叫做接口注入骂束。
在實際編程中耳璧,我們一般需要做到如下3點:
- 低層模塊盡量都要有抽象類或接口,或者兩者都有展箱。
- 變量的聲明類型盡量是抽象類或接口旨枯。
- 使用繼承時遵循里氏替換原則。
依賴倒置原則的核心就是要我們面向接口編程混驰,理解了面向接口編程攀隔,也就理解了依賴倒置皂贩。
講了這么多,估計大家對“倒置”這個詞還是有點不理解昆汹,那到底什么是“倒置”呢先紫?我們先說“正置”是什么意思,依賴正置就是類間的依賴是實實在在的實現類間的依賴筹煮,也就是面向實現編程遮精,這也是正常人的思維方式,我要開奔馳車就依賴奔馳車败潦,我要使用筆記本電腦就直接依賴筆記本電腦本冲,而編寫程序需要的是對現實世界的事物進行抽象,抽象的結果就是有了抽象類和接口劫扒,然后我們根據系統(tǒng)設計的需要產生了抽象間的依賴檬洞,代替了人們傳統(tǒng)思維中的事物間的依賴,“倒置”就是從這里產生的沟饥。
依賴倒置原則是6個設計原則中最難以實現的原則添怔,它是實現開閉原則的重要途徑,依賴倒置原則沒有實現贤旷,就別想實現對擴展開放广料,對修改關閉。在項目中幼驶,大家只要記住是“面向接口編程”就基本上抓住了依賴倒置原則的核心艾杏。
4、接口隔離原則(Interface Segregation Principle,簡稱ISP)
定義:客戶端不應該依賴它不需要的接口盅藻;一個類對另一個類的依賴應該建立在最小的接口上购桑。
新事物的定義一般都比較難理解,晦澀難懂是正常的氏淑。我們把這兩個定義剖析一下勃蜘,先說第一種定義:“客戶端不應該依賴它不需要的接口”,那依賴什么假残?依賴它需要的接口缭贡,客戶端需要什么接口就提供什么接口,把不需要的接口剔除掉守问,那就需要對接口進行細化匀归,保證其純潔性坑资;再看第二種定義:“類間的依賴關系應該建立在最小的接口上”耗帕,它要求是最小的接口,也是要求接口細化袱贮,接口純潔仿便,與第一個定義如出一轍,只是一個事物的兩種不同描述。
問題由來:類A通過接口I依賴類B嗽仪,類C通過接口I依賴類D荒勇,如果接口I對于類A和類B來說不是最小接口,則類B和類D必須去實現他們不需要的方法闻坚。
解決方案:將臃腫的接口I拆分為獨立的幾個接口沽翔,類A和類C分別與他們需要的接口建立依賴關系。也就是采用接口隔離原則窿凤。
舉例來說明接口隔離原則:
這個圖的意思是:類A依賴接口I中的方法1仅偎、方法2、方法3雳殊,類B是對類A依賴的實現橘沥。類C依賴接口I中的方法1、方法4夯秃、方法5座咆,類D是對類C依賴的實現。對于類B和類D來說仓洼,雖然他們都存在著用不到的方法(也就是圖中紅色字體標記的方法)介陶,但由于實現了接口I,所以也必須要實現這些用不到的方法色建。對類圖不熟悉的可以參照程序代碼來理解斤蔓,代碼如下:
interface I {
public void method1();
public void method2();
public void method3();
public void method4();
public void method5();
}
class A{
public void depend1(I i){
i.method1();
}
public void depend2(I i){
i.method2();
}
public void depend3(I i){
i.method3();
}
}
class B implements I{
public void method1() {
System.out.println("類B實現接口I的方法1");
}
public void method2() {
System.out.println("類B實現接口I的方法2");
}
public void method3() {
System.out.println("類B實現接口I的方法3");
}
//對于類B來說,method4和method5不是必需的镀岛,但是由于接口A中有這兩個方法弦牡,
//所以在實現過程中即使這兩個方法的方法體為空,也要將這兩個沒有作用的方法進行實現漂羊。
public void method4() {}
public void method5() {}
}
class C{
public void depend1(I i){
i.method1();
}
public void depend2(I i){
i.method4();
}
public void depend3(I i){
i.method5();
}
}
class D implements I{
public void method1() {
System.out.println("類D實現接口I的方法1");
}
//對于類D來說驾锰,method2和method3不是必需的,但是由于接口A中有這兩個方法走越,
//所以在實現過程中即使這兩個方法的方法體為空椭豫,也要將這兩個沒有作用的方法進行實現。
public void method2() {}
public void method3() {}
public void method4() {
System.out.println("類D實現接口I的方法4");
}
public void method5() {
System.out.println("類D實現接口I的方法5");
}
}
public class Client{
public static void main(String[] args){
A a = new A();
a.depend1(new B());
a.depend2(new B());
a.depend3(new B());
C c = new C();
c.depend1(new D());
c.depend2(new D());
c.depend3(new D());
}
}
可以看到旨指,如果接口過于臃腫赏酥,只要接口中出現的方法,不管對依賴于它的類有沒有用處谆构,實現類中都必須去實現這些方法裸扶,這顯然不是好的設計。如果將這個設計修改為符合接口隔離原則搬素,就必須對接口I進行拆分呵晨。在這里我們將原有的接口I拆分為三個接口魏保,拆分后的設計如圖2所示:
照例貼出程序的代碼,供不熟悉類圖的朋友參考:
interface I1 {
public void method1();
}
interface I2 {
public void method2();
public void method3();
}
interface I3 {
public void method4();
public void method5();
}
class A{
public void depend1(I1 i){
i.method1();
}
public void depend2(I2 i){
i.method2();
}
public void depend3(I2 i){
i.method3();
}
}
class B implements I1, I2{
public void method1() {
System.out.println("類B實現接口I1的方法1");
}
public void method2() {
System.out.println("類B實現接口I2的方法2");
}
public void method3() {
System.out.println("類B實現接口I2的方法3");
}
}
class C{
public void depend1(I1 i){
i.method1();
}
public void depend2(I3 i){
i.method4();
}
public void depend3(I3 i){
i.method5();
}
}
class D implements I1, I3{
public void method1() {
System.out.println("類D實現接口I1的方法1");
}
public void method4() {
System.out.println("類D實現接口I3的方法4");
}
public void method5() {
System.out.println("類D實現接口I3的方法5");
}
}
接口隔離原則的含義是:建立單一接口摸屠,不要建立龐大臃腫的接口谓罗,盡量細化接口,接口中的方法盡量少季二。也就是說檩咱,我們要為各個類建立專用的接口,而不要試圖去建立一個很龐大的接口供所有依賴它的類去調用胯舷。本文例子中税手,將一個龐大的接口變更為3個專用的接口所采用的就是接口隔離原則。在程序設計中需纳,依賴幾個專用的接口要比依賴一個綜合的接口更靈活芦倒。接口是設計時對外部設定的“契約”,通過分散定義多個接口不翩,可以預防外來變更的擴散兵扬,提高系統(tǒng)的靈活性和可維護性。
說到這里口蝠,很多人會覺的接口隔離原則跟之前的單一職責原則很相似器钟,其實不然。其一妙蔗,單一職責原則原注重的是職責傲霸;而接口隔離原則注重對接口依賴的隔離。其二眉反,單一職責原則主要是約束類昙啄,其次才是接口和方法,它針對的是程序中的實現和細節(jié)寸五;而接口隔離原則主要約束接口接口梳凛,主要針對抽象,針對程序整體框架的構建梳杏。
采用接口隔離原則對接口進行約束時韧拒,要注意以下幾點:
- 接口盡量小,但是要有限度十性。對接口進行細化可以提高程序設計靈活性是不掙的事實叛溢,但是如果過小,則會造成接口數量過多劲适,使設計復雜化楷掉。所以一定要適度。
- 為依賴接口的類定制服務减响,只暴露給調用的類它需要的方法靖诗,它不需要的方法則隱藏起來许饿。只有專注地為一個模塊提供定制服務谭确,才能建立最小的依賴關系。
- 提高內聚主之,減少對外交互颂鸿。使接口用最少的方法去完成最多的事情促绵。
運用接口隔離原則,一定要適度嘴纺,接口設計的過大或過小都不好败晴。設計接口的時候,只有多花些時間去思考和籌劃栽渴,才能準確地實踐這一原則尖坤。
參考資料:
設計模式之禪