和表妹去喝奶茶
表妹:哥啊,我想喝奶茶。
我:走啊,去哪裡喝?
表妹:走,我帶你去,我經常去的那家,不但好喝,還可以自由搭配很多小料。我每次都是不同的搭配,換著喝,嘻嘻。
我:你倒是挺會喝的嘛~
你看,這不是很像我們設計模式中的裝飾器模式嘛?
在我們生活中,還有很多這樣的例子。比如,女孩子墊個鼻子,整個雙眼皮;男孩子給自己的愛車改個剎車系統,改進氣和排氣系統等。
動態地給一個物件新增一些額外的職責,就增加功能來說,裝飾器模式比生成子類更為靈活。
走,我們來去奶茶店看看。
基於繼承的設計方案
如果使用單純使用繼承的方式來實現奶茶的各種品類的話,結構體系如下圖所示:
你看,如果新增一種奶茶,就要繼承父類,新增一個實現子類,如果要新增一種新的搭配,比如,紅豆珍珠果粒奶蓋奶茶,那麼就要繼承已有的子類,也是要新增一個實現子類。那麼,如此不斷新增下去,就會導致繼承體系過於龐大,且類膨脹。
那麼,如果修改已有的子類呢,比如修改紅豆奶蓋,那麼,紅豆珍珠奶蓋也要跟著修改。
所以,維護這樣的業務系統是很難受的。這個時候,裝飾器模式就派上用場啦~
基於裝飾器模式的設計方案
我們先來看看裝飾器模式的UML類圖:
-
Component:元件物件的抽象介面,可以給這些物件動態的增加職責/功能。
-
ConcreteComponent:具體的元件的物件,實現元件物件的介面,是被裝飾器裝飾的原始物件,即可以給這個物件動態地新增職責。
-
Decorator:所有裝飾器的抽象父類,實現了元件物件的介面,並且持有一個元件物件(被裝飾的物件)。
-
ConcreteDecorator:具體的裝飾器,具體實現向裝飾物件新增功能。
現在,奶茶店使用裝飾器模式後,如下圖所示:
我們之前在學習橋接模式的時候,說過【組合優於繼承】。你看,現在是不是清晰很多了,如果奶茶小料有新增的話,直接實現一個小料的裝飾器即可,客戶想怎麼搭配都可以。接下來,我們看看具體的程式碼實現。
Component:奶茶抽象類,該類定義了製作奶茶的抽象方法。
1 // 奶茶 2 abstract class MilkTea { 3 4 // 製作奶茶的過程 5 public abstract void process(); 6 7 }
ConcreteComponent:奶茶店提供的最基本的招牌奶茶,這種奶茶不加任何小料。
1 // 招牌奶茶 2 class SignatureMilkTea extends MilkTea { 3 4 @Override 5 // 製作招牌奶茶的過程 6 public void process() { 7 System.out.println("招牌奶茶製作完成"); 8 } 9 10 }
另外2種奶茶:紅豆奶茶和珍珠奶茶,不是用繼承實現,而是用裝飾器實現。這兩種奶茶都是基於上面招牌奶茶新增不同的小料製作而成的。Decorator:為了方便擴充套件,下面實現一個裝飾器的抽象類。
1 // 奶茶裝飾器 2 abstract class MilkTeaDecorator extends MilkTea { 3 4 private MilkTea milkTea; 5 6 public MilkTeaDecorator(MilkTea milkTea) { 7 this.milkTea = milkTea; 8 } 9 10 @Override 11 public void process() { 12 this.milkTea.process(); 13 } 14 }
ConcreteDecorator:接下來,就根據這個奶茶裝飾器,實現紅豆奶茶和珍珠奶茶。
1 // 紅豆奶茶 2 class RedBeanMilkTea extends MilkTeaDecorator { 3 4 public RedBeanMilkTea(MilkTea milkTea) { 5 super(milkTea); 6 } 7 8 @Override 9 public void process() { 10 super.process(); 11 System.out.println("加點紅豆"); 12 } 13 } 14 15 // 珍珠奶茶 16 class BubbleTea extends MilkTeaDecorator { 17 18 public BubbleTea(MilkTea milkTea) { 19 super(milkTea); 20 } 21 22 @Override 23 public void process() { 24 super.process(); 25 System.out.println("加點珍珠"); 26 } 27 }
昨天,表妹喝了紅豆奶茶:
1 MilkTea milkTea = new SignatureMilkTea(); 2 RedBeanMilkTea redBeanMilkTea = new RedBeanMilkTea(milkTea); 3 redBeanMilkTea.process(); 4 5 // 列印結果為: 6 // 招牌奶茶製作完成 7 // 加點紅豆
今天,她說她想和紅豆珍珠奶茶:
1 MilkTea milkTea = new SignatureMilkTea(); 2 RedBeanMilkTea redBeanMilkTea = new RedBeanMilkTea(milkTea); 3 BubbleTea redBeanBubble = new BubbleTea(redBeanMilkTea); 4 redBeanBubble.process(); 5 6 // 列印結果為: 7 // 招牌奶茶製作完成 8 // 加點紅豆 9 // 加點珍珠
你看,是不是很容易就滿足了客戶的需求。那如果還可以加奶蓋,應該怎麼設計呢?
1 // 奶蓋奶茶 2 class MilkCapMilkTea extends MilkTeaDecorator { 3 4 public MilkCapMilkTea(MilkTea milkTea) { 5 super(milkTea); 6 } 7 8 @Override 9 public void process() { 10 super.process(); 11 System.out.println("加上奶蓋"); 12 } 13 }
你看,直接繼承奶茶裝飾器MilkTeaDecorator,實現一個MilkCapMilkTea類即可,然後客戶想怎麼搭配都可以:
1 // 奶蓋招牌奶茶 2 MilkTea milkTea = new SignatureMilkTea(); 3 MilkCapMilkTea milkCapMilkTea = new MilkCapMilkTea(milkTea); 4 milkCapMilkTea.process(); 5 6 // 加了紅豆、珍珠和奶蓋的奶茶 7 MilkTea milkTea = new SignatureMilkTea(); 8 MilkCapMilkTea milkCapMilkTea = new MilkCapMilkTea(milkTea); 9 RedBeanMilkTea redBeanMilkTea = new RedBeanMilkTea(milkCapMilkTea); 10 BubbleTea luxuryMilkTea = new BubbleTea(redBeanMilkTea); 11 luxuryMilkTea.process();
你看,是不是擴充套件很靈活,遵守了開-閉原則。而且,如果已經有小料裝飾器的話,不管客戶如何搭配,我們都不需要增加實現類,從而避免了類膨脹的問題。
如果使用繼承來實現該業務系統,就會導致繼承體系過於龐大,類膨脹等問題,如果有一天要修改或刪除某一個子類,就會導致牽一髮而動全身,這是非常可怕的。而裝飾器模式提供了一個非常好的解決方案,它把每個要裝飾的“奶茶小料”放在單獨的類中,並讓這個類包裝它所要裝飾的物件(奶茶),因此,當需要新增小料的時候,客戶程式碼就可以在執行時候根據需要有選擇地、按順序地使用裝飾功能包裝物件了。
可能有些同學會問,這不就是代理模式嘛?
裝飾器模式和代理模式的區別
是的,對於裝飾器模式來說,裝飾者和被裝飾者都實現一個介面;對代理模式來說,代理類和委託類也都實現同一個介面。不論我們使用哪一種模式,都可以很容易地在真實物件的方法前面或後面加上自定義的方法。
這裡我們先簡單看一下兩者的區別,另外一篇我們再仔細分析這兩種設計模式的區別哈。
代理模式注重的是對物件的某一功能的流程把控和輔助,它可以控制物件做某些事,重點是為了借用物件的功能完成某一流程,而非物件功能如何。
裝飾器模式注重的是對物件功能的擴充套件,不關心外界如何呼叫,只注重對物件功能加強,裝飾後還是物件本身。
裝飾器模式的優點
-
比繼承更靈活
從為物件新增功能的角度來看,裝飾器模式比繼承更靈活。繼承是靜態的,而且一旦繼承所有子類都有一樣的功能。而裝飾器模式採用把功能分離到每個裝飾器當中,然後通過物件組合的方式,在執行時動態地組合功能,每個被裝飾的物件最終有哪些功能,是由執行期動態組合的功能來決定的。
-
更容易複用功能
裝飾器模式把一系列複雜的功能分散到每個裝飾器當中,一般一個裝飾器只實現一個功能,使實現裝飾器變得簡單,更重要的是這樣有利於裝飾器功能的複用,可以給一個物件增加多個同樣的裝飾器,也可以把一個裝飾器用來裝飾不同的物件,從而實現複用裝飾器的功能。
-
簡化高層定義
裝飾器模式可以通過組合裝飾器的方式,為物件增添任意多的功能。因此在進行高層定義的時候,不用把所有的功能都定義出來,而是定義最基本的就可以了,可以在需要使用的時候,組合相應的裝飾器來完成所需的功能。
裝飾器模式的缺點
-
會產生很多細粒度物件。
前面說了,裝飾器模式是把一系列複雜的功能,分散到每個裝飾器當中,一般一個裝飾器只實現一個功能,這樣會產生很多細粒度的物件,而且功能越複雜,需要的細粒度物件越多。
-
多層的裝飾是比較複雜的。
就像剝洋蔥一樣,你剝到了最後才發現是最裡層的裝飾出現了問題,這個工作量是很大的。因此,儘量減少裝飾類巢狀的層數,以便降低系統的複雜度。
裝飾器模式的應用場景
-
需要擴充套件一個類的功能,或給一個類增加附加功能;
-
需要動態地給一個物件增加功能,這些功能可以再動態地撤銷;
-
需要為一批的兄弟類進行改裝或加裝功能。
總結
繼承是靜態地給類新增功能,而裝飾器模式則是動態地增加功能。
Java IO類中大量使用了裝飾器模式,學完該模式,可以去看看原始碼中是如何使用的,鞏固知識。
參考
《研磨設計模式》
《設計模式之禪》