前言
關(guān)于設(shè)計(jì)模式六大設(shè)計(jì)原則的資料網(wǎng)上很多杯道,但感覺很多地方解釋地都太過于籠統(tǒng)化,特此再總結(jié)一波。
優(yōu)化第一步-單一職責(zé)原則SRP
單一職責(zé)原則(Single Responsibility Principle, SRP):一個(gè)類只負(fù)責(zé)一個(gè)功能領(lǐng)域中的相應(yīng)職責(zé),或者可以定義為:就一個(gè)類而言,應(yīng)該只有一個(gè)引起它變化的原因散休。
經(jīng)典問題:
類T負(fù)責(zé)兩個(gè)不同的職責(zé):職責(zé)P1,職責(zé)P2乐尊。當(dāng)職責(zé)P1需求發(fā)生改變而需要修改類T時(shí)戚丸,有可能會(huì)導(dǎo)致原本運(yùn)行正常的職責(zé)P2功能發(fā)生故障。
解決方案:
遵循單一職責(zé)原則扔嵌。分別建立兩個(gè)類T1限府、T2,使T1完成職責(zé)P1功能痢缎,T2完成職責(zé)P2功能胁勺。這樣,當(dāng)修改類T1時(shí)独旷,不會(huì)使職責(zé)P2發(fā)生故障風(fēng)險(xiǎn)署穗;同理,當(dāng)修改T2時(shí)嵌洼,也不會(huì)使職責(zé)P1發(fā)生故障風(fēng)險(xiǎn)案疲。
單一職責(zé)原則是實(shí)現(xiàn)高內(nèi)聚、低耦合的指導(dǎo)方針麻养,它是最簡單但又最難運(yùn)用的原則褐啡,因?yàn)閱我宦氊?zé)的劃分界限并不總是很清晰的,它需要設(shè)計(jì)人員發(fā)現(xiàn)類的不同職責(zé)并將其分離鳖昌,這就需要設(shè)計(jì)人員具有較強(qiáng)的分析設(shè)計(jì)能力和相關(guān)實(shí)踐經(jīng)驗(yàn)备畦。
在軟件系統(tǒng)中,一個(gè)類(大到模塊许昨,小到方法)承擔(dān)的職責(zé)越多懂盐,它被復(fù)用的可能性就越小,而且一個(gè)類承擔(dān)的職責(zé)過多糕档,就相當(dāng)于將這些職責(zé)耦合在一起允粤,當(dāng)其中一個(gè)職責(zé)發(fā)生變化時(shí),可能會(huì)影響其他職責(zé)的運(yùn)行,因此需要將這些不同的職責(zé)進(jìn)行封裝在不同的類中类垫,即將不同的變化封裝在不同的類中。如果多個(gè)職責(zé)總是同時(shí)發(fā)生改變則可將它們封裝在同一類中琅坡。
說到單一職責(zé)原則悉患,很多人都會(huì)不屑一顧。因?yàn)樗唵瘟擞馨场I杂薪?jīng)驗(yàn)的程序員在進(jìn)行設(shè)計(jì)軟件時(shí)也會(huì)自覺的遵守這一重要原則,而且誰也不希望因?yàn)樾薷牧艘粋€(gè)功
能導(dǎo)致其他的功能發(fā)生故障茴晋。雖然單一職責(zé)原則如此簡單诺擅,但是即便是經(jīng)驗(yàn)豐富的程序員也會(huì)有違背這一原則的代碼存在。為什么會(huì)出現(xiàn)這種現(xiàn)
象呢烁涌?
因?yàn)橛新氊?zé)擴(kuò)散苍碟。所謂職責(zé)擴(kuò)散,就是因?yàn)槟撤N原因撮执,職責(zé)P被分化為粒度更細(xì)的職責(zé)P1和P2微峰。
比如:類T只負(fù)責(zé)一個(gè)職責(zé)P,這樣設(shè)計(jì)是符合單一職責(zé)原則的抒钱。后來由于某種原因蜓肆,也許是需求變更了,也許是程序的設(shè)計(jì)者境界提高了谋币,需要將職責(zé)P細(xì)分為粒度更細(xì)的職責(zé)P1仗扬,P2,這時(shí)如果要使程序遵循單一職責(zé)原則瑞信,需要將類T也分解為兩個(gè)類T1和T2厉颤,分別負(fù)責(zé)P1、P2兩個(gè)職責(zé)凡简。但是在程序已經(jīng)寫好的情況下逼友,這樣做簡直太費(fèi)時(shí)間了。所以秤涩,簡單的修改類T帜乞,用它來負(fù)責(zé)兩個(gè)職責(zé)是一個(gè)比較不錯(cuò)的選擇,雖然這樣做有悖于單一職責(zé)原則筐眷。(這樣做的風(fēng)險(xiǎn)在于職責(zé)擴(kuò)散的不確定性黎烈,因?yàn)槲覀儾粫?huì)想到這個(gè)職責(zé)P,在未來可能會(huì)擴(kuò)散為P1,P2照棋,P3资溃,P4……Pn。所以記住烈炭,在職責(zé)擴(kuò)散到我們無法控制的程度之前溶锭,立刻對(duì)代碼進(jìn)行重構(gòu)。)
舉例說明符隙,用一個(gè)類描述動(dòng)物運(yùn)動(dòng)這個(gè)場景:
@Test
public void SrpTest() {
Animal animal = new Animal();
animal.move("牛");
animal.move("羊");
}
private class Animal {
private void move(String animal) {
System.out.println(animal + "奔跑");
}
}
運(yùn)行結(jié)果:
牛奔跑
羊奔跑
程序上線后趴捅,發(fā)現(xiàn)問題了,并不是所有的動(dòng)物都是奔跑的霹疫,比如魚就是在水游的拱绑。修改時(shí)如果遵循單一職責(zé)原則,需要將Animal類細(xì)分為陸生動(dòng)物類Terrestrial丽蝎,水生動(dòng)物Aquatic猎拨,代碼如下:
@Test
public void SrpTest() {
Terrestrial terrestrial = new Terrestrial();
terrestrial.move("牛");
terrestrial.move("羊");
Aquatic aquatic = new Aquatic();
aquatic.move("魚");
}
private class Terrestrial {
private void move(String animal) {
System.out.println(animal + "奔跑");
}
}
private class Aquatic {
private void move(String animal) {
System.out.println(animal + "在水里游");
}
}
運(yùn)行結(jié)果:
牛奔跑
羊奔跑
魚在水里游
我們會(huì)發(fā)現(xiàn)如果這樣修改花銷是很大的,除了將原來的類分解之外征峦,還需要修改客戶端迟几。而直接修改類Animal來達(dá)成目的雖然違背了單一職責(zé)原則,但花銷卻小的多栏笆。
@Test
public void SrpTest() {
Animal animal = new Animal();
animal.move("牛");
animal.move("羊");
animal.move("魚");
}
private class Animal {
// 極差的拓展方式
private void move(String animal) {
if ("魚".equals(animal)) {
System.out.println(animal + "奔跑");
} else {
System.out.println(animal + "在水里游");
}
}
}
運(yùn)行結(jié)果:
牛奔跑
羊奔跑
魚在水里游
可以看到类腮,這種修改方式要簡單的多。但是卻存在著隱患:有一天需要將魚分為在淺水游的魚和在深水里游的魚蛉加,或者是需要添加鳥要在天上飛等情況蚜枢,則又需要修改Animal類的move方法,而對(duì)原有代碼的修改會(huì)對(duì)調(diào)用“耪爰ⅲ”“羊”等相關(guān)功能帶來風(fēng)險(xiǎn)厂抽,也許某一天你會(huì)發(fā)現(xiàn)程序運(yùn)行的結(jié)果變?yōu)椤芭T谒镉巍绷恕_@種修改方式直接在代碼級(jí)別上違背了單一職責(zé)原則丁眼,雖然修改起來最簡單筷凤,但隱患卻是最大的。還有一種修改方式:
@Test
public void SrpTest() {
Animal animal = new Animal();
animal.move("牛");
animal.move("羊");
animal.move("魚");
}
private class Animal {
private void move(String animal) {
System.out.println(animal + "奔跑");
}
private void move2(String animal) {
System.out.println(animal + "在水里游");
}
}
運(yùn)行結(jié)果:
牛奔跑
羊奔跑
魚在水里游
可以看到苞七,這種修改方式?jīng)]有改動(dòng)原來的方法藐守,而是在類中新加了一個(gè)方法,這樣雖然也違背了單一職責(zé)原則蹂风,但在方法級(jí)別上卻是符合單一職責(zé)原則的卢厂,因?yàn)樗]有動(dòng)原來方法的代碼。這三種方式各有優(yōu)缺點(diǎn)惠啄,那么在實(shí)際編程中慎恒,采用哪一中呢任内?其實(shí)這真的比較難說,需要根據(jù)實(shí)際情況來確定融柬。我的原則是:只有邏輯足夠簡單死嗦,才可以在代碼級(jí)別上違反單一職責(zé)原則;只有類中方法數(shù)量足夠少粒氧,才可以在方法級(jí)別上違反單一職責(zé)原則越走;
遵循單一職責(zé)原可以降低類的復(fù)雜度(一個(gè)類只負(fù)責(zé)一項(xiàng)職責(zé),其邏輯肯定要比負(fù)責(zé)多項(xiàng)職責(zé)簡單的多)靠欢;提高類的可讀性,提高系統(tǒng)的可維護(hù)性铜跑;降低變更引起的風(fēng)險(xiǎn)门怪;
如何劃分一個(gè)類、一個(gè)函數(shù)的職責(zé)锅纺,每個(gè)人都有自己的看法掷空,但是它也是有一些基本的指導(dǎo)原則的:
- 兩個(gè)完全不一樣的功能就不應(yīng)該放在同一個(gè)類中;
- 一個(gè)類中應(yīng)該是一組相關(guān)性很高的函數(shù)、數(shù)據(jù)的封裝囤锉;
工程師不斷審視自己的代碼坦弟,根據(jù)具體的業(yè)務(wù)、功能對(duì)類進(jìn)行相應(yīng)的拆分官地,這是工程師優(yōu)化代碼邁出的第一步酿傍。
讓程序更穩(wěn)定、更靈活-開閉原則OCP
開閉原則(Open-Closed Principle, OCP):軟件中的對(duì)象(類驱入、模塊赤炒、函數(shù)等)應(yīng)當(dāng)對(duì)擴(kuò)展開放,對(duì)修改關(guān)閉亏较。即軟件實(shí)體應(yīng)盡量在不修改原有代碼的情況下進(jìn)行擴(kuò)展莺褒。
經(jīng)典問題:
在軟件的生命周期內(nèi),因?yàn)樽兓┣椤⑸?jí)和維護(hù)等原因需要對(duì)軟件原有代碼進(jìn)行修改時(shí)遵岩,可能會(huì)將錯(cuò)誤引入原本已測試通過的代碼,也可能會(huì)使我們不得不對(duì)整個(gè)功能進(jìn)行重構(gòu)巡通,并且需要原有代碼經(jīng)過重新測試尘执。如何確保原有軟件模塊的正確性,以及盡量少的影響原有模塊扁达?
解決方案:
遵循單一職責(zé)原則正卧。當(dāng)軟件需要變化時(shí),盡量通過擴(kuò)展軟件實(shí)體的行為來實(shí)現(xiàn)變化跪解,而不是通過修改已有的代碼來實(shí)現(xiàn)變化炉旷。
用抽象構(gòu)建框架签孔,用實(shí)現(xiàn)擴(kuò)展細(xì)節(jié)。為了滿足開閉原則窘行,需要對(duì)系統(tǒng)進(jìn)行抽象化設(shè)計(jì)饥追,抽象化是開閉原則的關(guān)鍵。我們?yōu)橄到y(tǒng)定義一個(gè)相對(duì)穩(wěn)定的抽象層罐盔,通過接口但绕、抽象類等機(jī)制將不同的實(shí)現(xiàn)行為移至具體的實(shí)現(xiàn)層中完成。如果需要修改系統(tǒng)的行為,無須對(duì)抽象層進(jìn)行任何改動(dòng)店雅,只需要增加新的具體類來實(shí)現(xiàn)新的業(yè)務(wù)功能即可偶芍,實(shí)現(xiàn)在不修改已有代碼的基礎(chǔ)上擴(kuò)展系統(tǒng)的功能,達(dá)到開閉原則的要求幅骄。開閉原則是面向?qū)ο蟮目蓮?fù)用設(shè)計(jì)的第一塊基石,它是最重要的面向?qū)ο笤O(shè)計(jì)原則.
舉例說明本今,還是描述動(dòng)物運(yùn)動(dòng)的場景:
@Test
public void ocpTest() {
Animal animal = new Animal();
animal.move("terrestrial", "牛");
animal.move("terrestrial", "羊");
animal.move("aquatic", "魚");
}
private class Animal {
// 這里做個(gè)經(jīng)典的示范
private void move(String type, String animal) {
if (type.equals("terrestrial")) {
Terrestrial terrestrial = new Terrestrial(animal);
terrestrial.move();
} else if (type.equals("aquatic")) {
Aquatic aquatic = new Aquatic(animal);
aquatic.move();
}
}
}
private class Terrestrial {
private String mTerrestrial;
Terrestrial(String animal) {
mTerrestrial = animal;
}
@Override
public void move() {
System.out.println(mTerrestrial + "奔跑");
}
}
private class Aquatic {
private String mAquatic;
Aquatic(String animal) {
mAquatic = animal;
}
@Override
public void move() {
System.out.println(mAquatic + "在水里游");
}
}
但是如果有一天我發(fā)現(xiàn)我需要要添加鳥這種需求拆座,因?yàn)槲也荒苷f鳥是在水里游的,此時(shí)就需要要修改Animal類的move()方法的源代碼冠息,增加新的判斷邏輯挪凑,這就違反了開閉原則,對(duì)于拓展是開放的逛艰,但對(duì)于修改是封閉的躏碳。
竟然所有的動(dòng)物都可以移動(dòng),那么我們是否可以總結(jié)抽象出動(dòng)物都可以移動(dòng)這一特性瓮孙?
@Test
public void ocpTest() {
Animal animalTerrestrial = new Terrestrial("牛");
animalTerrestrial.movement();
Animal animalAquatic = new Terrestrial("魚");
animalAquatic.movement();
}
// 這里是通過抽象類的方法實(shí)現(xiàn)唐断,接口也可以實(shí)現(xiàn)
// 使用接口還是抽象類請(qǐng)根據(jù)實(shí)際情況來選擇
private abstract class Animal {
abstract void move();
private void movement() {
move();
}
}
private class Terrestrial extends Animal {
private String mTerrestrial;
Terrestrial(String animal) {
mTerrestrial = animal;
}
@Override
public void move() {
System.out.println(mTerrestrial + "奔跑");
}
}
private class Aquatic extends Animal {
private String mAquatic;
Aquatic(String animal) {
mAquatic = animal;
}
@Override
public void move() {
System.out.println(mAquatic + "在水里游");
}
}
private class Celestial extends Animal{
private String mCelestial;
Celestial(String animal) {
mCelestial = animal;
}
@Override
public void move() {
System.out.println(mCelestial + "在天空飛");
}
}
這里只是為了突出對(duì)于拓展是開放的,但對(duì)于修改是封閉的一特性杭抠,如果想要繼續(xù)優(yōu)化則涉及到工廠模式方面脸甘,這里就先不延伸下去了。
為什么使用開閉原則偏灿?
- 開閉原則是最基礎(chǔ)的設(shè)計(jì)原則丹诀,其它的五個(gè)設(shè)計(jì)原則都是開閉原則的具體形態(tài),也就是說其它的五個(gè)設(shè)計(jì)原則是指導(dǎo)設(shè)計(jì)的工具和方法翁垂,而開閉原則才是其精神領(lǐng)袖铆遭。
- 開閉原則可以提高軟件的維護(hù)性和拓展性,符合開閉原則的代碼更易讀懂理解沿猜,對(duì)于拓展也不需去修改一個(gè)類枚荣,而是新增一個(gè)類,減少了出錯(cuò)的可能性啼肩。
- 面向?qū)ο箝_發(fā)的要求橄妆,萬物皆在發(fā)展變化衙伶,有變化就要有策略去應(yīng)對(duì),在設(shè)計(jì)之初考慮到可能會(huì)變化的因素害碾,抽象出對(duì)應(yīng)的特性矢劲,將“可能”轉(zhuǎn)變?yōu)椤皩?shí)際”。
遵守開閉原則的重要手段應(yīng)該是通過抽象慌随;當(dāng)軟件需要變化時(shí)芬沉,應(yīng)該盡量通過擴(kuò)展的方式來實(shí)現(xiàn)變化,而不是通過修改已有的代碼來實(shí)現(xiàn)阁猜。
“應(yīng)該盡量”
說明OCP原則并不是說絕對(duì)不可以修改原始類丸逸,當(dāng)我們嗅到代碼“腐朽氣味”時(shí),應(yīng)盡早的重構(gòu)剃袍,而不是通過繼承等方式添加新的實(shí)現(xiàn)椭员,這會(huì)導(dǎo)致類型的膨脹以及歷史遺留代碼的冗余;實(shí)際的開發(fā)過程也沒那么理想化完全的不需要修改原理的代碼笛园,因此,在開發(fā)過程中要結(jié)合實(shí)際的具體的情況去進(jìn)行考量侍芝,是通過修改舊代碼還是通過繼承使得軟件系統(tǒng)更穩(wěn)定研铆、更靈活;
構(gòu)建擴(kuò)展性更好的系統(tǒng)-里氏替換原則LSP
里氏代換原則(Liskov Substitution Principle, LSP):
定義1:如果對(duì)每一個(gè)類型為 T1的對(duì)象 o1州叠,都有類型為 T2 的對(duì)象o2棵红,使得以 T1定義的所有程序 P 在所有的對(duì)象 o1 都代換成 o2 時(shí),程序 P 的行為沒有發(fā)生變化咧栗,那么類型 T2 是類型 T1 的子類型逆甜。
定義2:所有引用基類的地方必須能透明地使用其子類的對(duì)象。
經(jīng)典問題:
有一功能P1致板,由類A完成〗簧罚現(xiàn)需要將功能P1進(jìn)行擴(kuò)展,擴(kuò)展后的功能為P斟或,其中P由原有功能P1與新功能P2組成素征。新功能P由類A的子類B來完成,則子類B在完成新功能P2的同時(shí)萝挤,有可能會(huì)導(dǎo)致原有功能P1發(fā)生故障御毅。
解決方案:
當(dāng)使用繼承時(shí),遵循里氏替換原則怜珍。類B繼承類A時(shí)端蛆,除添加新的方法完成新增功能P2外,盡量不要重寫父類A的方法酥泛,也盡量不要重載父類A的方法今豆。
里氏代換原則告訴我們嫌拣,在軟件中將一個(gè)基類對(duì)象替換成它的子類對(duì)象,程序?qū)⒉粫?huì)產(chǎn)生任何錯(cuò)誤和異常晚凿,反過來則不成立亭罪,如果一個(gè)軟件實(shí)體使用的是一個(gè)子類對(duì)象的話,那么它不一定能夠使用基類對(duì)象歼秽。例如:我喜歡動(dòng)物应役,那我一定喜歡狗,因?yàn)楣肥莿?dòng)物的子類燥筷;但是我喜歡狗箩祥,不能據(jù)此斷定我喜歡動(dòng)物,因?yàn)槲也⒉幌矚g老鼠肆氓,雖然它也是動(dòng)物袍祖。說了那么多,其實(shí)最終總結(jié)就兩個(gè)字:抽象谢揪;
里氏代換原則是實(shí)現(xiàn)開閉原則的重要方式之一蕉陋,由于使用基類對(duì)象的地方都可以使用子類對(duì)象,因此在程序中盡量使用基類類型來對(duì)對(duì)象進(jìn)行定義拨扶,而在運(yùn)行時(shí)再確定其子類類型凳鬓,用子類對(duì)象來替換父類對(duì)象(上面的例子也體現(xiàn)了這一點(diǎn))。
里氏替換原則的核心原理是抽象患民,抽象又依賴于繼承這個(gè)特性缩举,在OOP中,繼承的優(yōu)缺點(diǎn)都相當(dāng)?shù)拿黠@匹颤。
繼承的優(yōu)點(diǎn):
- 代碼重用仅孩,減少創(chuàng)建類的成本,每個(gè)子類都擁有父類的方法和屬性印蓖;
- 子類和父類基本相似辽慕,但又與父類有所區(qū)別;
- 提高代碼的可拓展性赦肃;
繼承的缺點(diǎn):
- 繼承是侵入性的鼻百,只要繼承就必須擁有父類所有的屬性和方法;
- 可能造成子類代碼冗余摆尝,靈活性減低温艇;
- 如果一個(gè)類被其他的類所繼承,則當(dāng)這個(gè)類需要修改時(shí)堕汞,必須考慮到所有的子類勺爱,并且父類修改后,所有涉及到子類的功能都有可能會(huì)產(chǎn)生故障讯检。
繼承包含這樣一層含義:父類中凡是已經(jīng)實(shí)現(xiàn)好的方法(相對(duì)于非抽象方法而言)琐鲁,實(shí)際上是在設(shè)定一系列的規(guī)范和契約卫旱,雖然它不強(qiáng)制要求所有的子類必須遵從這些契約,但是如果子類對(duì)這些非抽象方法任意修改围段,就會(huì)對(duì)整個(gè)繼承體系造成破壞顾翼。而里氏替換原則就是表達(dá)了這一層含義。
舉例說明繼承的風(fēng)險(xiǎn)奈泪,我們需要完成一個(gè)兩數(shù)相減的功能适贸,由類A來負(fù)責(zé)。
@Test
public void lspTest() {
A a = new A();
System.out.println("100 + 50 = " + a.add(100, 50));
}
private class A {
public int add(int a, int b) {
return a + b;
}
}
運(yùn)行結(jié)果:
100 + 50 = 150
后來涝桅,我們需要增加一個(gè)新的功能:完成兩數(shù)相加拜姿,然后再與100求和,由類B來負(fù)責(zé)冯遂。由于類A已經(jīng)實(shí)現(xiàn)了第一個(gè)功能蕊肥,所以類B繼承類A后,只需要再完成第二個(gè)功能就可以了蛤肌,代碼如下:
@Test
public void lspTest() {
A a = new A();
System.out.println("100 + 50 = " + a.add(100, 50));
B b = new B();
System.out.println("100 + 50 = " + b.add(100, 50));
System.out.println("100 + 50 + 100 = " + b.minus(100, 50));
}
private class A {
public int add(int a, int b) {
return a + b;
}
}
private class B extends A {
public int add(int a, int b) {
// a 不再是加上 b
return a - b;
}
private int minus(int a, int b) {
return add(a , b) + 100;
}
}
運(yùn)行結(jié)果:
100 + 50 = 150
100 + 50 = 50
100 + 50 + 100 = 150
我們發(fā)現(xiàn)原本運(yùn)行正常的相加功能發(fā)生了錯(cuò)誤壁却。原因就是類B在給方法起名時(shí)無意中重寫了父類的方法,造成所有運(yùn)行相加功能的代碼全部調(diào)用了類B重寫后的方法裸准,造成原本運(yùn)行正常的功能出現(xiàn)了錯(cuò)誤儒洛。
在本例中,引用基類A完成的功能狼速,換成子類B之后,發(fā)生了異常卦停。在實(shí)際編程中向胡,我們常常會(huì)通過重寫父類的方法來完成新的功能,這樣寫起來雖然簡單惊完,但是整個(gè)繼承體系的可復(fù)用性會(huì)比較差僵芹,特別是運(yùn)用多態(tài)比較頻繁時(shí),程序運(yùn)行出錯(cuò)的幾率非常大小槐。如果非要重寫父類的方法拇派,比較通用的做法是:原來的父類和子類都繼承一個(gè)更通俗的基類,原有的繼承關(guān)系去掉凿跳,采用依賴件豌、聚合,組合等關(guān)系代替控嗜。
里氏替換原則通俗的來講就是:子類可以擴(kuò)展父類的功能茧彤,但不能改變父類原有的功能。
里氏代換原則是實(shí)現(xiàn)開閉原則的重要方式之一疆栏。除了父類外曾掂,在傳遞參數(shù)時(shí)使用基類對(duì)象惫谤,在定義成員變量、定義局部變量珠洗、確定方法返回類型時(shí)都可使用里氏代換
原則溜歪,針對(duì)基類編程,在程序運(yùn)行時(shí)再確定具體子類许蓖。
讓項(xiàng)目擁有變化的能力-依賴倒置原則DIP
依賴倒轉(zhuǎn)原則(Dependency Inversion Principle, DIP):高層模塊不應(yīng)該依賴底層模塊蝴猪,兩者都應(yīng)該依賴其抽象,抽象不應(yīng)該依賴于細(xì)節(jié)蛔糯,細(xì)節(jié)應(yīng)當(dāng)依賴于抽象拯腮。
經(jīng)典問題:
類A直接依賴類B,假如要將類A改為依賴類C蚁飒,則必須通過修改類A的代碼來達(dá)成动壤。這種場景下,類A一般是高層模塊淮逻,負(fù)責(zé)復(fù)雜的業(yè)務(wù)邏輯琼懊;類B和類C是低層模塊,負(fù)責(zé)基本的原子操作爬早;假如修改類A哼丈,會(huì)給程序帶來不必要的風(fēng)險(xiǎn)。
解決方案:
將類A修改為依賴接口I筛严,類B和類C各自實(shí)現(xiàn)接口I醉旦,類A通過接口I間接與類B或者類C發(fā)生聯(lián)系,則會(huì)大大降低修改類A的幾率桨啃。
如果說開閉原則是面向?qū)ο笤O(shè)計(jì)的目標(biāo)的話车胡,那么依賴倒轉(zhuǎn)原則就是面向?qū)ο笤O(shè)計(jì)的主要實(shí)現(xiàn)機(jī)制之一,它是系統(tǒng)抽象化的具體實(shí)現(xiàn).在Java中照瘾,抽象就是指接口或抽象類匈棘,兩者都是不能被實(shí)例化的;細(xì)節(jié)就是實(shí)現(xiàn)類析命,實(shí)現(xiàn)接口或繼承抽象類而產(chǎn)生的類就是細(xì)節(jié)主卫,它是可以被實(shí)例化的;
依賴倒置原則在Java語言中的表現(xiàn)為:模塊間的依賴通過抽象發(fā)生鹃愤,實(shí)現(xiàn)類之間不發(fā)生直接的依賴關(guān)系簇搅,其依賴關(guān)系是通過接口或抽象類產(chǎn)生的;
依賴倒轉(zhuǎn)原則要求我們?cè)诔绦虼a中傳遞參數(shù)時(shí)或在關(guān)聯(lián)關(guān)系中软吐,盡量引用層次高的抽象層類馍资,即使用接口和抽象類進(jìn)行變量類型聲明、參數(shù)類型聲明、方法返回類型聲明鸟蟹,以及數(shù)據(jù)類型的轉(zhuǎn)換等乌妙,而不要用具體類來做這些事情。
在實(shí)現(xiàn)依賴倒轉(zhuǎn)原則時(shí)建钥,我們需要針對(duì)抽象層編程藤韵,而將具體類的對(duì)象通過依賴注入(DependencyInjection,DI)的方式注入到其他對(duì)象中,依賴注入是指當(dāng)一個(gè)對(duì)象要與其他對(duì)象發(fā)生依賴關(guān)系時(shí)熊经,通過抽象來注入所依賴的對(duì)象泽艘。
常用的注入方式有三種:
- 構(gòu)造注入,構(gòu)造注入是指通過構(gòu)造函數(shù)來傳入具體類的對(duì)象;
- 設(shè)值注入,設(shè)值注入是指通過Setter方法來傳入具體類的對(duì)象;
- 接口注入,接口注入是指通過在接口中聲明的業(yè)務(wù)方法來傳入具體類的對(duì)象;
這些方法在定義時(shí)使用的是抽象類型镐依,在運(yùn)行時(shí)再傳入具體類型的對(duì)象匹涮,由子類對(duì)象來覆蓋父類對(duì)象。
依賴倒置原則基于這樣一個(gè)事實(shí):相對(duì)于細(xì)節(jié)的多變性槐壳,抽象的東西要穩(wěn)定的多然低。以抽象為基礎(chǔ)搭建起來的架構(gòu)比以細(xì)節(jié)為基礎(chǔ)搭建起來的架構(gòu)要穩(wěn)定的多。
依賴倒置原則的核心思想是面向接口編程务唐,我們依舊用一個(gè)例子來說明面向接口編程比相對(duì)于面向?qū)崿F(xiàn)編程好在什么地方雳攘。假設(shè)當(dāng)前場景為讀文章:
@Test
public void dipTest() {
Writer writer = new Writer();
Book book = new Book();
writer.read(book);
}
private class Book {
private String getContent() {
return "讀書";
}
}
private class Writer {
private void read(Book book) {
System.out.println("作家" + book.getContent());
}
}
運(yùn)行結(jié)果:作家讀書
假如有一天,需求變成這樣:作家讀書枫笛,也可以讀報(bào)紙吨灭,報(bào)紙的代碼如下:
private class Newspaper {
private String getContent() {
return "讀報(bào)紙";
}
}
private class Writer {
private void read(Book book) {
System.out.println("作家" + book.getContent());
}
private void read(Newspaper newspaper) {
System.out.println("作家" + newspaper.getContent());
}
}
運(yùn)行結(jié)果:
作家讀書
作家讀報(bào)紙
僅僅是添加閱讀報(bào)紙的功能就需要去修改Writer類,以后還要讀雜志刑巧、小說等等的怎么辦呢喧兄?這顯然不是好的設(shè)計(jì),因?yàn)榫褪荳riter與Book啊楚、Newspaper之間的耦合性太高了(這里是很明顯違背了開閉原則)
下面我們引入一個(gè)抽象的接口IReader來降低他們之間的耦合度
@Test
public void dipTest() {
Writer writer = new Writer();
writer.read(new Book());
writer.read(new Newspaper());
}
private interface IReader {
String getContent();
}
private class Book implements IReader{
@Override
public String getContent() {
return "讀書";
}
}
private class Newspaper implements IReader{
@Override
public String getContent() {
return "讀報(bào)紙";
}
}
運(yùn)行結(jié)果:
作家讀書
作家讀報(bào)紙
這只是一個(gè)簡單的例子吠冤,實(shí)際情況中,代表高層模塊的Writer類將負(fù)責(zé)完成主要的業(yè)務(wù)邏輯特幔,一旦需要對(duì)它進(jìn)行修改,引入錯(cuò)誤的風(fēng)險(xiǎn)極大闸昨。所以遵循依賴倒置原則可以降低類之間的耦合性蚯斯,提高系統(tǒng)的穩(wěn)定性,降低修改程序造成的風(fēng)險(xiǎn)饵较。
注: 這里的Writer類也可以說是表明一種職業(yè)拍嵌,有興趣的話可以將它再抽象化;
依賴倒置原則的核心就是要我們面向接口編程循诉。采用依賴倒置原則給多人并行開發(fā)帶來了極大的便利横辆,比如上例中,原本W(wǎng)riter類與Book類直接耦合時(shí)茄猫,Writer類必須等Book類編碼完成后才可以進(jìn)行編碼狈蚤,因?yàn)閃riter類直接依賴于Book類困肩。修改后的程序則可以同時(shí)開工,互不影響脆侮,因?yàn)閃riter與Book類一點(diǎn)關(guān)系也沒有锌畸。參與協(xié)作開發(fā)的人越多、項(xiàng)目越龐大靖避,采用依賴導(dǎo)致原則的意義就越重大潭枣,現(xiàn)在很流行的TDD開發(fā)模式就是依賴倒置原則最成功的應(yīng)用。
系統(tǒng)有更高的靈活性-接口隔離原則ISP
接口隔離原則(Interface Segregation Principle, ISP):
定義一:客戶端不應(yīng)該依賴那些它不需要的接口幻捏;
定義二:類間的依賴關(guān)系應(yīng)該建立在最小的接口上盆犁;
經(jīng)典問題:
類A通過接口I依賴類B,類C通過接口I依賴類D篡九,如果接口I對(duì)于類A和類B來說不是最小接口谐岁,則類B和類D必須去實(shí)現(xiàn)他們不需要的方法。
解決方案:
將臃腫的接口I拆分為獨(dú)立的幾個(gè)接口瓮下,類A和類C分別與他們需要的接口建立依賴關(guān)系翰铡。也就是采用接口隔離原則。
接口原則將非常龐大讽坏、臃腫的接口拆分為更小更具體的接口锭魔,這樣客戶端只需要知道他們感興趣的方法;接口隔離原則目的是系統(tǒng)解開耦合路呜,從而更容易重構(gòu)迷捧、更改和重新部署;
接口隔離原則的含義是:建立單一接口胀葱,不要建立龐大臃腫的接口漠秋,盡量細(xì)化接口,接口中的方法盡量少抵屿。在面向?qū)ο缶幊陶Z言中庆锦,實(shí)現(xiàn)一個(gè)接口就需要實(shí)現(xiàn)該接口中定義的所有方法,因此最好能根據(jù)其職責(zé)不同分別放在不同的小接口中轧葛,以確保每個(gè)接口使用起來都較為方便搂抒,并都承擔(dān)某一單一角色。接口是設(shè)計(jì)時(shí)對(duì)外部設(shè)定的“契約”尿扯,通過分散定義多個(gè)接口求晶,可以預(yù)防外來變更的擴(kuò)散,提高系統(tǒng)的靈活性和可維護(hù)性衷笋。
類A依賴接口I中的方法1芳杏、方法2、方法3,類B是對(duì)類A依賴的實(shí)現(xiàn)爵赵。類C依賴接口I中的方法1吝秕、方法4、方法5亚再,類D是對(duì)類C依賴的實(shí)現(xiàn)郭膛。對(duì)于類B和類D來說,雖然他們都存在著用不到的方法(也就是圖中紅色字體標(biāo)記的方法)氛悬,但由于實(shí)現(xiàn)了接口I则剃,所以也必須要實(shí)現(xiàn)這些用不到的方法。
可以看出如果接口過于臃腫如捅,只要接口中出現(xiàn)的方法棍现,不管對(duì)依賴于它的類有沒有用處,實(shí)現(xiàn)類中都必須去實(shí)現(xiàn)這些方法镜遣,這顯然不是好的設(shè)計(jì)己肮。下面將這個(gè)設(shè)計(jì)修改為符合接口隔離原則的接口,我們需要對(duì)接口I進(jìn)行拆分悲关。
| 這里我就不舉例子了谎僻,上面兩幅圖已經(jīng)很清晰的體現(xiàn)了接口隔離原則;
采用接口隔離原則對(duì)接口進(jìn)行約束時(shí)寓辱,要注意以下幾點(diǎn):
- 需要注意控制接口的粒度艘绍,接口不能太小,如果太小會(huì)導(dǎo)致系統(tǒng)中接口泛濫秫筏,不利于維護(hù)诱鞠;接口也不能太大,太大的接口將違背接口隔離原則这敬;
- 一般而言航夺,接口中僅包含為某一類用戶定制的方法或是為依賴接口的類定制服務(wù),不應(yīng)該強(qiáng)迫客戶依賴于那些它們不用的方法崔涂,只有專注地為一個(gè)模塊提供定制服務(wù)阳掐,才能建立最小的依賴關(guān)系。
- 提高內(nèi)聚冷蚂,減少對(duì)外交互缭保。使接口用最少的方法去完成最多的事情。
接口隔離原則與單一職責(zé)原則的不同
- 單一職責(zé)原則注重的是職責(zé)帝雇;而接口隔離原則注重對(duì)接口依賴的隔離;
- 單一職責(zé)原則主要是約束類涮俄,其次才是接口和方法蛉拙,它針對(duì)的是程序中的實(shí)現(xiàn)和細(xì)節(jié)尸闸;而接口隔離原則主要約束接口接口,主要針對(duì)抽象,針對(duì)程序整體框架的構(gòu)建吮廉。
更好的可擴(kuò)展性-迪米特原則LOD
迪米特法則(Law of Demeter, LoD)苞尝,或稱最小知識(shí)原則(Least Knowledge Principle, LDP):一個(gè)對(duì)象應(yīng)該對(duì)其他對(duì)象有最少的了解。
經(jīng)典問題:
類與類之間的關(guān)系越密切宦芦,耦合度越大宙址,當(dāng)一個(gè)類發(fā)生改變時(shí),對(duì)另一個(gè)類的影響也越大调卑。
解決方案:
盡量降低類與類之間的耦合抡砂。
迪米特法則還有一個(gè)更簡單的定義:只與直接的朋友通信。
在迪米特法則中恬涧,對(duì)于一個(gè)對(duì)象注益,其朋友包括以下幾類:
- 當(dāng)前對(duì)象本身(this);
- 以參數(shù)形式傳入到當(dāng)前對(duì)象方法中的對(duì)象溯捆;
- 當(dāng)前對(duì)象的成員對(duì)象丑搔;
- 如果當(dāng)前對(duì)象的成員對(duì)象是一個(gè)集合,那么集合中的元素也都是朋友提揍;
- 當(dāng)前對(duì)象所創(chuàng)建的對(duì)象啤月。
迪米特法則要求我們?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è)方法的話售碳,可以通過第三者轉(zhuǎn)發(fā)這個(gè)調(diào)用强重。簡言之,就是通過引入一個(gè)合理的第三者來降低現(xiàn)有對(duì)象之間的耦合度贸人。
假設(shè)當(dāng)前場景為通過中介找房:
@Test
public void ispTest() {
Tenant tenant = new Tenant(18, 2720);
Mediator mediator = new Mediator();
tenant.rentRoom(mediator);
}
private class Room {
private int mPrice;
private int mArea;
Room(int price, int area) {
mPrice = price;
mArea = area;
}
@Override
public String toString() {
return "當(dāng)前房子" +
"房價(jià): " + mPrice +
", 房子面積: " + mArea +
'}';
}
}
// 中介
private class Mediator {
List<Room> mRooms = new ArrayList<Room>();
Mediator() {
for (int i = 0; i < 5; i++) {
mRooms.add(new Room(15 + i, (15 + i) * 150));
}
}
private List<Room> getRooms() {
return mRooms;
}
}
// 租客
private class Tenant {
private int mRoomPrice;
private int mRoomArea;
private int mDiffPrice = 10;
private int mDiffArea = 100;
Tenant(int price, int area) {
mRoomPrice = price;
mRoomArea = area;
}
private void rentRoom(Mediator mediator) {
List<Room> rooms = mediator.getRooms();
for (Room room: rooms) {
if (isSuitable(room)) {
System.out.println("租到房子了间景," + room.toString());
break;
}
}
}
private boolean isSuitable(Room room) {
return Math.abs(room.mPrice - mRoomPrice) < mDiffPrice
&& Math.abs(room.mArea - mRoomArea) < mDiffArea;
}
}
從上面的代碼可以看到Tenant類不僅依賴了Mediator類還頻繁的跟Room類打交道。當(dāng)Room變化時(shí)Tenant也會(huì)隨之變化艺智,而Tenant類又與Mediator類耦合倘要,這就出現(xiàn)了糾纏不清的關(guān)系。這個(gè)時(shí)候我們就需要分清誰是我們的朋友了
@Test
public void ispTest() {
Tenant tenant = new Tenant(18, 2720);
Mediator mediator = new Mediator();
tenant.rentRoom(mediator);
}
private class Room {
private int mPrice;
private int mArea;
Room(int price, int area) {
mPrice = price;
mArea = area;
}
@Override
public String toString() {
return "當(dāng)前房子" +
"房價(jià): " + mPrice +
", 房子面積: " + mArea +
'}';
}
}
// 中介
private class Mediator {
private int mDiffPrice = 10;
private int mDiffArea = 100;
List<Room> mRooms = new ArrayList<Room>();
Mediator() {
for (int i = 0; i < 5; i++) {
mRooms.add(new Room(15 + i, (15 + i) * 150));
}
}
private Room rent(int price, int area) {
for (Room room : mRooms) {
if (isSuitable(room, price, area)) {
System.out.println("租到房子了十拣," + room.toString());
return room;
}
}
return new Room(0, 0);
}
private boolean isSuitable(Room room, int price, int area) {
return Math.abs(room.mPrice - price) < mDiffPrice
&& Math.abs(room.mArea - area) < mDiffArea;
}
}
// 租客
private class Tenant {
private int mRoomPrice;
private int mRoomArea;
Tenant(int price, int area) {
mRoomPrice = price;
mRoomArea = area;
}
private void rentRoom(Mediator mediator) {
mediator.rent(mRoomPrice, mRoomArea);
}
}
這里是將Room的判斷操作移到了Mediator類中封拧,這本該是Mediator類的職責(zé),根據(jù)租客的設(shè)定條件查找房子夭问,并將結(jié)果返回給租客泽西;租客不應(yīng)該知道太多房子的細(xì)節(jié),我們只需通過中介溝通就好缰趋,不需要跟房主等其他角色溝通捧杉,因?yàn)檫@些角色都不是我們的直接朋友陕见。“只與直接的朋友通信”能將我們從復(fù)雜的關(guān)系網(wǎng)中抽離出來味抖,使程序耦合性更低评甜,更穩(wěn)定;
在將迪米特法則運(yùn)用到系統(tǒng)設(shè)計(jì)中時(shí)仔涩,要注意下面的幾點(diǎn):
- 在類的劃分上忍坷,應(yīng)當(dāng)盡量創(chuàng)建松耦合的類,類之間的耦合度越低熔脂,就越有利于復(fù)用佩研,一個(gè)處在松耦合中的類一旦被修改,不會(huì)對(duì)關(guān)聯(lián)的類造成太大波及霞揉;
- 在類的結(jié)構(gòu)設(shè)計(jì)上韧骗,每一個(gè)類都應(yīng)當(dāng)盡量降低其成員變量和成員函數(shù)的訪問權(quán)限;
- 在類的設(shè)計(jì)上零聚,只要有可能袍暴,一個(gè)類型應(yīng)當(dāng)設(shè)計(jì)成不變類;在對(duì)其他類的引用上隶症,一個(gè)對(duì)象對(duì)其他對(duì)象的引用應(yīng)當(dāng)降到最低政模。
迪米特法則的做法觀念就是類間解耦,弱耦合蚂会,只有弱耦合了以后淋样,類的復(fù)用率才可以提高,其要求的結(jié)果就是產(chǎn)生了大量的中轉(zhuǎn)或跳轉(zhuǎn)類胁住,導(dǎo)致的復(fù)雜性提高趁猴,同時(shí)也為維護(hù)帶來了難度,所以在采用迪米特法則時(shí)需要反復(fù)權(quán)衡彪见,既做到讓結(jié)構(gòu)清晰儡司,又做到高內(nèi)聚低耦合。
但是過度使用迪米特法則余指,也會(huì)造成系統(tǒng)的不同模塊之間的通信效率降低捕犬,使系統(tǒng)的不同模塊之間不容易協(xié)調(diào)等缺點(diǎn)。同時(shí)酵镜,因?yàn)榈厦滋胤▌t要求類與類之間盡量不直接通信碉碉,如果類之間需要通信就通過第三方轉(zhuǎn)發(fā)的方式,這就直接導(dǎo)致了系統(tǒng)中存在大量的中介類淮韭,這些類存在的唯一原因是為了傳遞類與類之間的相互調(diào)用關(guān)系垢粮,這就毫無疑問的增加了系統(tǒng)的復(fù)雜度。解決這個(gè)問題的方式是:使用依賴倒轉(zhuǎn)原則(通俗的講就是要針對(duì)接口編程靠粪,不要針對(duì)具體編程)蜡吧, 這要就可以是調(diào)用方和被調(diào)用方之間有了一個(gè)抽象層粱腻,被調(diào)用方在遵循抽象層的前提下就可以自由的變化,此時(shí)抽象層成了調(diào)用方的朋友斩跌。
總結(jié)
單一職責(zé)原則告訴我們實(shí)現(xiàn)類要職責(zé)單一;
里氏替換原則告訴我們不要破壞繼承體系捞慌;
依賴倒置原則告訴我們要面向接口編程耀鸦;
接口隔離原則告訴我們?cè)谠O(shè)計(jì)接口的時(shí)候要精簡單一;
迪米特法則告訴我們要降低耦合啸澡。
而開閉原則是總綱袖订,他告訴我們要對(duì)擴(kuò)展開放,對(duì)修改關(guān)閉嗅虏。
最后說明一下如何去遵守這六個(gè)原則洛姑。對(duì)于六個(gè)原則的遵守并非是和否的問題,而是多和少的問題皮服,也就是說楞艾,一般我們只會(huì)說遵守程度是否合理,是否很好的平衡了各個(gè)原則達(dá)到一個(gè)較優(yōu)的解決方案龄广。任何事都是過猶不及硫眯,設(shè)計(jì)模式的六個(gè)設(shè)計(jì)原則也是一樣,我們需要的是理解六個(gè)原則的思想及原因择同,根據(jù)實(shí)際的情況靈活的運(yùn)用他們两入,而不是刻意和死板的去遵守它們;
一千個(gè)讀者眼中有一千個(gè)哈姆雷特敲才,如果大家對(duì)這六項(xiàng)原則的理解跟我有所不同或是我哪里理解錯(cuò)誤裹纳,歡迎留言,大家共同探討紧武。文中例子源碼:請(qǐng)點(diǎn)擊這里
查考文獻(xiàn):
- lovelion:劉偉技術(shù)博客
- 憤怒的韭菜:卡奴達(dá)摩的專欄
- Android 源碼設(shè)計(jì)模式解析與實(shí)戰(zhàn)第2版