引言:本文并不是從代碼角度上來(lái)談設(shè)計(jì)模式,而是希望類比手機(jī)制造嘗試以一條線來(lái)從常用設(shè)計(jì)模式的使用場(chǎng)景來(lái)介紹設(shè)計(jì)模式的(畢竟設(shè)計(jì)模式是面向?qū)ο蟮模┧枷牒褪褂梅椒ǎ瑫r(shí)我在每個(gè)設(shè)計(jì)模式場(chǎng)景的介紹中也會(huì)提供類圖幫助大家理解,如果需要在代碼層次有更多的實(shí)踐可以參考《Head First設(shè)計(jì)模式》
本文中我會(huì)提到以下設(shè)計(jì)模式:
- 策略模式
- 工廠模式
- 裝飾者模式
- 模板方法模式
- 適配器模式
- 迭代器與組合模式
- 觀察者模式
獨(dú)身模式和構(gòu)造者模式可以看我的另一篇文章獨(dú)身模式+構(gòu)建者模式打造私有個(gè)性化“商城”
First:手機(jī)制造中的策略模式
大家都知道每個(gè)手機(jī)制造廠家都有很多機(jī)型,每一款機(jī)型的每個(gè)部件往往都是不一樣的,我們拿攝像頭和小米手機(jī)來(lái)舉個(gè)栗子:小米5的攝像頭是光學(xué)防抖
的,小米5s的攝像頭擁有超感光元器件
(因此被譽(yù)為暗夜之眼),而小米6的攝像頭則是變焦雙攝
。很明顯我標(biāo)識(shí)出來(lái)的地方都是這些攝像頭的屬性,依據(jù)這些屬性每個(gè)攝像頭可以完成不同的拍照行為,我們姑且做一個(gè)映射:
攝像頭 | 拍照能力(或行為) |
---|---|
光學(xué)防抖 | 在手機(jī)抖動(dòng)的情況下依然可以拍出清晰的照片 |
超感光元器件 | 在光線很低的情況下依然可以拍出清晰的照片 |
變焦雙攝 | 單反級(jí)的景深和背景虛化 |
那么不用設(shè)計(jì)模式,正常的去定義這些手機(jī)用類圖如何表示呢?
大概如下:
很明顯所有的手機(jī)機(jī)型都需要繼承一個(gè)基類,而且所有手機(jī)都具有打電話這個(gè)方法且都是一致的,所以把打電話放在基類里面是可以的。但是拍照這個(gè)方法不可以,因?yàn)槊總€(gè)機(jī)型拍照的能力不一樣,如果放在基類里面則都會(huì)被繼承,這樣是不對(duì)的。所以常見(jiàn)的方法是寫一個(gè)接口,每個(gè)機(jī)型都去繼承這個(gè)接口并各自實(shí)現(xiàn),這樣就可以解決問(wèn)題。但是這又帶來(lái)另外一個(gè)問(wèn)題,這個(gè)接口實(shí)在是被繼承太多次了!而且只是為了一個(gè)不同的行為,假設(shè)還有其他行為如打游戲又需要繼承另一個(gè)接口,簡(jiǎn)單算個(gè)賬,假設(shè)總共有2300萬(wàn)部手機(jī),5個(gè)不同接口,2300萬(wàn)*5,很大的一個(gè)數(shù)字.....那么沒(méi)有辦法把它放在父類里面去繼承嗎?
答案:是可以的,而且很簡(jiǎn)單,“就把它放在父類里面啊!”(感覺(jué)自己在找打)實(shí)現(xiàn)如下:
我們按類圖的上下兩部分來(lái)說(shuō):
- 封裝行為
首先我們依然把camera當(dāng)成一個(gè)接口,這一點(diǎn)和非策略模式是一致的,但是我們有了具體實(shí)現(xiàn)類,光學(xué)防抖的camera,超感光元器件的camera,變焦雙攝的camera,并且我們?cè)诓煌腸amera里面實(shí)現(xiàn)了自己的takePhoto方法,這樣行為就被封裝了。
- 動(dòng)態(tài)設(shè)置行為
看下半部分,父類里面多了一個(gè)屬性叫camera,其實(shí)是引用類,但是沒(méi)有初始化,初始化的步驟我們放在了setCamera里面,同樣takePhoto的我們也作為一個(gè)方法。
我們看看子類怎么用,以小米5為??:
1.初始化camera
private void setCamear() { camera = new 光學(xué)防抖的camera();
}
2.可以拍照了
public void useCameraTakePhoto() { print(camera.takePhoto);
}
3.拍照結(jié)果:
log:抖動(dòng)時(shí)的照片依然清晰,具備防抖效果
策略模式的好處是顯而易見(jiàn)的,我們不需要再繼承大量的接口并實(shí)現(xiàn)里面的方法,我們只需要在我們需要的時(shí)候在子類中去初始化行為就可以,我們甚至可以不初始化camera,誰(shuí)知道以后拍照需要不要相機(jī)呢?這樣是不是提高了可變性?
Second:手機(jī)制造中的工廠模式(抽象工廠)
我想這個(gè)模式放在手機(jī)制造這個(gè)例子里面來(lái)講大概是最好理解的,我們先看定義(取自《HeadFirst 設(shè)計(jì)模式》)
抽象工廠模式提供一個(gè)接口,用于創(chuàng)建相關(guān)或依賴對(duì)象的家族,而不需要明確指定具體類。
回到手機(jī)工廠。顧名思義,手機(jī)工廠一定擁有制造各種手機(jī)的能力,這種能力體現(xiàn)在擁有不同手機(jī)的元器件材料,然后再以一種流水線的方式把各種材料組件起來(lái),這樣手機(jī)就誕生了。這里我們需要再細(xì)化一下工作,手機(jī)工廠僅僅提供手機(jī)的各種元器件材料,我們把組裝手機(jī)的工作放在流水線里面,這樣不同的流水線就可以組裝不同的手機(jī),實(shí)踐生產(chǎn)中肯定也是這樣。我們看一下工廠方法的類圖:
這里我們把工廠更具體化了一點(diǎn)(可能實(shí)際上只需要一家工廠),我們有兩個(gè)工廠,一個(gè)是海淀工廠,一個(gè)是朝陽(yáng)工廠,每個(gè)工廠提供不同的元器件。這兩個(gè)工廠生產(chǎn)手機(jī)的能力有什么不同呢?答案可以從流水線上看出來(lái),很明顯海淀工廠可以擁有兩條流水線,一個(gè)是小米5s,一個(gè)是小米5sPlus,僅僅只需要把屏幕的大小換一下(實(shí)際上肯定還有其他的元器件,這里只用屏幕大小來(lái)區(qū)別)。然而朝陽(yáng)工廠只擁有一條流水線即米6。
這個(gè)時(shí)候我們有一家海淀的線下店缺貨了,這個(gè)時(shí)候需要“造一些”手機(jī),這里我們把用海淀工廠來(lái)造手機(jī)(當(dāng)然用朝陽(yáng)工廠也行,或者兩個(gè)工廠用list傳進(jìn)去)。
protected Phone createPhone(String type) {
PipeLine pipeLine = null;
PhoneFactory factory = new HaiDianFactory();
if (type.equals("xiaomi5s")) {
pipeLine = new xiaomi5sPipeLine(factory);
} else if (type.equals("xiaomi5splus")) {
pipeLine = new xiaomi5sPlusPipeLine(factory);
}
pipeLine.prepare();
return pipeLine.getPhone();
}
這樣做的好處是什么?我們解耦了實(shí)際的工廠!這是比簡(jiǎn)單工廠進(jìn)步的地方,我們可以根據(jù)需要去初始化工廠接口,同時(shí)根據(jù)所需要的產(chǎn)品類型把實(shí)際工廠傳入,這樣就可以制造出不同的產(chǎn)品。如我們線下店的小米5s缺貨啦!我們把海淀工廠當(dāng)做參數(shù)傳給小米5s的流水線,這樣流水線就可以拿到所需要的元器件去準(zhǔn)備生產(chǎn)小米5s,這樣不是比先判斷是什么手機(jī)我們?cè)僖粋€(gè)一個(gè)初始化元器件然后再準(zhǔn)備組裝的過(guò)程方便的多?
抽象工廠果然英明。
Third:手機(jī)制造中的裝飾者模式
大家都知道每一種機(jī)型都是由各種元器件組成的,不同機(jī)型之間有相同的元器件也有不相同的元器件,我們還是以小米5、小米6舉栗子:
機(jī)型 | 組成 |
---|---|
小米5 |
camera +fingerPrinter +大猩猩screen
|
小米6 |
camera +camera +四曲面screen +fingerPrinter
|
很明顯可以看出小米6與小米5的不同之處,小米5是單攝像頭,小米6是雙攝像頭,同時(shí)小米5和小米6的屏幕也是不一樣的。這樣會(huì)有什么影響呢?我們從兩個(gè)方面談一下:
- 價(jià)格
很明顯選擇不同的器件組合起來(lái)的手機(jī)價(jià)格一定是不同,比如有兩款除了攝像頭不同其余器件都相同的機(jī)型,雙攝一定是比單攝貴的;同樣四曲面玻璃一定是比普通玻璃貴的。所以僅有上面幾個(gè)器件決定的手機(jī)價(jià)格小米6的一定是比小米5高的,這是由成本和工藝決定的(當(dāng)然前提是同一時(shí)間段)。那么價(jià)格是如何算的呢?很簡(jiǎn)單,組件的價(jià)格之和,A+B+C=總價(jià)格,是具有累加屬性的。 - 描述
發(fā)燒友都喜歡看手機(jī)的參數(shù),比如驍龍835就比驍龍820更吸引人的眼球,所以我們可以這樣描述小米6,這是一款擁有驍龍835cpu``變焦雙攝``四曲面屏幕
的旗艦手機(jī)。當(dāng)然你也可以用同樣的方式去描述米5,這些描述賦予了手機(jī)鮮明的特點(diǎn)。同樣,這些描述也是可以累加的,我有這樣一個(gè)A+B+C的手機(jī)
好了,廢話扯完了,該扯到設(shè)計(jì)模式了,假設(shè)不用設(shè)計(jì)模式,我們需要得到一臺(tái)機(jī)型的價(jià)格和描述我們?cè)撊绾巫觯靠赡苋缦拢?/p>
這是一個(gè)看上去簡(jiǎn)單實(shí)際上很傻的設(shè)計(jì),我們把cost實(shí)現(xiàn)全部放在具體的機(jī)型里面,這樣的后果是可怕的——假設(shè)攝像頭是從美國(guó)采購(gòu)回來(lái)的,因?yàn)閰R率問(wèn)題,價(jià)格變化了,我們需要在每個(gè)機(jī)型的cost方法里面重新定義價(jià)格,就算我們改良一下,我們把所有的元器件都放在父類里面定義,這樣好嗎?也不好,這樣所有的機(jī)型都必須繼承這些器件,假設(shè)米5繼承了父類的四曲面screen
,實(shí)際上根本用不著,而且顯然父類是需要維護(hù)的而且維護(hù)的成本不低,這并不是一個(gè)父類該做的事情,它應(yīng)該只保留子類共性的部分并提供抽象的方法。好吧,用裝飾者模式改造一下設(shè)計(jì)吧:
我們分成三部分來(lái)討論,被裝飾者
、裝飾者
、裝飾過(guò)程
被裝飾者
被裝飾者這里指的就是小米5和小米6,它們同時(shí)繼承了手機(jī)這個(gè)抽象類并且各種實(shí)現(xiàn)了cost方法,但是這樣還夠,因?yàn)樗鼈儍H僅是孤零零的裸機(jī)
,什么都沒(méi)有,沒(méi)有攝像頭,沒(méi)有屏幕也沒(méi)有cpu,我們需要裝飾一下它們。裝飾者
我們需要定義一個(gè)抽象的decorator來(lái)繼承抽象父類phone,這樣做是為了繼承父類的抽象方法cost和getDescription。實(shí)際的裝飾者是誰(shuí)?是那些元器件,比如攝像頭
,四曲面玻璃
,驍龍cpu
等等,我們用這些描述累加起來(lái)去具體形容一個(gè)機(jī)型,同時(shí)我們?cè)诶锩孢€順便實(shí)現(xiàn)一下cost和getDescription方法(如何實(shí)現(xiàn)看裝飾過(guò)程),要注意實(shí)際的裝飾者必須關(guān)聯(lián)一個(gè)實(shí)際的被裝飾者phone(定義在屬性里面)。-
裝飾過(guò)程
我們先來(lái)裝飾一個(gè)小米5
Phone xiaomi5phone = new 大猩猩screen(new 驍龍cpu(new camera (new fingerPrinter(new xiaomi5()))))
首先,這么做可以嗎?毫無(wú)疑問(wèn)是可以的,因?yàn)槊總€(gè)裝飾者的構(gòu)造方法里面都必須傳入一個(gè)已經(jīng)初始化過(guò)的phone對(duì)象,而且每個(gè)裝飾者本身又可以是被裝飾的對(duì)象。這樣就有趣了,我們可以一直裝飾下去。我們可以用用攝像頭裝飾米5,可以描述成有攝像頭的米5
,我們可以繼續(xù)用大猩猩屏幕來(lái)裝飾有攝像頭的米5
,這樣就可以描述成有大猩猩屏幕且有攝像頭的米5
......米6我們?nèi)匀豢梢赃@樣裝飾
Phone xiaomi6phone = new 四曲面screen(new 驍龍cpu(new camera(new camera(new fingerPrinter(new xiaomi6())))))
我們甚至可以用camera裝飾兩次讓米6變成雙攝的手機(jī)
我們?cè)賹?shí)現(xiàn)一下組件的,cost和getDescripition方法,以camera為栗子:
public class Camera extends CondimenDecorator {
Phone phone;public Camera(Phone phone) { this.phone = phone; } public String getDescription() { return phone.getDescription() + "has 攝像頭"; } public double cost() { return 20 + phone.cost(); } }
很容易看出來(lái)無(wú)論是描述還是價(jià)格都具有累加的屬性,這正是我前面所強(qiáng)調(diào)的地方,這樣我們就可以方便的獲取到裝飾后的價(jià)格和描述,因?yàn)檫@些屬性均具有累加的性質(zhì),這就是裝飾者模式的三個(gè)部分。
Fouth:手機(jī)制造中的模板方法模式
前面說(shuō)過(guò),手機(jī)在工廠中的加工方法其實(shí)就是一條流水線,往往很多手機(jī)的流水線是相似的,僅僅只是修改了流水線中的一兩個(gè)步驟,這次我們拿小米5標(biāo)準(zhǔn)版和小米5高配版來(lái)做個(gè)對(duì)比,這兩條流水線如下(圖畫的有點(diǎn)歪見(jiàn)諒):
很明顯,搞出來(lái)兩條流水線是浪費(fèi)的,因?yàn)樾∶?標(biāo)準(zhǔn)版和高配版之間的區(qū)別僅僅是內(nèi)存和rom的不同,所以我們可以合并一下流程:
其實(shí)流水線跟程序中的算法是一致,很多時(shí)候算法往往是解決一類問(wèn)題的,但是每個(gè)問(wèn)題都有自己的條件且條件通常不同,這就需要算法可以動(dòng)態(tài)的根據(jù)條件來(lái)改變算法的步驟,這其實(shí)就是
模板方法
的精髓。
我們先來(lái)看看模板方法模式的定義:
模板方法模式在一個(gè)方法中定義一個(gè)算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以在不改變算法結(jié)構(gòu)的情況下,重新定義算法中的某些步驟。
我們把上面的流水線用模板方法改造一下:
我們?cè)谧宇愔兄匦露x流水線的兩個(gè)步驟installRAM()
和installROM
,我們?cè)購(gòu)拇a上看一下父類的流水線這個(gè)方法:
final void templatePipeLine() {
installMainBoard();
installCPU();
installRAM();
installROM();
hook();
}
這個(gè)算法和上面的流水線基本一致,但是還有一點(diǎn)欠缺,上面的流水線只要判斷是否是高配就決定了流水線的走勢(shì),我們這個(gè)還是要在子類里面重新實(shí)現(xiàn)installRAM()
和installROM
,但是沒(méi)有關(guān)系,模板方法還留了一手,我們可以通過(guò)hook
(鉤子)的方式改變算法的走向,比如我們把hook()定義為一個(gè)boolean isHigh()
,重新改變一下模板方法:
final void templatePipeLine() {
installMainBoard();
installCPU();
if (isHigh) {
install3GRAM();
install64GROM();
} else{
install2GRAM();
install32GROM();
}
}
這樣改造一下,子類僅僅需要提供一下是否是高版本的這個(gè)條件就可以輕易的得到想要的流水線產(chǎn)品,豈不是很方便?