本文集網(wǎng)絡(luò)上文章及自己coding和理解的結(jié)果而來(lái)栈暇,是設(shè)計(jì)模式學(xué)習(xí)的開(kāi)篇宁脊。
本文介紹設(shè)計(jì)模式的一些概念,分類(lèi)撕彤,和設(shè)計(jì)原則鱼鸠。
概念:
設(shè)計(jì)模式(Design Patterns)是一套被反復(fù)使用、多數(shù)人知曉的羹铅、經(jīng)過(guò)分類(lèi)的蚀狰、代碼設(shè)計(jì)經(jīng)驗(yàn)的總結(jié)。使用設(shè)計(jì)模式是為了可重用代碼职员、讓代碼更容易被他人理解麻蹋、保證代碼可靠性。毫無(wú)疑問(wèn)焊切,設(shè)計(jì)模式于己于他人于系統(tǒng)都是多贏的扮授,設(shè)計(jì)模式使代碼編制真正工程化芳室,設(shè)計(jì)模式是軟件工程的基石,如同大廈的一塊塊磚石一樣刹勃。項(xiàng)目中合理的運(yùn)用設(shè)計(jì)模式可以完美的解決很多問(wèn)題堪侯,每種模式在現(xiàn)在中都有相應(yīng)的原理來(lái)與之對(duì)應(yīng),每一個(gè)模式描述了一個(gè)在我們周?chē)粩嘀貜?fù)發(fā)生的問(wèn)題深夯,以及該問(wèn)題的核心解決方案抖格,這也是它能被廣泛應(yīng)用的原因。
分類(lèi):
總共23種設(shè)計(jì)模式咕晋,分為三類(lèi):
創(chuàng)建型模式雹拄,共五種:工廠方法模式、抽象工廠模式掌呜、單例模式滓玖、建造者模式、原型模式质蕉。
結(jié)構(gòu)型模式势篡,共七種:適配器模式、裝飾器模式模暗、代理模式禁悠、外觀模式、橋接模式兑宇、組合模式碍侦、享元模式。
行為型模式隶糕,共十一種:策略模式瓷产、模板方法模式、觀察者模式枚驻、迭代子模式濒旦、責(zé)任鏈模式、命令模式再登、備忘錄模式尔邓、狀態(tài)模式、訪(fǎng)問(wèn)者模式霎冯、中介者模式铃拇、解釋器模式。
設(shè)計(jì)原則:
- 開(kāi)閉原則(Open Close Principle)
開(kāi)閉原則就是說(shuō)對(duì)擴(kuò)展開(kāi)放沈撞,對(duì)修改關(guān)閉慷荔。在程序需要進(jìn)行拓展的時(shí)候,不能去修改原有的代碼,實(shí)現(xiàn)一個(gè)熱插拔的效果显晶。所以一句話(huà)概括就是:為了使程序的擴(kuò)展性好贷岸,易于維護(hù)和升級(jí)。想要達(dá)到這樣的效果磷雇,我們需要使用接口和抽象類(lèi)偿警,后面的具體設(shè)計(jì)中我們會(huì)提到這點(diǎn)。
下面我們以簡(jiǎn)單工廠和工廠方法為例唯笙,體會(huì)下開(kāi)閉原則:
- 簡(jiǎn)單工廠
/**
* 功能簡(jiǎn)述: 運(yùn)算符抽象類(lèi)螟蒸,輸入兩個(gè)整數(shù),返回結(jié)果
*
* @author
*/
public abstract class Operation {
protected int numA;
protected int numB;
public abstract int getResult();
}
/**
* 功能簡(jiǎn)述: 加法實(shí)現(xiàn)類(lèi)
*
* @author
*/
public class OperationAdd extends Operation {
@Override
public int getResult() {
return numA + numB;
}
}
/**
* 功能簡(jiǎn)述: 減法實(shí)現(xiàn)類(lèi)
*
* @author
*/
public class OperationSub extends Operation {
@Override
public int getResult() {
return numA - numB;
}
}
/**
* 功能簡(jiǎn)述: 簡(jiǎn)單工廠類(lèi)
*
* @author
*/
public class OperationFactory {
public static Operation createOperate(String operate) {
Operation oper = null;
switch (operate) {
case "+":
oper = new OperationAdd();
break;
case "-":
oper = new OperationSub();
break;
}
return oper;
}
}
/**
* 功能簡(jiǎn)述: 客戶(hù)端調(diào)用
*
* @author
*/
public class Client {
public static void main(String[] args) {
//輸出3
Operation oper = OperationFactory.createOperate("+");
if (null != oper) {
oper.numA = 1;
oper.numB = 2;
System.out.println(oper.getResult());
}
//-----------------------------------------------------------------------
//輸出-1
oper = OperationFactory.createOperate("-");
if (null != oper) {
oper.numA = 1;
oper.numB = 2;
System.out.println(oper.getResult());
}
}
}
上述的代碼是簡(jiǎn)單工廠的一個(gè)例子崩掘,比較簡(jiǎn)單七嫌。下面我們需要新增一個(gè)乘法的實(shí)現(xiàn)類(lèi),我們需要怎么做苞慢?首先诵原,需要寫(xiě)一個(gè)OperationMul繼承Operation,實(shí)現(xiàn)父類(lèi)中的getResult方法挽放,返回乘積绍赛;其次,需要在工廠類(lèi)OperationFactory中新增一段case語(yǔ)句辑畦,當(dāng)case"*"號(hào)時(shí)返回OperationMul實(shí)現(xiàn)吗蚌;最后,在client方法中測(cè)試實(shí)現(xiàn)纯出。
/**
* 功能簡(jiǎn)述: 乘法實(shí)現(xiàn)類(lèi)
*
* @author
*/
public class OperationMul extends Operation {
@Override
public int getResult() {
return numA * numB;
}
}
/**
* 功能簡(jiǎn)述: 簡(jiǎn)單工廠類(lèi)褪测,改造,新增了對(duì)*的處理
*
* @author
*/
public class OperationFactory {
public static Operation createOperate(String operate) {
Operation oper = null;
switch (operate) {
case "+":
oper = new OperationAdd();
break;
case "-":
oper = new OperationSub();
break;
case "*":
oper = new OperationMul();
break;
}
return oper;
}
}
/**
* 功能簡(jiǎn)述: 客戶(hù)端調(diào)用
*
* @author
*/
public class Client {
public static void main(String[] args) {
//輸出3
Operation oper = OperationFactory.createOperate("+");
if (null != oper) {
oper.numA = 1;
oper.numB = 2;
System.out.println(oper.getResult());
}
//-----------------------------------------------------------------------
//輸出-1
oper = OperationFactory.createOperate("-");
if (null != oper) {
oper.numA = 1;
oper.numB = 2;
System.out.println(oper.getResult());
}
//-----------------------------------------------------------------------
//輸出2
oper = OperationFactory.createOperate("*");
if (null != oper) {
oper.numA = 1;
oper.numB = 2;
System.out.println(oper.getResult());
}
}
}
我們分析下上面的代碼潦刃,為了新增對(duì)"*"的處理,我們新增了一個(gè)類(lèi)懈叹,同時(shí)修改了一個(gè)類(lèi)乖杠,在這個(gè)例子中我們對(duì)新增開(kāi)放,同時(shí)也對(duì)修改開(kāi)放了澄成。因?yàn)樾薷牧撕?jiǎn)單工廠類(lèi)胧洒,雖然加的代碼不多,但為了驗(yàn)證代碼的正確性墨状,我們不得不回歸測(cè)試"+","-"的使用場(chǎng)景卫漫。
- 工廠方法
工廠方法是工廠模式的變體,遵循了開(kāi)閉原則肾砂,下面看代碼列赎。我們稍微簡(jiǎn)化下簡(jiǎn)單工廠的實(shí)現(xiàn)方法,一開(kāi)始我們只有一個(gè)"+"實(shí)現(xiàn)類(lèi)镐确,之后我們?cè)傩略?-"實(shí)現(xiàn)類(lèi)包吝。
/**
* 功能簡(jiǎn)述: 運(yùn)算符抽象類(lèi)饼煞,輸入兩個(gè)整數(shù),返回結(jié)果
*
* @author
*/
public abstract class Operation {
protected int numA;
protected int numB;
public abstract int getResult();
}
/**
* 功能簡(jiǎn)述: 加法實(shí)現(xiàn)類(lèi)
*
* @author
*/
public class OperationAdd extends Operation {
@Override
public int getResult() {
return numA + numB;
}
}
/**
* 功能簡(jiǎn)述: 抽象工廠類(lèi)诗越,此類(lèi)只定義接口砖瞧,不定義實(shí)現(xiàn),此為和簡(jiǎn)單工廠的區(qū)別之一
*
* @author
*/
public interface OperationFactory {
Operation createOperate();
}
/**
* 功能簡(jiǎn)述: 加法工廠類(lèi)
*
* @author
*/
public class OperationAddFactory implements OperationFactory {
@Override
public Operation createOperate() {
return new OperationAdd();
}
}
/**
* 功能簡(jiǎn)述: 客戶(hù)端調(diào)用
*
* @author
*/
public class Client {
public static void main(String[] args) {
OperationAddFactory addFactory = new OperationAddFactory();
Operation oper = addFactory.createOperate();
oper.numA = 1;
oper.numB = 2;
//輸出3
System.out.println(oper.getResult());
}
}
下面同樣嚷狞,我們來(lái)新增一個(gè)減法實(shí)現(xiàn)類(lèi)块促。
/**
* 功能簡(jiǎn)述: 減法實(shí)現(xiàn)類(lèi)
*
* @author
*/
public class OperationSub extends Operation {
@Override
public int getResult() {
return numA - numB;
}
}
/**
* 功能簡(jiǎn)述:減法工廠類(lèi)
*
* @author
*/
public class OperationSubFactory implements OperationFactory {
@Override
public Operation createOperate() {
return new OperationSub();
}
}
/**
* 功能簡(jiǎn)述: 測(cè)試實(shí)現(xiàn)
*
* @author
*/
public class Client {
/**
* 功能描述: <br>
*
* @param args
*/
public static void main(String[] args) {
OperationAddFactory addFactory = new OperationAddFactory();
Operation oper = addFactory.createOperate();
oper.numA = 1;
oper.numB = 2;
//輸出3
System.out.println(oper.getResult());
OperationSubFactory subFactory = new OperationSubFactory();
oper = subFactory.createOperate();
oper.numA = 1;
oper.numB = 2;
//輸出-1
System.out.println(oper.getResult());
}
}
同樣,我們分析下工廠方法模式床未,我們新增了一個(gè)減法實(shí)現(xiàn)類(lèi)竭翠,及減法工廠類(lèi),但并沒(méi)有修改加法相關(guān)的任何代碼即硼,真正做到了開(kāi)閉原則逃片。至于工廠類(lèi)的區(qū)別,那正是簡(jiǎn)單工廠和工廠方法的區(qū)別之一只酥,在工廠方法這種實(shí)現(xiàn)中褥实,不僅抽象了操作,還抽象了工廠裂允。
讀到這里损离,大家應(yīng)該對(duì)開(kāi)閉原則有個(gè)初步的印象了。
- 里氏代換原則(Liskov Substitution Principle)
里氏代換原則(Liskov Substitution Principle LSP)面向?qū)ο笤O(shè)計(jì)的基本原則之一绝编。
里氏代換原則中說(shuō)僻澎,任何基類(lèi)可以出現(xiàn)的地方,子類(lèi)一定可以出現(xiàn)十饥。
LSP是繼承復(fù)用的基石窟勃,只有當(dāng)衍生類(lèi)可以替換掉基類(lèi),軟件單位的功能不受到影響時(shí)逗堵,基類(lèi)才能真正被復(fù)用秉氧,而衍生類(lèi)也能夠在基類(lèi)的基礎(chǔ)上增加新的行為。里氏代換原則是對(duì)“開(kāi)-閉”原則的補(bǔ)充蜒秤。實(shí)現(xiàn)“開(kāi)-閉”原則的關(guān)鍵步驟就是抽象化汁咏。而基類(lèi)與子類(lèi)的繼承關(guān)系就是抽象化的具體實(shí)現(xiàn),所以里氏代換原則是對(duì)實(shí)現(xiàn)抽象化的具體步驟的規(guī)范作媚。
里氏替換原則包含以下4層含義:
- 子類(lèi)可以實(shí)現(xiàn)父類(lèi)的抽象方法攘滩,但是不能覆蓋父類(lèi)的非抽象方法。
- 子類(lèi)中可以增加自己特有的方法纸泡。
- 當(dāng)子類(lèi)覆蓋或?qū)崿F(xiàn)父類(lèi)的方法時(shí)漂问,方法的前置條件(即方法的形參)要比父類(lèi)方法的輸入?yún)?shù)更寬松。
- 當(dāng)子類(lèi)的方法實(shí)現(xiàn)父類(lèi)的抽象方法時(shí),方法的后置條件(即方法的返回值)要比父類(lèi)更嚴(yán)格级解。
下面逐個(gè)闡述下:
- 子類(lèi)可以實(shí)現(xiàn)父類(lèi)的抽象方法冒黑,但是不能覆蓋父類(lèi)的非抽象方法。
在我們做系統(tǒng)設(shè)計(jì)時(shí)勤哗,經(jīng)常會(huì)設(shè)計(jì)接口或抽象類(lèi)抡爹,然后由子類(lèi)來(lái)實(shí)現(xiàn)抽象方法,這里使用的其實(shí)就是里氏替換原則芒划。子類(lèi)可以實(shí)現(xiàn)父類(lèi)的抽象方法很好理解冬竟,事實(shí)上,子類(lèi)也必須完全實(shí)現(xiàn)父類(lèi)的抽象方法民逼,哪怕寫(xiě)一個(gè)空方法泵殴,否則會(huì)編譯報(bào)錯(cuò)。
里氏替換原則的關(guān)鍵點(diǎn)在于不能覆蓋父類(lèi)的非抽象方法拼苍。父類(lèi)中凡是已經(jīng)實(shí)現(xiàn)好的方法笑诅,實(shí)際上是在設(shè)定一系列的規(guī)范和契約,雖然它不強(qiáng)制要求所有的子類(lèi)必須遵從這些規(guī)范疮鲫,但是如果子類(lèi)對(duì)這些非抽象方法任意修改吆你,就會(huì)對(duì)整個(gè)繼承體系造成破壞。而里氏替換原則就是表達(dá)了這一層含義俊犯。
看一個(gè)例子:
public class C {
public int func(int a, int b){
return a+b;
}
}
public class C1 extends C{
@Override
public int func(int a, int b) {
return a-b;
}
}
public class Client{
public static void main(String[] args) {
C c = new C1();
//運(yùn)行結(jié)果:2+1=1
System.out.println("2+1=" + c.func(2, 1));
}
}
上面的運(yùn)行結(jié)果明顯是錯(cuò)誤的妇多。類(lèi)C1繼承C,后來(lái)需要增加新功能燕侠,類(lèi)C1并沒(méi)有新寫(xiě)一個(gè)方法者祖,而是直接重寫(xiě)了父類(lèi)C的func方法,違背里氏替換原則绢彤,引用父類(lèi)的地方并不能透明的使用子類(lèi)的對(duì)象七问,導(dǎo)致運(yùn)行結(jié)果出錯(cuò)。
- 子類(lèi)中可以增加自己特有的方法茫舶。
在繼承父類(lèi)屬性和方法的同時(shí)烂瘫,每個(gè)子類(lèi)也都可以有自己的個(gè)性,在父類(lèi)的基礎(chǔ)上擴(kuò)展自己的功能奇适。前面其實(shí)已經(jīng)提到,當(dāng)功能擴(kuò)展時(shí)芦鳍,子類(lèi)盡量不要重寫(xiě)父類(lèi)的方法嚷往,而是另寫(xiě)一個(gè)方法,所以對(duì)上面的代碼加以更改柠衅,使其符合里氏替換原則皮仁,代碼如下:
public class C {
public int func(int a, int b) {
return a + b;
}
}
public class C1 extends C {
public int func2(int a, int b) {
return a - b;
}
}
public class Client {
public static void main(String[] args) {
C1 c = new C1();
//運(yùn)行結(jié)果:2-1=1
System.out.println("2-1=" + c.func2(2, 1));
}
}
- 當(dāng)子類(lèi)覆蓋或?qū)崿F(xiàn)父類(lèi)的方法時(shí),方法的前置條件(即方法的形參)要比父類(lèi)方法的輸入?yún)?shù)更寬松。
面向?qū)ο蟮木幊檀恚渲欣^承有個(gè)大原則趋急,任何子類(lèi)的對(duì)象都可以當(dāng)成父類(lèi)的對(duì)象使用。
如有父類(lèi)為人類(lèi)势誊,可以使用一般的槍?zhuān)芯祛?lèi)呜达,可以使用任何的槍。
class Person {
void shoot(SimpleGun simpleGun);
}
class Police extends Person {
void shoot(Gun gun);
}
其中SimpleGun extends Gun粟耻。
這樣的話(huà)任何警察類(lèi)的對(duì)象都可以被當(dāng)做人類(lèi)來(lái)使用查近。
也就是說(shuō)警察類(lèi)既然會(huì)使用任何的槍?zhuān)?dāng)然可以使用一般的槍。
Person person = new Police();
person.shoot(simpleGun);
而如果反過(guò)來(lái)挤忙,普通人可以使用任何搶?zhuān)熘荒苁褂靡话銟尅?/p>
class Person {
void shoot(Gun gun);
}
class Police extends Person {
void shoot(SimpleGun simpleGun);
}
這樣的話(huà)就不合理了霜威,既然警察是人類(lèi)的一個(gè)子類(lèi),所以警察也是人類(lèi)册烈,既然是人類(lèi)就應(yīng)該能使用人類(lèi)的方法戈泼,也就是使用任何的槍?zhuān)梢愿鶕?jù)上面的定義,反而警察類(lèi)的能力還變小了赏僧。
所以大猛,子類(lèi)的能力必須大于等于父類(lèi),即父類(lèi)可以使用的方法次哈,子類(lèi)都可以使用胎署。
- 當(dāng)子類(lèi)的方法實(shí)現(xiàn)父類(lèi)的抽象方法時(shí),方法的后置條件(即方法的返回值)要比父類(lèi)更嚴(yán)格窑滞。
返回值也是同樣的道理琼牧。
假設(shè)一個(gè)父類(lèi)方法返回一個(gè)List,子類(lèi)返回一個(gè)ArrayList哀卫,這當(dāng)然可以巨坊。
如果父類(lèi)方法返回一個(gè)ArrayList,子類(lèi)返回一個(gè)List此改,就說(shuō)不通了趾撵。
這里子類(lèi)返回值的能力是比父類(lèi)小的。
還有拋出異常的情況共啃。
任何子類(lèi)方法可以聲明拋出父類(lèi)方法聲明異常的子類(lèi)占调。
而不能聲明拋出父類(lèi)沒(méi)有聲明的異常。
這一切都是為了移剪,任何子類(lèi)的對(duì)象都可以當(dāng)做父類(lèi)使用究珊。
- 依賴(lài)倒轉(zhuǎn)原則(Dependence Inversion Principle)
這個(gè)是開(kāi)閉原則的基礎(chǔ),具體內(nèi)容:針對(duì)接口編程纵苛,依賴(lài)于抽象而不依賴(lài)于具體剿涮。
高層模塊不應(yīng)該依賴(lài)低層模塊言津,二者都應(yīng)該依賴(lài)其抽象;抽象不應(yīng)該依賴(lài)細(xì)節(jié)取试;細(xì)節(jié)應(yīng)該依賴(lài)抽象悬槽。
類(lèi)A直接依賴(lài)類(lèi)B,假如要將類(lèi)A改為依賴(lài)類(lèi)C瞬浓,則必須通過(guò)修改類(lèi)A的代碼來(lái)達(dá)成初婆。這種場(chǎng)景下,類(lèi)A一般是高層模塊瑟蜈,負(fù)責(zé)復(fù)雜的業(yè)務(wù)邏輯烟逊;類(lèi)B和類(lèi)C是低層模塊,負(fù)責(zé)基本的原子操作铺根;假如修改類(lèi)A宪躯,會(huì)給程序帶來(lái)不必要的風(fēng)險(xiǎn)。
將類(lèi)A修改為依賴(lài)接口I位迂,類(lèi)B和類(lèi)C各自實(shí)現(xiàn)接口I访雪,類(lèi)A通過(guò)接口I間接與類(lèi)B或者類(lèi)C發(fā)生聯(lián)系,則會(huì)大大降低修改類(lèi)A的幾率掂林。
依賴(lài)倒置原則基于這樣一個(gè)事實(shí):相對(duì)于細(xì)節(jié)的多變性臣缀,抽象的東西要穩(wěn)定的多。以抽象為基礎(chǔ)搭建起來(lái)的架構(gòu)比以細(xì)節(jié)為基礎(chǔ)搭建起來(lái)的架構(gòu)要穩(wěn)定的多泻帮。在Java中精置,抽象指的是接口或者抽象類(lèi),細(xì)節(jié)就是具體的實(shí)現(xiàn)類(lèi)锣杂,使用接口或者抽象類(lèi)的目的是制定好規(guī)范和契約脂倦,而不去涉及任何具體的操作,把展現(xiàn)細(xì)節(jié)的任務(wù)交給他們的實(shí)現(xiàn)類(lèi)去完成元莫。
依賴(lài)倒置原則的核心思想是面向接口編程赖阻,我們依舊用一個(gè)例子來(lái)說(shuō)明面向接口編程比相對(duì)于面向?qū)崿F(xiàn)編程好在什么地方。場(chǎng)景是這樣的踱蠢,母親給孩子講故事火欧,只要給她一本書(shū),她就可以照著書(shū)給孩子講故事了茎截。代碼如下:
class Book{
public String getContent(){
return "很久很久以前有一個(gè)阿拉伯的故事……";
}
}
class Mother{
public void narrate(Book book){
System.out.println("媽媽開(kāi)始講故事");
System.out.println(book.getContent());
}
}
public class Client{
public static void main(String[] args){
Mother mother = new Mother();
mother.narrate(new Book());
}
}
運(yùn)行結(jié)果:
媽媽開(kāi)始講故事
很久很久以前有一個(gè)阿拉伯的故事……
運(yùn)行良好苇侵,假如有一天,需求變成這樣:不是給書(shū)而是給一份報(bào)紙企锌,讓這位母親講一下報(bào)紙上的故事衅檀,報(bào)紙的代碼如下:
class Newspaper{
public String getContent(){
return "林書(shū)豪38+7領(lǐng)導(dǎo)尼克斯擊敗湖人……";
}
}
這位母親卻辦不到,因?yàn)樗尤徊粫?huì)讀報(bào)紙上的故事霎俩,這太荒唐了哀军,只是將書(shū)換成報(bào)紙,居然必須要修改Mother才能讀打却。假如以后需求換成雜志呢杉适?換成網(wǎng)頁(yè)呢?還要不斷地修改Mother柳击,這顯然不是好的設(shè)計(jì)猿推。原因就是Mother與Book之間的耦合性太高了,必須降低他們之間的耦合度才行捌肴。
我們引入一個(gè)抽象的接口IReader蹬叭。讀物,只要是帶字的都屬于讀物:
interface IReader{
public String getContent();
}
Mother類(lèi)與接口IReader發(fā)生依賴(lài)關(guān)系状知,而B(niǎo)ook和Newspaper都屬于讀物的范疇秽五,他們各自都去實(shí)現(xiàn)IReader接口,這樣就符合依賴(lài)倒置原則了饥悴,代碼修改為:
class Newspaper implements IReader {
public String getContent(){
return "林書(shū)豪17+9助尼克斯擊敗老鷹……";
}
}
class Book implements IReader{
public String getContent(){
return "很久很久以前有一個(gè)阿拉伯的故事……";
}
}
class Mother{
public void narrate(IReader reader){
System.out.println("媽媽開(kāi)始講故事");
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());
}
}
運(yùn)行結(jié)果:
媽媽開(kāi)始講故事
很久很久以前有一個(gè)阿拉伯的故事……
媽媽開(kāi)始講故事
林書(shū)豪17+9助尼克斯擊敗老鷹……
這樣修改后坦喘,無(wú)論以后怎樣擴(kuò)展Client類(lèi),都不需要再修改Mother類(lèi)了西设。這只是一個(gè)簡(jiǎn)單的例子瓣铣,實(shí)際情況中,代表高層模塊的Mother類(lèi)將負(fù)責(zé)完成主要的業(yè)務(wù)邏輯贷揽,一旦需要對(duì)它進(jìn)行修改棠笑,引入錯(cuò)誤的風(fēng)險(xiǎn)極大。所以遵循依賴(lài)倒置原則可以降低類(lèi)之間的耦合性禽绪,提高系統(tǒng)的穩(wěn)定性蓖救,降低修改程序造成的風(fēng)險(xiǎn)。
采用依賴(lài)倒置原則給多人并行開(kāi)發(fā)帶來(lái)了極大的便利丐一,比如上例中藻糖,原本Mother類(lèi)與Book類(lèi)直接耦合時(shí),Mother類(lèi)必須等Book類(lèi)編碼完成后才可以進(jìn)行編碼库车,因?yàn)镸other類(lèi)依賴(lài)于Book類(lèi)巨柒。修改后的程序則可以同時(shí)開(kāi)工,互不影響柠衍,因?yàn)镸other與Book類(lèi)一點(diǎn)關(guān)系也沒(méi)有洋满。參與協(xié)作開(kāi)發(fā)的人越多、項(xiàng)目越龐大珍坊,采用依賴(lài)導(dǎo)致原則的意義就越重大∥矗現(xiàn)在很流行的TDD開(kāi)發(fā)模式就是依賴(lài)倒置原則最成功的應(yīng)用。
傳遞依賴(lài)關(guān)系有三種方式阵漏,以上的例子中使用的方法是接口傳遞驻民,另外還有兩種傳遞方式:構(gòu)造方法傳遞和setter方法傳遞翻具,相信用過(guò)spring框架的,對(duì)依賴(lài)的傳遞方式一定不會(huì)陌生回还。在實(shí)際編程中裆泳,我們一般需要做到如下3點(diǎn):
低層模塊盡量都要有抽象類(lèi)或接口,或者兩者都有柠硕。
變量的聲明類(lèi)型盡量是抽象類(lèi)或接口工禾。
使用繼承時(shí)遵循里氏替換原則。
依賴(lài)倒置原則的核心就是要我們面向接口編程蝗柔,理解了面向接口編程闻葵,也就理解了依賴(lài)倒置。
- 接口隔離原則(Interface Segregation Principle)
這個(gè)原則的意思是:使用多個(gè)隔離的接口癣丧,比使用單個(gè)接口要好槽畔。
類(lèi)A通過(guò)接口I依賴(lài)類(lèi)B,類(lèi)C通過(guò)接口I依賴(lài)類(lèi)D坎缭,如果接口I對(duì)于類(lèi)A和類(lèi)B來(lái)說(shuō)不是最小接口竟痰,則類(lèi)B和類(lèi)D必須去實(shí)現(xiàn)他們不需要的方法。
將臃腫的接口I拆分為獨(dú)立的幾個(gè)接口掏呼,類(lèi)A和類(lèi)C分別與他們需要的接口建立依賴(lài)關(guān)系坏快。也就是采用接口隔離原則。
這個(gè)圖的意思是:類(lèi)A依賴(lài)接口I中的方法1憎夷、方法2莽鸿、方法3,類(lèi)B是對(duì)類(lèi)A依賴(lài)的實(shí)現(xiàn)拾给。類(lèi)C依賴(lài)接口I中的方法1祥得、方法4、方法5蒋得,類(lèi)D是對(duì)類(lèi)C依賴(lài)的實(shí)現(xiàn)级及。對(duì)于類(lèi)B和類(lèi)D來(lái)說(shuō),雖然他們都存在著用不到的方法(也就是圖中紅色字體標(biāo)記的方法)额衙,但由于實(shí)現(xiàn)了接口I饮焦,所以也必須要實(shí)現(xiàn)這些用不到的方法。
可以看到窍侧,如果接口過(guò)于臃腫县踢,只要接口中出現(xiàn)的方法,不管對(duì)依賴(lài)于它的類(lèi)有沒(méi)有用處伟件,實(shí)現(xiàn)類(lèi)中都必須去實(shí)現(xiàn)這些方法硼啤,這顯然不是好的設(shè)計(jì)。如果將這個(gè)設(shè)計(jì)修改為符合接口隔離原則斧账,就必須對(duì)接口I進(jìn)行拆分谴返。在這里我們將原有的接口I拆分為三個(gè)接口煞肾,拆分后的設(shè)計(jì)下圖所示:
接口隔離原則的含義是:建立單一接口,不要建立龐大臃腫的接口嗓袱,盡量細(xì)化接口扯旷,接口中的方法盡量少。也就是說(shuō)索抓,我們要為各個(gè)類(lèi)建立專(zhuān)用的接口,而不要試圖去建立一個(gè)很龐大的接口供所有依賴(lài)它的類(lèi)去調(diào)用毯炮。本文例子中逼肯,將一個(gè)龐大的接口變更為3個(gè)專(zhuān)用的接口所采用的就是接口隔離原則。在程序設(shè)計(jì)中桃煎,依賴(lài)幾個(gè)專(zhuān)用的接口要比依賴(lài)一個(gè)綜合的接口更靈活篮幢。接口是設(shè)計(jì)時(shí)對(duì)外部設(shè)定的“契約”,通過(guò)分散定義多個(gè)接口为迈,可以預(yù)防外來(lái)變更的擴(kuò)散三椿,提高系統(tǒng)的靈活性和可維護(hù)性。
說(shuō)到這里葫辐,很多人會(huì)覺(jué)的接口隔離原則跟之前的單一職責(zé)原則(后面會(huì)細(xì)說(shuō))很相似搜锰,其實(shí)不然。其一耿战,單一職責(zé)原則原注重的是職責(zé)蛋叼;而接口隔離原則注重對(duì)接口依賴(lài)的隔離。其二剂陡,單一職責(zé)原則主要是約束類(lèi)狈涮,其次才是接口和方法,它針對(duì)的是程序中的實(shí)現(xiàn)和細(xì)節(jié)鸭栖;而接口隔離原則主要約束接口接口歌馍,主要針對(duì)抽象,針對(duì)程序整體框架的構(gòu)建晕鹊。
采用接口隔離原則對(duì)接口進(jìn)行約束時(shí)松却,要注意以下幾點(diǎn):
- 接口盡量小,但是要有限度捏题。對(duì)接口進(jìn)行細(xì)化可以提高程序設(shè)計(jì)靈活性是不掙的事實(shí)玻褪,但是如果過(guò)小,則會(huì)造成接口數(shù)量過(guò)多公荧,使設(shè)計(jì)復(fù)雜化带射。所以一定要適度。
- 為依賴(lài)接口的類(lèi)定制服務(wù)循狰,只暴露給調(diào)用的類(lèi)它需要的方法窟社,它不需要的方法則隱藏起來(lái)券勺。只有專(zhuān)注地為一個(gè)模塊提供定制服務(wù),才能建立最小的依賴(lài)關(guān)系灿里。
- 提高內(nèi)聚关炼,減少對(duì)外交互。使接口用最少的方法去完成最多的事情匣吊。
運(yùn)用接口隔離原則儒拂,一定要適度,接口設(shè)計(jì)的過(guò)大或過(guò)小都不好色鸳。設(shè)計(jì)接口的時(shí)候社痛,只有多花些時(shí)間去思考和籌劃,才能準(zhǔn)確地實(shí)踐這一原則命雀。
- 迪米特法則(最少知道原則)(Demeter Principle)
為什么叫最少知道原則蒜哀,就是說(shuō):一個(gè)實(shí)體應(yīng)當(dāng)盡量少的與其他實(shí)體之間發(fā)生相互作用,使得系統(tǒng)功能模塊相對(duì)獨(dú)立吏砂。
狹義的迪米特法則定義:也叫最少知識(shí)原則(LKP撵儿,Least Knowledge Principle)。如果兩個(gè)類(lèi)不必彼此直接通信狐血,那么這兩個(gè)類(lèi)就不應(yīng)當(dāng)發(fā)生直接的相互作用淀歇。如果其中的一個(gè)類(lèi)需要調(diào)用另一個(gè)類(lèi)的某一個(gè)方法的話(huà),可以通過(guò)第三者轉(zhuǎn)發(fā)這個(gè)調(diào)用氛雪。
廣義的迪米特法則定義:一個(gè)模塊設(shè)計(jì)得好壞的一個(gè)重要的標(biāo)志就是該模塊在多大的程度上將自己的內(nèi)部數(shù)據(jù)與實(shí)現(xiàn)有關(guān)的細(xì)節(jié)隱藏起來(lái)房匆。信息的隱藏非常重要的原因在于,它可以使各個(gè)子系統(tǒng)之間脫耦报亩,從而允許它們獨(dú)立地被開(kāi)發(fā)浴鸿、優(yōu)化、使用閱讀以及修改弦追。
比較形象的說(shuō)明這個(gè)法則的示例:
如果你想讓你的狗狗跑的話(huà)岳链,你會(huì)對(duì)狗狗說(shuō)還是對(duì)四條狗腿說(shuō)?
如果你去店里買(mǎi)東西劲件,你會(huì)把錢(qián)交給店員掸哑,還是會(huì)把錢(qián)包交給店員讓他自己拿?
《設(shè)計(jì)模式其實(shí)很簡(jiǎn)單》這本書(shū)中給了一個(gè)例子,如下:
//某人類(lèi)
public class Somebody{
//參數(shù)Friend類(lèi)的方法
public void operation1(Friend friend){
Stranger stranger=friend.provide();
stranger.operation3();
}
}
//朋友類(lèi)Friend
public class Friend{
//私有數(shù)據(jù)成員零远,某個(gè)陌生人
private Stranger stranger=new Stranger();
public void operation2(){
}
public Stranger provide(){
return stranger;
}
}
上面的代碼違反了迪米特法則,因?yàn)槿绻oSomebody定義一個(gè)它的直接關(guān)聯(lián)類(lèi)(即一個(gè)Friend的話(huà))苗分,那么這個(gè)類(lèi)應(yīng)該是Friend類(lèi),因?yàn)镾omebody的一個(gè)方法需要Friend類(lèi)作為參數(shù)牵辣,那么這樣以來(lái)摔癣,Stranger類(lèi)就不是Somebody的直接關(guān)聯(lián)類(lèi),但是上面的Somebody的operation1方法卻直接調(diào)用了Stranger類(lèi)的operation3方法,所以這違反了迪米特法則。
所以應(yīng)該在Friend類(lèi)中加一個(gè)方法择浊,它執(zhí)行了對(duì)Stranger類(lèi)的operation3方法的調(diào)用戴卜。然后在Somebody的operation1方法中有friend調(diào)用加入的那個(gè)方法,從而實(shí)現(xiàn)和上面代碼相同的功能琢岩。
代碼如下:
//朋友類(lèi)
public class Friend{
private Stranger stranger=new Stranger();
pubic void operation2(){
}
public void provide(){
return stranger;
}
pubic void forward(){
stranger.operation3();
}
}
//某人類(lèi)
public class Somebody{
public void operation1(Friend friend){
friend.forward();
}
}
- 合成復(fù)用原則(Composite Reuse Principle)
原則是盡量使用合成/聚合的方式投剥,而不是使用繼承。
- 對(duì)象的繼承關(guān)系在編譯時(shí)就定義好了担孔,所以無(wú)法在運(yùn)行時(shí)改變從父類(lèi)繼承的子類(lèi)的實(shí)現(xiàn)
- 子類(lèi)的實(shí)現(xiàn)和它的父類(lèi)有非常緊密的依賴(lài)關(guān)系江锨,以至于父類(lèi)實(shí)現(xiàn)中的任何變化必然會(huì)導(dǎo)致子類(lèi)發(fā)生變化
- 當(dāng)你復(fù)用子類(lèi)的時(shí)候,如果繼承下來(lái)的實(shí)現(xiàn)不適合解決新的問(wèn)題糕篇,則父類(lèi)必須重寫(xiě)或者被其它更適合的類(lèi)所替換
這種依賴(lài)關(guān)系限制了靈活性泳桦,并最終限制了復(fù)用性