1. 程式人生 > >設計模式學習筆記二------裝飾者模式

設計模式學習筆記二------裝飾者模式

設計原則:

  1. 少用繼承,多用組合
  2. 類應該對擴充套件開放,對修改關閉

目錄

本文的結構如下:

  • 什麼是裝飾者模式
  • 為什麼要用該模式
  • 模式的結構
  • 程式碼示例
  • 優點和缺點
  • 適用環境
  • 總結

一、什麼是裝飾模式

裝飾者模式動態地將責任附加到物件上。若要擴充套件功能,裝飾者提供了比繼承更有彈性的替代方案。和代理模式很相似,但在對被裝飾的物件的控制程度是不同的;裝飾者模式是對物件功能的加強,而代理模式是對物件施加控制,並不提供對物件本身功能的加強。

通俗點講,假設現在一杯純豆漿(Soya)賣1元錢,你可以選擇往裡邊加糖0.5元(Suger),加蜂蜜0.5元(Honey),加牛奶1元(Milk),加黑豆1元(BlackBean)。這裡面純豆漿就是被裝飾者,而糖、蜂蜜、牛奶、黑豆就是裝飾者了。

二、為什麼要用該模式

通過繼承的方式可以使子類具有父類的屬性和方法。子類繼承父類後,因為一些業務需求可以通過重寫的方式來加強父類的方法的一些功能,也可以重新定義某些屬性,即覆蓋父類的原有屬性和方法,使其獲得與父類不同的功能。

而裝飾者模式的最基本的功能就是對傳入的一個物件進行功能的加強與優化。

那麼問題來了,既然繼承方式也可以對已有類或物件進行加強,那為什麼還要衍生出裝飾者模式這一思想呢?

裝飾者模式的意圖定義為:動態地給一個物件新增一些額外的職責。裝飾者模式存在的更重要的意義就在於動態的為物件新增一些功能(或分配額外職責)。

以豆漿的例子為例,先嚐試用繼承的方式分析一下:設豆漿類為基類,每個類中有一個money屬性,那麼豆漿加牛奶可模擬為Soya類繼承Milk並重寫pay()方法,如此繼承確實可以計算出每種組合的價錢,於是有下圖:

一般的,我們為了擴充套件一個類經常使用繼承方式實現,由於繼承為類引入靜態特徵,並且隨著擴充套件功能的增多,子類會很膨脹。

在不想增加很多子類的情況下擴充套件類,如何實現呢?這時就要用到今天的主角:裝飾者模式了。

Java中的IO機制就用到了裝飾者模式。比如最常用的語句:

BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(filepath)));

BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

這就是最常見的裝飾者模式了,通過BufferedReader對已有物件FileReader的功能進行加強和優化。其實它不僅可以加強FileReader,所有的字元輸入流都可以通過這種方式進行包裝。

它是如何實現的呢?

將所有的字元輸入流抽象出了一個基類或介面即Reader,然後通過構造方法的形式將Reader傳遞給BufferedReader,此時BufferedReader就可以對所有的字元輸入流進行攔截和優化了。

如果採用繼承機制,每個XXXReader就要衍生出一個BufferedXXXReader,再加上字元輸出流和位元組輸入輸出流,那麼Java的IO體系結構該是多麼的臃腫不堪啊!而裝飾者模式的出現解決了這個問題,並且,裝飾者的出現也再一次的證明了面向物件的設計原則:多用組合,少用繼承!對擴充套件開放,對修改關閉

三、模式的結構

  1. Component,給出一個抽象介面或者抽象類,規範實現類的一些方法;
  2. ConcreteComponent:具體的一個元件,實現了抽象方法;
  3. Decorator:抽象的裝飾者,對抽象介面或者抽象類的一個引用,在 method 方法裡面使用這個引用完成任務;(代理模式需要例項化)
  4. ConcreteDecorator:具體的裝飾者,對抽象裝飾者的抽象部分進行實現。

第一步:抽象元件

/**
 * 抽象元件
 *
 * Created by w1992wishes on 2017/10/30.
 */
public abstract class Component {
    public abstract void method();
}

第二步:具體元件

/**
 * 具體元件
 *
 * Created by w1992wishes on 2017/10/30.
 */
public class ConcreteComponent extends Component {
    public void method() {
        System.out.println("work");
    }
}

第三步:抽象裝飾者

/**
 * 抽象的裝飾者
 *
 * Created by w1992wishes on 2017/10/30.
 */
public abstract class Decorator extends Component{
    private Component component;
    public Decorator(Component component){
        this.component = component;
    }

    @Override
    public void method(){
        beforeM();
        component.method();
        afterM();
    }

    public abstract void beforeM();

    public abstract void afterM();
}

第四步:具體的抽象者

/**
 * 具體的裝飾者
 *
 * Created by w1992wishes on 2017/10/30.
 */
public class ConcreteDecorator extends Decorator {

    public ConcreteDecorator(Component component){
        super(component);
    }

    @Override
    public void beforeM() {
        System.out.println("Relax, first having a game before work!");
    }

    public void afterM() {
        System.out.println("Relax, first having a game after work!");
    }
}

第五步:客戶端執行

/**
 * Created by w1992wishes on 2017/10/30.
 */
public class Client {
    public static void main(String[] args) {
        Component c = new ConcreteComponent();
        Decorator d = new ConcreteDecorator(c);
        d.method();
    }
}

結果:  Relax, first having a game before work!  work  Relax, first having a game after work!

四、程式碼示例

前面其實可以看作是一個程式碼示例,下面接著用豆漿的例子來段程式碼(這裡使用抽象介面):

首先抽象出一個介面,作為裝飾者建構函式的引數,即被裝飾者的父類:

/**
 * Created by w1992wishes on 2017/10/30.
 */
public interface Drink {
    public float money();//獲取價格。
    public String description();//返回商品資訊。
}

接下來就是裝飾者類,繼承此介面並通過構造方法獲取被裝飾物件公共介面:

/**
 * Created by w1992wishes on 2017/10/30.
 */
public abstract class Decorator implements Drink{
    private Drink drink;
    public Decorator (Drink drink){
        this.drink = drink;
    }
    @Override
    public String description() {
        return drink.description();
    }
    @Override
    public float money() {
        return drink.money();
    }
}

下面是被裝飾者Soya:

/**
 * 具體的裝飾者物件,純豆漿
 *
 * Created by w1992wishes on 2017/10/30.
 */
public class Soya implements Drink {
    public String description() {
        return "純豆漿";
    }
    public float money() {
        return 2f;
    }
}

下面分別是裝飾者Suger,Milk,BlackBean,Honey類:  Suger

/**
 * 具體的裝飾者類:糖
 *
 * Created by w1992wishes on 2017/10/30.
 */
public class Suger extends Decorator {
    public Suger(Drink drink) {
        super(drink);
    }
    public String description() {
        return super.description()+"+糖";
    }
    public float money() {
        return super.money()+1.5f;
    }
}

Milk

/**
 * 具體的裝飾者物件:牛奶
 *
 * Created by w1992wishes on 2017/10/30.
 */
public class Milk extends Decorator {
    public Milk(Drink drink) {
        super(drink);
    }
    public String description() {
        return super.description()+"+牛奶";
    }
    public float money() {
        return super.money()+1.5f;
    }
}

Honey

/**
 * 具體的裝飾者類:蜂蜜
 * 
 * Created by w1992wishes on 2017/10/30.
 */
public class Honey extends Decorator {
    public Honey(Drink drink) {
        super(drink);
    }
    public String description() {
        return super.description()+"+蜂蜜";
    }
    public float money() {
        return super.money()+1.5f;
    }
}

BlackBean

/**
 * 具體的裝飾者物件:黑豆
 *
 * Created by w1992wishes on 2017/10/30.
 */
public class BlackBean extends Decorator {
    public BlackBean(Drink drink) {
        super(drink);
    }
    public String description() {
        return super.description()+"+黑豆";
    }
    public float money() {
        return super.money()+0.5f;
    }
}

最後是測試程式碼:

/**
 * Created by w1992wishes on 2017/10/30.
 */
public class Client {
    public static void main(String[] args) {
        //定義一杯純豆漿
        Soya soya = new Soya();
        System.out.print("-----------------");
        System.out.println(soya.description()+" 價錢:"+soya.money());

        //豆漿加奶
        Milk milkSoya = new Milk(soya);
        System.out.print("-----------------");
        System.out.println(milkSoya.description()+" 價錢:"+milkSoya.money());

        //黑豆豆漿+奶
        BlackBean beanMilkSoya = new BlackBean(milkSoya);
        System.out.print("-----------------");
        System.out.println(beanMilkSoya.description()+"  價錢:"+beanMilkSoya.money());

        //黑豆豆漿+奶+蜂蜜
        Honey honeyBeanMilkSoya = new Honey(beanMilkSoya);
        System.out.print("-----------------");
        System.out.println(honeyBeanMilkSoya.description()+" 價錢:"+honeyBeanMilkSoya.money());
    }
}

結果:  —————–純豆漿 價錢:2.0  —————–純豆漿+牛奶 價錢:3.5  —————–純豆漿+牛奶+黑豆 價錢:4.0  —————–純豆漿+牛奶+黑豆+蜂蜜 價錢:5.5

五、優點和缺點

5.1、優點:

  • 裝飾模式與繼承關係的目的都是要擴充套件物件的功能,但是裝飾模式可以提供比繼承更多的靈活性。
  • 可以通過一種動態的方式來擴充套件一個物件的功能,通過配置檔案可以在執行時選擇不同的裝飾器,從而實現不同的行為。
  • 通過使用不同的具體裝飾類以及這些裝飾類的排列組合,可以創造出很多不同行為的組合。可以使用多個具體裝飾類來裝飾同一物件,得到功能更為強大的物件。
  • 具體構件類與具體裝飾類可以獨立變化,使用者可以根據需要增加新的具體構件類和具體裝飾類,在使用時再對其進行組合,原有程式碼無須改變,符合“開閉原則”。

5.2、缺點:

  • 使用裝飾模式進行系統設計時將產生很多小物件,這些物件的區別在於它們之間相互連線的方式有所不同,而不是它們的類或者屬性值有所不同,同時還將產生很多具體裝飾類。這些裝飾類和小物件的產生將增加系統的複雜度,加大學習與理解的難度。
  • 這種比繼承更加靈活機動的特性,也同時意味著裝飾模式比繼承更加易於出錯,排錯也很困難,對於多次裝飾的物件,除錯時尋找錯誤可能需要逐級排查,較為煩瑣。

六、適用環境

因為裝飾者模式的以下特點:

  1. 裝飾物件和真實物件有相同的介面。這樣客戶端物件就能以和真實物件相同的方式和裝飾物件互動。
  2. 裝飾物件包含一個真實物件的引用(reference)。
  3. 裝飾物件接受所有來自客戶端的請求。它把這些請求轉發給真實的物件。
  4. 裝飾物件可以在轉發這些請求以前或以後增加一些附加功能。這樣就確保了在執行時,不用修改給定物件的結構就可以在外部增加附加的功能。

所以在以下情況下可以使用裝飾模式:

  • 在不影響其他物件的情況下,以動態、透明的方式給單個物件新增職責。
  • 需要動態地給一個物件增加功能,這些功能也可以動態地被撤銷。
  • 當不能採用繼承的方式對系統進行擴充或者採用繼承不利於系統擴充套件和維護時。不能採用繼承的情況主要有兩類:第一類是系統中存在大量獨立的擴充套件,為支援每一種組合將產生大量的子類,使得子類數目呈爆炸性增長;第二類是因為類定義不能繼承(如final類)。

七、總結

  • 裝飾模式用於動態地給一個物件增加一些額外的職責,就增加物件功能來說,裝飾模式比生成子類實現更為靈活。它是一種物件結構型模式。
  • 裝飾模式包含四個角色:抽象構件定義了物件的介面,可以給這些物件動態增加職責(方法);具體構件定義了具體的構件物件,實現了在抽象構件中宣告的方法,裝飾器可以給它增加額外的職責(方法);抽象裝飾類是抽象構件類的子類,用於給具體構件增加職責,但是具體職責在其子類中實現;具體裝飾類是抽象裝飾類的子類,負責向構件新增新的職責。
  • 使用裝飾模式來實現擴充套件比繼承更加靈活,它以對客戶透明的方式動態地給一個物件附加更多的責任。裝飾模式可以在不需要創造更多子類的情況下,將物件的功能加以擴充套件。
  • 裝飾模式的主要優點在於可以提供比繼承更多的靈活性,可以通過一種動態的 方式來擴充套件一個物件的功能,並通過使用不同的具體裝飾類以及這些裝飾類的排列組合,可以創造出很多不同行為的組合,而且具體構件類與具體裝飾類可以獨立變化,使用者可以根據需要增加新的具體構件類和具體裝飾類;其主要缺點在於使用裝飾模式進行系統設計時將產生很多小物件,而且裝飾模式比繼承更加易於出錯,排錯也很困難,對於多次裝飾的物件,除錯時尋找錯誤可能需要逐級排查,較為煩瑣。
  • 裝飾模式適用情況包括:在不影響其他物件的情況下,以動態、透明的方式給 單個物件新增職責;需要動態地給一個物件增加功能,這些功能也可以動態地被撤銷;當不能採用繼承的方式對系統進行擴充或者採用繼承不利於系統擴充套件和維護時。
  • 裝飾模式可分為透明裝飾模式和半透明裝飾模式:在透明裝飾模式中,要求客 戶端完全針對抽象程式設計,裝飾模式的透明性要求客戶端程式不應該宣告具體構件型別和具體裝飾型別,而應該全部宣告為抽象構件型別;半透明裝飾模式允許使用者在客戶端宣告具體裝飾者型別的物件,呼叫在具體裝飾者中新增的方法。