大家好,我是每周在這里陪你進(jìn)步的網(wǎng)管~,本次我們繼續(xù)填坑,說一下裝飾器模式。
上篇文章我們說過裝飾器是代理模式的特殊應(yīng)用,而且很多人說中間件是用裝飾器模式實(shí)現(xiàn)的,有的人說是用職責(zé)鏈實(shí)現(xiàn)的,那么這篇文章我們就來一起看看他們的異同。
什么是裝飾器
裝飾器模式(Decorator Pattern)也叫作包裝器模式(Wrapper Pattern),指在不改變原有對象的基礎(chǔ)上,動態(tài)地給一個對象添加一些額外的職責(zé)。就增加功能來說,裝飾器模式相比生成子類更為靈活,屬于結(jié)構(gòu)型設(shè)計(jì)模式。
給對象添加新行為最簡單直觀的辦法就是擴(kuò)展本體對象,通過繼承的方式達(dá)到目的。但是使用繼承不可避免地有如下兩個弊端:
繼承是靜態(tài)的,在編譯期間就已經(jīng)確定,無法在運(yùn)行時改變對象的行為。
子類只能有一個父類,當(dāng)需要添加的新功能太多時,容易導(dǎo)致類的數(shù)量劇增。
而使用裝飾器模式,我們通過將現(xiàn)有對象放置在實(shí)現(xiàn)了相同一套接口的包裝器對象中來動態(tài)地向現(xiàn)有對象添加新行為。在包裝器中進(jìn)行我們代碼的擴(kuò)展,有助于重用功能并且不會修改現(xiàn)有對象的代碼,符合“開閉原則”。
這里被放置在包裝對象的“現(xiàn)有對象”通常會被叫做“組件”(Component),而包裝組件的包裝器對象就是我們常說的“裝飾器”(Decorator),因?yàn)檠b飾器會組件實(shí)現(xiàn)相同接口,故客戶端無法識別兩者的差異,也就不需要在增加裝飾器時對客戶端調(diào)用代碼進(jìn)行修改了。
從上面關(guān)于裝飾器模式的描述中 ,會感覺他跟代理模式很像。這是因?yàn)樗麄儽緛碓诮Y(jié)構(gòu)上也幾乎一樣,裝飾器算是代理的一個特殊應(yīng)用--裝飾器模式的一個特點(diǎn)是可以嵌套多層裝飾器,相當(dāng)于給代理再加代理。不過代理強(qiáng)調(diào)的是對本體對象的訪問控制,而裝飾器是用來對本地進(jìn)行增強(qiáng),兩者在使用目的上不一樣。
上面裝飾器模式的用處特點(diǎn)用文字描述了這么多,下面我們用 UML 類圖展示一下它的結(jié)構(gòu),讓我們在寫代碼前對模式中的各個角色有個更清晰的認(rèn)識。
裝飾器的結(jié)構(gòu)
用 UML 類圖表示裝飾器模式的結(jié)構(gòu)如下:
從圖中可以看到裝飾器模式中主要有如下幾個角色:
客戶端:會用多層裝飾器來封裝組件,最后調(diào)用裝飾好的包裝器的方法,啟動執(zhí)行。
組件接口:Component 聲明裝飾器對象和被裝飾的組件對象要實(shí)現(xiàn)的公用接口。
組件實(shí)現(xiàn):具體的組件實(shí)現(xiàn)類它的 Operation 方法中定義了組件的基礎(chǔ)行為,裝飾類可以增強(qiáng)這些行為。
基礎(chǔ)裝飾類:擁有一個指向被封裝對象的成員變量。在自己的 Operation 方法中調(diào)用被裝飾對象的 Operation 方法
具體裝飾類:重寫父類的 Operation 方法實(shí)現(xiàn)增強(qiáng)邏輯。類圖里已經(jīng)給出了要實(shí)現(xiàn)的主要邏輯,第四步的基礎(chǔ)裝飾類并不需要一定存在,完全可以由具體裝飾類來持有對被裝飾對象的引用,并實(shí)現(xiàn)增強(qiáng)邏輯,這樣一來整體的結(jié)構(gòu)會更簡單一些。
注意:圖中的方法名在代碼實(shí)現(xiàn)里可自己定義,不需要完全跟圖里給出的方法名一樣。
我們可以跟上節(jié)代理模式的 UML 類圖做個對比,兩者在結(jié)構(gòu)上非常相似,尤其是省略了 BaseDecorator 這一層后,在結(jié)構(gòu)上基本上是一摸一樣,這樣我們一直再強(qiáng)調(diào)的--"裝飾器是代理模式的特殊應(yīng)用" 的一個論據(jù)。
下面我們看一下實(shí)現(xiàn)裝飾器模式的代碼模版,本文中提供了 Go 語言實(shí)現(xiàn)一個簡單裝飾器模式的代碼模版。
裝飾器模式代碼實(shí)現(xiàn)
清楚了裝飾器模式結(jié)構(gòu)的組成后,再來寫代碼就會清晰很多,接下來我們演示一下用裝飾器模式實(shí)現(xiàn)增強(qiáng)游戲主機(jī)的一個例子。
首先我們定義一個游戲主機(jī)的產(chǎn)品接口,它就是上面類圖中組件和裝飾器的公共接口。
// PS5 產(chǎn)品接口 type PS5 interface { StartGPUEngine() GetPrice() int64 }
然后我們提供一個基礎(chǔ)的產(chǎn)品實(shí)現(xiàn)類作為裝飾器模式中的組件。
// CD 版 PS5主機(jī) "本文使用的完整可運(yùn)行源碼 去公眾號「網(wǎng)管叨bi叨」發(fā)送【設(shè)計(jì)模式】即可領(lǐng)取" type PS5WithCD struct{} func (p PS5WithCD) StartGPUEngine() { fmt.Println("start engine") } func (p PS5WithCD) GetPrice() int64 { return 5000 }
這里給出的是一個 CD 版的游戲主機(jī),平時玩游戲的同學(xué)都會知道,一般還會有數(shù)字版的主機(jī),價格會便宜點(diǎn),這種情況我們可以提供一個數(shù)字版游戲主機(jī)的實(shí)現(xiàn)作為組件實(shí)現(xiàn)類。
// PS5 數(shù)字版主機(jī) type PS5WithDigital struct{} func (p PS5WithDigital) StartGPUEngine() { fmt.Println("start normal gpu engine") } func (p PS5WithDigital) GetPrice() int64 { return 3600 }
那么除了這兩種基礎(chǔ)的產(chǎn)品類型,廠商一般還會開發(fā)各種主題限定配色的主機(jī)、增加了硬件配置的主機(jī)等等,這兩種在價格上肯定會跟基礎(chǔ)版有些不一樣,針對這種層面的擴(kuò)展我們可以使用裝飾器來實(shí)現(xiàn),避免對基礎(chǔ)組件類的更改。
下面是用兩個裝飾器實(shí)現(xiàn)的 Plus 版和主題配色版的兩個增強(qiáng)。
"本文使用的完整可運(yùn)行源碼 去公眾號「網(wǎng)管叨bi叨」發(fā)送【設(shè)計(jì)模式】即可領(lǐng)取" // Plus 版的裝飾器 func (p *PS5MachinePlus) SetPS5Machine(ps5 PS5) { p.ps5Machine = ps5 } func (p PS5MachinePlus) StartGPUEngine() { p.ps5Machine.StartGPUEngine() fmt.Println("start plus plugin") } func (p PS5MachinePlus) GetPrice() int64 { return p.ps5Machine.GetPrice() + 500 } // 主題色版的裝飾器 type PS5WithTopicColor struct { ps5Machine PS5 } func (p *PS5WithTopicColor) SetPS5Machine(ps5 PS5) { p.ps5Machine = ps5 } func (p PS5WithTopicColor) StartGPUEngine() { p.ps5Machine.StartGPUEngine() fmt.Println("尊貴的主題色主機(jī),GPU啟動") } func (p PS5WithTopicColor) GetPrice() int64 { return p.ps5Machine.GetPrice() + 200 }
根據(jù)裝飾器模式的特點(diǎn),兩個增強(qiáng)還可以疊加在一起,組合出即高配主題限定版主機(jī)...... 呃,是不是有點(diǎn)某游戲大廠每年發(fā)新機(jī)時給你的感覺了,就是不出第二代,每年給你多發(fā)幾個限定配色、升級下屏幕,說的就是你 XXX(各位自己評論里腦補(bǔ)一下)
好了,在客戶端我們把裝飾器和組件組合起來就能獲得一款高配主題限定版主機(jī)......
"本文使用的完整可運(yùn)行源碼 去公眾號「網(wǎng)管叨bi叨」發(fā)送【設(shè)計(jì)模式】即可領(lǐng)取" func main() { ps5MachinePlus := PS5MachinePlus{} ps5MachinePlus.SetPS5Machine(PS5WithCD{}) // ps5MachinePlus.SetPS5Machine(PS5WithDigital{}) // 可以在更換主機(jī) ps5MachinePlus.StartGPUEngine() price := ps5MachinePlus.GetPrice() fmt.Printf("PS5 CD 豪華Plus版,價格 %d 元\n\n", price ps5WithTopicColor := PS5WithTopicColor{} ps5WithTopicColor.SetPS5Machine(ps5MachinePlus) ps5WithTopicColor.StartGPUEngine() price = ps5WithTopicColor.GetPrice() fmt.Printf("PS5 CD 豪華Plus 經(jīng)典主題配色版,價格 %d 元\n", price }
裝飾器和幾個模式的區(qū)別
裝飾器和代理在結(jié)構(gòu)上類似,在行為上跟職責(zé)鏈模式類似,現(xiàn)在我們總結(jié)一下他們之間的區(qū)別
裝飾器模式 VS 代理模式
裝飾器模式就是代理模式的一個特殊應(yīng)用。
裝飾器模式強(qiáng)調(diào)自身功能的擴(kuò)展。
代理模式強(qiáng)調(diào)對代理過程的控制。
裝飾器 VS 職責(zé)鏈模式
裝飾器和職責(zé)鏈在行為上看都是多個單元進(jìn)行組合完成邏輯處理,但是裝飾器注重給某樣?xùn)|西添加擴(kuò)展,最終會得到一個產(chǎn)品。而職責(zé)鏈更強(qiáng)調(diào)分步驟完成某個流程,更像是一個任務(wù)鏈表,而且與裝飾器模式不同的是,職責(zé)鏈可以隨時終止。
舉個例子來說,針對 OA 系統(tǒng)請假審批這個場景,假設(shè)員工請假需要得到組長、總監(jiān)和經(jīng)理的批準(zhǔn)才行。在這種情況下,使用裝飾器模式實(shí)現(xiàn)的話無論您的請假在前面的環(huán)節(jié)被批準(zhǔn)還是被拒絕,整個鏈條都不會中斷,最終我們會得到三個級別審批人對申請的全部反饋。
而使用職責(zé)鏈模式的話,在每個階段,每個審批人都有權(quán)批準(zhǔn)或拒絕。如果請求在任何級別被拒絕,那么整個流程就會結(jié)束,請求不會繼續(xù)流轉(zhuǎn)到下一個級別的審批人那里。
所以看到這里,你覺得像 Web 框架的中間件這種東西應(yīng)該拿職責(zé)鏈還是裝飾器實(shí)現(xiàn)呢?
總結(jié)
裝飾器模式有不少優(yōu)點(diǎn),它是繼承的有力補(bǔ)充,比繼承靈活,在不改變原有對象的情況下,動態(tài)地給一個對象擴(kuò)展功能,即插即用。通過使用不同裝飾類及這些裝飾類的排列組合,可以實(shí)現(xiàn)不同效果,完全遵循程序設(shè)計(jì)的“開閉原則”。
但裝飾器的使用必將會給程序帶來更高的復(fù)雜性,更低的可讀性,子類集成的代碼結(jié)構(gòu)會更直白易懂一些,而且雖然裝飾器符合“開閉原則”,但是它會給程序帶來更多的類,動態(tài)裝飾在多層裝飾時會更復(fù)雜。
所以總體上使用裝飾器模式的時候也是兩害相較取其輕,為了不頻繁修改已經(jīng)成型的子類而引入更多裝飾器類。
應(yīng)用的時候一定要謹(jǐn)記裝飾器是“增強(qiáng)”某個事物用的,可千萬別把事物本身實(shí)現(xiàn)的主邏輯用裝飾器實(shí)現(xiàn)了。
本文來自微信公眾號:網(wǎng)管叨 bi 叨 (ID:kevin_tech),作者:KevinYan11
廣告聲明:文內(nèi)含有的對外跳轉(zhuǎn)鏈接(包括不限于超鏈接、二維碼、口令等形式),用于傳遞更多信息,節(jié)省甄選時間,結(jié)果僅供參考,IT之家所有文章均包含本聲明。