天天看點

軟體設計模式 | 結構型之裝飾者模式詳解

目錄

    • 星巴茲咖啡的故事
    • 裝飾者模式
      • 一、模式介紹
      • 二、模式實作
      • 三、模式設計
      • 四、模式的優缺點
      • 五、使用場景:
      • 六、裝飾者用到的設計原則:

星巴茲咖啡的故事

  1. 現在有一個咖啡館,他有一套自己的訂單系統,當顧客來咖啡館的時候,可以通過訂單系統來點自己想要的咖啡。他們原先的設計是這樣子的:
    軟體設計模式 | 結構型之裝飾者模式詳解
  2. 每一種飲料都繼承抽象飲料類 Beverage,計算價錢隻需要調用相應子類的 cost() 方法即可,其中 description 是其相應的飲料描述。飲料少的情況下完全沒問題,但咖啡館為了吸引更多的顧客,在系統中允許顧客選擇加入不同的調料,如:蒸奶(Steamed Milk)、豆漿(Soy)、摩卡(Mocha,也就是巧克力風味)或覆寫奶泡。星巴茲會根據所加入的調料收取不同的費用。是以訂單系統必須考慮到這些調料部分。如果說依舊用原來的設計那麼整個系統會出現爆炸的狀況。(當然我隻畫了一小部分,再多點 visio 就崩潰了 /(ㄒoㄒ)/ )
    軟體設計模式 | 結構型之裝飾者模式詳解
  3. 這時,有個人提出了新的方案,利用執行個體變量和繼承,來追蹤這些調料。具體為:先從Beverage基類下手,加上執行個體變量代表是否加上調料(牛奶、豆漿、摩卡、奶泡……)
    軟體設計模式 | 結構型之裝飾者模式詳解
    軟體設計模式 | 結構型之裝飾者模式詳解

    這種設計雖然滿足了現在的需求,但也存在非常大的局限性:

      最主要的問題就是,如果顧客選擇了雙份的奶泡(配料),該怎麼辦?

      其次,出現了新的材料,那麼還需要修改父類中的 cost() 方法,這違反了開放關閉原則(類應該對擴充開放,對修改關閉)。

       …

      那我們怎麼辦呢?好啦,裝飾者可以非常完美的解決以上的所有問題,讓我們有一個設計非常nice的咖啡館。

裝飾者模式

一、模式介紹

  裝飾器模式(Decorator Pattern)允許向一個現有的對象添加新的功能,同時又不改變其結構。這種類型的設計模式屬于結構型模式,它是作為現有的類的一個包裝。

  這種模式建立了一個裝飾類,用來包裝原有的類,并在保持類方法簽名完整性的前提下,提供了額外的功能。

  我們通過下面的執行個體來示範裝飾器模式的用法。

二、模式實作

以摩卡和奶泡深焙咖啡為例:

步驟:

  1. 拿到一個深焙咖啡(DarkRoast)對象
  2. 以摩卡(Mocha)對象去裝飾它
  3. 以奶泡(Whip)對象去裝飾它
  4. 調用 cost() 方法

圖示步驟:

  1. 拿到一個深焙咖啡(DarkRoast)對象
    軟體設計模式 | 結構型之裝飾者模式詳解
  2. 顧客要摩卡(Mocha),是以建立 Mocha 對象,并用它将 DarkRoast 對象裝起來
    軟體設計模式 | 結構型之裝飾者模式詳解
  3. 顧客要奶泡(Whip),是以建立 Whip 對象,并用它将以 Mocha 裝飾的 DarkRoast 對象裝起來
    軟體設計模式 | 結構型之裝飾者模式詳解
      可以把 Mocha 裝飾之後的 DarkRoast 仍然看成是一個 Beverage ,原因在于多态!無論是裝飾者摩卡(Mocha)、奶泡(Whip)還是被裝飾者深焙咖啡 歸根結底都是繼承着頂層的抽象類(Beverage)。是以無論 DarkRoast 被多少裝飾者裝飾着包裝着都是一個Beverage。
  4. 調用 cost() 方法,遞歸實作計算最終價錢。
軟體設計模式 | 結構型之裝飾者模式詳解

三、模式設計

裝飾者模式的模闆類圖:

軟體設計模式 | 結構型之裝飾者模式詳解

裝飾者模式所包含的角色如下:

  1. Component : 抽象構件角色,定義對象的接口,可以給這些對象動态增加職責。
  2. ConcreteComponent : 被裝飾者,它是我們将要動态地加上新行為的對象,它擴充自Component。
  3. Decorator : 這是裝飾者共同實作的接口(抽象類)。
  4. ConcreteDecator : 具體的裝飾者,可以加上新的方法。新行為使用過在舊行為前面或者後面做一些計算來添加的,并且每一個裝飾者 都有一個執行個體變量,它可以記錄所裝飾的事物。

根據以上模闆類圖來繪制出訂單系統的類圖:

軟體設計模式 | 結構型之裝飾者模式詳解

代碼如下:

Beverage : 抽象飲料類

/**
 * 抽象飲料類
 */
public abstract class Beverage {

    String name = "Unknown Beverage";

    //傳回飲料名字
    public String getName() {
        return name;
    }

    //cost方法是用來傳回飲料的價錢,需在具體類中自己實作
    public abstract int cost();
}
           

具體飲料類:被裝飾者,包括:獨家調配的咖啡(HouseBlend)、深焙咖啡(DarkRoast)、濃縮咖啡(Espresso)、低咖啡因咖啡類(Decaf)

/**
 * 獨家調配的咖啡類(一種具體飲料)
 */
public class HouseBlend extends Beverage {
    //說明他是HouseBlend飲料
    public HouseBlend() {
        name = "獨家調配的咖啡";
    }
    //實作cost方法,用來傳回 獨家調配的咖啡 的價格
    @Override
    public int cost() {
        return 9;
    }
}
           
/**
 * 深焙咖啡類(一種具體飲料)
 */
public class DarkRoast extends Beverage {

    //說明他是DarkRoast飲料
    public DarkRoast() {
        name = "深焙咖啡";
    }

    //實作cost方法,用來傳回 深焙咖啡 的價格
    @Override
    public int cost() {
        return 11;
    }
}
           
/**
 * 濃縮咖啡類(一種具體飲料)
 */
public class Espresso extends Beverage {

    //說明他是Espresso飲料
    public Espresso() {
        name = "濃縮咖啡";
    }

    //實作cost方法,用來傳回 濃縮咖啡 的價格
    @Override
    public int cost() {
        return 12;
    }
}
           
/**
 * 低咖啡因咖啡類(一種具體飲料)
 */
public class Decaf extends Beverage {

    //說明他是Decaf飲料
    public Decaf() {
        name = "低咖啡因咖啡";
    }

    //實作cost方法,用來傳回 低咖啡因咖啡 的價格
    @Override
    public int cost() {
        return 10;
    }
}
           

Dector : 裝飾者共同實作的接口

/**
 * 調料裝飾着抽象類(繼承自飲料抽象類)
 */
public abstract class Decorator extends Beverage {

    /**
     * 所有的調料裝飾者都必須重新實作getDescription()方法
     * 這樣才能夠用遞歸的方式來得到所選飲料的整體描述
     */
    public abstract String getName();
}
           

ConcreteDecorator : 具體的裝飾者,包括:牛奶(Milk)、摩卡(Mocha)、豆漿(Soy)、奶泡(Whip)

/**
 * 牛奶調料類(繼承自 Decorator)
 */
public class Milk extends Decorator {
    //用一個執行個體變量記錄飲料,也就是被裝飾者
    Beverage beverage;

    //構造器初始化飲料變量
    public Milk(Beverage beverage) {
        this.beverage = beverage;
    }

    //在原來飲料的基礎上添加上Milk描述(原來的飲料加入Milk調料,被Milk調料裝飾)
    @Override
    public String getName() {
        return beverage.getName() + ",牛奶";
    }

    //在原來飲料的基礎上加上Soy的價格(原來的飲料加入Soy調料,被Soy調料裝飾)
    @Override
    public int cost() {
        return beverage.cost() + 3;
    }
}
           
/**
 * 摩卡調料類(繼承自 Decorator)
 */
public class Mocha extends Decorator {

    //用一個執行個體變量記錄飲料,也就是被裝飾者
    Beverage beverage;

    //構造器初始化飲料變量
    public Mocha(Beverage beverage) {
        this.beverage = beverage;
    }

    //在原來飲料的基礎上添加上Mocha描述(原來的飲料加入Mocha調料,被Mocha調料裝飾)
    @Override
    public String getName() {
        return beverage.getName() + ",摩卡";
    }

    //在原來飲料的基礎上加上Mocha的價格(原來的飲料加入Mocha調料,被Mocha調料裝飾)
    @Override
    public int cost() {
        return beverage.cost() + 2;
    }
}
           
/**
 * 豆漿調料類(繼承自 Decorator)
 */
public class Soy extends Decorator {

    //用一個執行個體變量記錄飲料,也就是被裝飾者
    Beverage beverage;

    //構造器初始化飲料變量
    public Soy(Beverage beverage) {
        this.beverage = beverage;
    }

    //在原來飲料的基礎上添加上Soy描述(原來的飲料加入Soy調料,被Soy調料裝飾)
    @Override
    public String getName() {
        return beverage.getName() + ",豆漿";
    }

    //在原來飲料的基礎上加上Soy的價格(原來的飲料加入Soy調料,被Soy調料裝飾)
    @Override
    public int cost() {
        return beverage.cost() + 1;
    }
}
           
/**
 * 奶泡調料類(繼承自 Decorator)
 */
public class Whip extends Decorator {

    //用一個執行個體變量記錄飲料,也就是被裝飾者
    Beverage beverage;

    //構造器初始化飲料變量
    public Whip(Beverage beverage) {
        this.beverage = beverage;
    }

    //在原來飲料的基礎上添加上 Milk 描述(原來的飲料加入 Whip 調料,被 Whip 調料裝飾)
    @Override
    public String getName() {
        return beverage.getName() + ",奶泡";
    }

    //在原來飲料的基礎上加上 Whip 的價格(原來的飲料加入Soy調料,被 Whip 調料裝飾)
    @Override
    public int cost() {
        return beverage.cost() + 4;
    }
}
           

Client : 用戶端示範代碼

public class Client {
    public static void main(String[] args) {
        System.out.println("飲料價格如下:");
        System.out.println("獨家調配的咖啡類:9、低咖啡因咖啡:10、深焙咖啡類:11、濃縮咖啡類:12");
        System.out.println("調料價格如下:");
        System.out.println("豆漿:1、摩卡:2、牛奶:3、奶泡:4\n");
        //訂一杯濃縮咖啡類Espresso(12) 對象,不需要調料,列印出它的描述與價錢。
        Beverage beverage1 = new Espresso();
        System.out.println("飲料描述: " + beverage1.getName() + "  價格為: " + beverage1.cost());
        System.out.println("---------------------------------------------------");

        //制造出一個深焙咖啡類DarkRoast(11) 對象,用摩卡Mocha(2)裝飾它,用第二個摩卡Mocha(2)裝飾它,用奶泡Whip(4)裝飾它,列印出它的描述與價錢。
        Beverage beverage2 = new DarkRoast();
        beverage2 = new Mocha(beverage2);
        beverage2 = new Mocha(beverage2);
        System.out.println("飲料描述: " + beverage2.getName() + "  價格為: " + beverage2.cost());
        System.out.println("---------------------------------------------------");

        //也可用這種形式
        Beverage beverage3 = new Mocha( new Mocha(new DarkRoast()));
        System.out.println("飲料描述: " + beverage3.getName() + "  價格為: " + beverage3.cost());
        System.out.println("---------------------------------------------------");

        //再來一杯 低咖啡因咖啡Decaf(10)對象,并用豆漿Soy(1)、摩卡Mocha(2)、奶泡Whip(4),列印出它的描述與價錢。
        Beverage beverage4 = new Decaf();
        beverage4 = new Soy(beverage4);
        beverage4 = new Mocha(beverage4);
        beverage4 = new Whip(beverage4);
        System.out.println("飲料描述: " + beverage4.getName() + "  價格為: " + beverage4.cost());
    }
}
           

結果展示:

軟體設計模式 | 結構型之裝飾者模式詳解

四、模式的優缺點

優點: 裝飾類和被裝飾類可以獨立發展,不會互相耦合,裝飾模式是繼承的一個替代模式,裝飾模式可以動态擴充一個實作類的功能。

缺點: 多層裝飾比較複雜。

  

五、使用場景:

  1. 擴充一個類的功能。
  2. 動态增加功能,動态撤銷。

      

六、裝飾者用到的設計原則:

  1. 多用組合,少用繼承。
  2. 對擴充開放,對修改關閉。

本文的主要思路以及開篇引述的故事都借鑒于下面這篇部落格,部落客寫的非常清晰,如果沒有看懂的地方,可前往連結檢視詳情。

參考部落格:https://www.cnblogs.com/of-fanruice/p/11565679.html

繼續閱讀