設計模式之【裝飾器模式】

Gopher大威發表於2022-03-16

和表妹去喝奶茶

表妹:哥啊,我想喝奶茶。

:走啊,去哪裡喝?

表妹:走,我帶你去,我經常去的那家,不但好喝,還可以自由搭配很多小料。我每次都是不同的搭配,換著喝,嘻嘻。

:你倒是挺會喝的嘛~

你看,這不是很像我們設計模式中的裝飾器模式嘛?

在我們生活中,還有很多這樣的例子。比如,女孩子墊個鼻子,整個雙眼皮;男孩子給自己的愛車改個剎車系統,改進氣和排氣系統等。


動態地給一個物件新增一些額外的職責,就增加功能來說,裝飾器模式比生成子類更為靈活。

走,我們來去奶茶店看看。

基於繼承的設計方案

如果使用單純使用繼承的方式來實現奶茶的各種品類的話,結構體系如下圖所示:

 

你看,如果新增一種奶茶,就要繼承父類,新增一個實現子類,如果要新增一種新的搭配,比如,紅豆珍珠果粒奶蓋奶茶,那麼就要繼承已有的子類,也是要新增一個實現子類。那麼,如此不斷新增下去,就會導致繼承體系過於龐大,且類膨脹。

那麼,如果修改已有的子類呢,比如修改紅豆奶蓋,那麼,紅豆珍珠奶蓋也要跟著修改。

所以,維護這樣的業務系統是很難受的。這個時候,裝飾器模式就派上用場啦~

基於裝飾器模式的設計方案

我們先來看看裝飾器模式的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類中大量使用了裝飾器模式,學完該模式,可以去看看原始碼中是如何使用的,鞏固知識。

參考

《研磨設計模式》

《設計模式之禪》

https://mp.weixin.qq.com/s/BquPNZmG3tvG562hJm3YsQ

相關文章