裝飾者模式
每天我們出門前,一定都會選擇今天上衣穿什么,褲子穿什么,搭配什么鞋子,大衣穿什么。最后一定是做好選擇,打扮好才會出門。這個過程其實就是裝飾者模要做的事情 ---- 對一個對象增加額外的功能。
我們再看一個例子。我們都吃過煎餅,除了面餅之外,我們還要加雞蛋、加蔥花、香菜、面醬、辣醬。現在還有新花樣,加辣條、加雞柳。一切都始于一張面餅,攤煎餅的過程就是在不斷對這張面餅添加新特性。
我們通過繼承也可以為對象增加功能,比如我們有個煎餅的父類,默認已經有面餅、面醬、雞蛋啊。那么我們可以派生出 全都放的普通煎餅、不辣的普通煎餅、不辣不放香菜的普通煎餅、不辣不放蔥的普通煎餅、全都放的辣條煎餅、全都放的雞柳煎餅…… 這只是很小一部分。通過繼承的話,由于情況太多,會造成對象爆炸。
那我們還可以通過組合的方式來擴展類啊,比如煎餅對象中,我們可以設置不同屬性,比如是否有蔥、是否有香菜、是否有辣條、是否有雞柳等等。這樣看起來也能很好的解決攤煎餅的問題。但如果想要加腸、加油條怎么辦?想要加兩個雞蛋怎么辦?我們只能修改煎餅對象。這就違反了開閉原則。顯然這樣也是不夠靈活的。
裝飾者模式能夠很好的解決對象的動態擴展,不管你想穿什么,都可以隨便搭配。不過這個煎餅要怎么做,也都能隨意的擴展支持,而不需要改已有的代碼。接下來我們就來看看如何通過裝飾者模式來攤煎餅的。
1. 實現裝飾者模式
對于攤煎餅來說,我們都是對于一個基礎的煎餅對象做裝飾,比如我想要一套兩個雞蛋、有辣椒、蔥、辣條的煎餅,那么我只需要先聲明一個基本的煎餅對象,然后用加雞蛋裝飾類裝飾它,然后再用加辣醬裝飾類裝飾它,再用加蔥的裝飾類裝飾它,最后再用加辣條的裝飾類裝飾它。最終就得到了我想要的煎餅。不過請注意,不管你怎么裝飾,最終得到的還是煎餅,并不是其他東西。
裝飾者模式的核心思想是對已有的對象,一層一層的用裝飾類去裝飾它,擴展它的特性。這樣做可以更為動態的為對象增加功能。我們看看代碼如何實現:
先定義煎餅接口:
public interface Pancake {
void cook();
}
接口里只定義了一個制作方法。
煎餅接口的實現類:
public class BasicPancake implements Pancake {
@Override
public void cook() {
System.out.println("加一勺面");
System.out.println("加一個雞蛋");
}
}
作為一個最基本的煎餅,總得有面,有雞蛋吧。其他的材料留給裝飾類來實現。
接下來我們定義裝飾抽象類:
public abstract class PancakeDecorator implements Pancake {
protected Pancake pancake;
public void setPancake(Pancake pancake) {
this.pancake = pancake;
}
public void cook() {
if (pancake != null) {
pancake.cook();
}
}
}
可以看到 PancakeDecorator
同樣要實現 Pancke
接口。并且持有 Pancke
類型的引用,自己實現的 cook 方法實際調用了持有的 Pancake
對象的 cook 方法。
加辣醬的裝飾類代碼如下,其他裝飾實現類是類似的。
public class AddSpicyDecorator extends PancakeDecorator{
@Override
public void cook(){
super.cook();
System.out.println("加辣醬");
}
}
cook 方法首先調父類的 cook 方法,然后再加入自己的特性。
客戶端代碼如下,我們看看如何利用裝飾類來生成你想要的煎餅。
public class Client {
public static void main(String[] args) {
Pancake pancake = new BasicPancake();
PancakeDecorator addEggPancake = new AddEggDecorator();
addEggPancake.setPancake(pancake);
PancakeDecorator addSaucePancake = new AddSauceDecorator();
addSaucePancake.setPancake(addEggPancake);
PancakeDecorator addLaTiaoPancake = new AddLaTiaoDecorator();
addLaTiaoPancake.setPancake(addSaucePancake);
addLaTiaoPancake.cook();
}
}
我們聲明了三個包裝類,對 BasicPancake
層層包裝,最后得到一套兩個雞蛋、加辣醬、加辣條的煎餅。運行后輸出如下:
加一勺面
加一個雞蛋
加一個雞蛋
加面醬
加辣條
如果你研發了新煎餅,要加新的輔料,比如香腸、榨菜之類,那么只需要增加裝飾類的實現即可。從而實現了開閉原則。
類圖如下:
2. 裝飾者模式優缺點
2.1 優點
- 動態的為對象添加額外職責:通過組合不同裝飾類,非常靈活的為對象增加額外的職責;
- 避免子類爆炸:當不同的特性組合,構成不同的子類時,必然造成子類爆炸。但通過裝飾者靈活組合,可以避免這個問;
- 分離核心功能和裝飾功能:核心業務保留在
Component
的子類中。而裝飾特性在Decorator
的實現類中去實現。面對裝飾特性的變化,實現了開閉原則,只需要增加裝飾實現類; - 很方便的重復添加特性:我想要一套兩個雞蛋,雙份辣條的煎餅。是不是只需要多裝飾一次就可以了?就是這么簡單。
2.2 缺點
- 由于不是通過繼承實現添加職責,所以被裝飾后的對象并不能通過對象本身就能了解其特性。而需要分析所有對其裝飾過的對象;
- 裝飾模式會造成有很多功能類似的小對象。通過組合不同的裝飾實現,來達成不同的需求。這樣對于不了解系統的人,比較難以學習。過多的裝飾類進行裝飾,也稍顯繁瑣。
3. 裝飾者模式適用場景
使用裝飾者模式,有以下幾種情況:
- 需要一個裝飾的載體。不能將全部特性都放在裝飾類中。換句話講得有個裝飾主體,核心特性在主體對象中實現。例如瀏覽器窗口,不管是加邊框還是滾動條,都是基于窗口的;
- 有多種特性可以任意搭配,對主體進行擴展。并且你想以動態、透明的方式來實;
- 不能以生成子類的方式擴展??赡苡袃煞N情況,一是對大量子類帶來的類爆炸有所顧慮。二是類定義被隱藏,或者不能用于生成子類。
4. 小結
裝飾者模式的優勢在于動態、透明的添加特性。要記住裝飾者裝飾完的對象還是之前的對象類型。通過分離核心特性和裝飾特性,客戶端代碼可以靈活的搭配使用包裝對象,從而得到具有想要行為的對象。不過要注意,有些時候裝飾的順序是要保證的。比如先放雞蛋,再放芝麻,芝麻就不會掉下去了。最好的做法是保證裝飾類的獨立。