從未這麼明白的設計模式(三):裝飾器模式


本文原創地址:jsbintask的博客(食用效果最佳),轉載請註明出處!

同系列文章:
從未這麼明白的設計模式(二):觀察者模式
從未這麼明白的設計模式(一):單例模式

前言

裝飾器模式是爲了運行時動態的擴展一個類的功能。它謹遵開閉原則,它實現的關鍵在於繼承和組合的結合使用,解耦對象之間的關係。
各種設計模式學習地址:https://github.com/jsbintask22/design-pattern-learning

栗子

首先我們列舉一個案例,並且按照面向對象的思想來對應實體之間的關係。

有一個咖啡店,銷售各種各樣的咖啡,拿鐵,卡布奇洛,藍山咖啡等,在沖泡前,會詢問顧客是否要加糖,加奶,加薄荷等。這樣不同的咖啡配上不同的調料就會賣出不同的價格。


V1

針對上面的栗子,我們很容易就抽象出對應的實現,如上圖。接着,我們就要編寫對應的類來實現對應的功能。在這個例子中,主題當然就是咖啡,並且它有一個屬性是名字,一個行爲 價格,出於“面向對象”的思想,我們自然會設計出抽象類Coffee:

public abstract class Coffee {
    /**
     * 獲取咖啡得名字
     */
    public abstract String getName();

    /**
     * 獲取咖啡的價格
     */
    public abstract double getPrice();
}

接着,按照繼承的思想,我們要開始設計出具體的實現類,因爲拿鐵,卡布奇洛,藍山搭配上不同的調料(上面三種)會有不同的價格,名字,所以我們至少得設計出 3 X 3 = 9 個類來分別對應它們的名字和價格:


嗯!我想不用說這樣設計得缺陷也很明顯了! 由於不同的咖啡和不同的調料得各種任意組合,使得出現了類爆炸的現象。既然有這麼明顯的缺陷,那我們當然得改! 我們可以考慮把各種調料當作屬性加入到Coffee這個抽象類中,接着在實現類中計算價格和名字時,分別判斷是否加入了各種調料包,得到不同的名字和價格!

按照上面的思想,我們的Coffee類現在變成了這樣:

public abstract class Coffee {
    // 是否加了牛奶
    protected boolean addedMilk;
    // 是否加了糖
    protected boolean addedSugar;
    // 是否加了薄荷
    protected boolean addedMint;

    /**
     * 獲取咖啡得名字
     */
    public abstract String getName();

    /**
     * 獲取咖啡的價格
     */
    public abstract double getPrice();
}

接着,我們實現一種咖啡,藍山咖啡:

public class BuleCoffee extends Coffee {
    @Override
    public String getName() {
        StringBuilder name = new StringBuilder();
        name.append("藍山");
        if (addedMilk) {
            name.append("牛奶");
        }
        if (addedMilk) {
            name.append("薄荷");
        }
        if (addedSugar) {
            name.append("加糖");
        }
        return name.toString();
    }

    @Override
    public double getPrice() {
        double price = 10;
        if (addedMilk) {
            price += 1.1;
        }
        if (addedMilk) {
            price += 3.2;
        }
        if (addedSugar) {
            price += 2.7;
        }

        return price;
    }
}

嗯!現在似乎比上面愉快多了。其實不然!我們仔細分析這種設計,會發現它似乎不太符合”封裝的思想“,比如說針對拿鐵,對於加薄荷而言,對他總是多餘的! 而對於藍山而言,牛奶又顯得很多餘! 所以這種設計也並不合理。 另外,我們假設coffee,拿鐵等實體類來自第三方類庫,我們並不能改動這些類的實現, 又要怎麼得到名字和價格呢?

這個時候,我們就得使用裝飾器模式來動態的擴展類行爲! 所以我們設計出V3版本。

V3

開閉原則

首先,我們需要了解一個面向對象的一個基本設計原則:開閉原則,它指的是類應該對修改關閉,對擴展開放

怎麼理解呢? 就比如我們上方說的:假如cofee和它的一衆實現拿鐵,卡布奇洛,藍山來自第三方類庫,並且這個類庫已經很”適合“,”實用“了。 而我們爲了得到加入不同調料的咖啡的名字和價格,我們就得修改這些實現,而這樣的修改,總是免不了穩定性的改變。對原本的系統來說也是一種風險! 所以我們應該 對修改關閉,對擴展開放;

繼承和組合

遵循開閉原則,那我們就得對外擴展,那怎麼對外擴展呢? 這也是裝飾器模式實現的關鍵,利用繼承和組合的結合; 現在我們可以考慮設計出一個裝飾類,它也繼承自coffee,並且它內部有一個coffee的實例對象:


現在,我們多了一個咖啡裝飾器: CoffeeDecorator:

public abstract class CoffeeDecorator implements Coffee {
    private Coffee delegate;

    public CoffeeDecorator(Coffee coffee) {
        this.delegate = coffee;
    }

    @Override
    public String getName() {
        return delegate.getName();
    }

    @Override
    public double getPrice() {
        return delegate.getPrice();
    }
}

接着,我們將牛奶,薄荷作爲抽象出一個類,繼承自CoffeeDecorator,所以,現在類圖就成了這樣:


我們實現一個MilkCoffeeDecorator

public class MilkCoffeeDecorator extends CoffeeDecorator {
    public MilkCoffeeDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getName() {
        return "牛奶, " + super.getName();
    }

    @Override
    public double getPrice() {
        return 1.1 + super.getPrice();
    }
}

按同樣的方法可以實現出MintCoffeeDecoratorSugarCoffeeDecorator。接着我們寫一個測試類:

public class App {
    public static void main(String[] args) {
        // 得到一杯原始的藍山咖啡
        Coffee blueCoffee = new BlueCoffee();
        System.out.println(blueCoffee.getName() + ": " + blueCoffee.getPrice());

        // 加入牛奶
        blueCoffee = new MilkCoffeeDecorator(blueCoffee);
        System.out.println(blueCoffee.getName() + ": " + blueCoffee.getPrice());

        // 再加入薄荷
        blueCoffee = new MintCoffeeDecorator(blueCoffee);
        System.out.println(blueCoffee.getName() + ": " + blueCoffee.getPrice());

        // 再加入糖
        blueCoffee = new SugarCoffeeDecorator(blueCoffee);
        System.out.println(blueCoffee.getName() + ": " + blueCoffee.getPrice());
    }
}

從結果我們可以看出,隨着不斷加入各種調料,價格,名字都在改變! 這說明我們加入不同的調料,動態的改變了咖啡的名字和價格!

思考

從上面的最後的裝飾器模式的實現來看,我們可以得出以下結論:

  1. 通過裝飾器模式可以動態的將責任附加到原有的對象上,而不改變原有的code。
  2. 遵循開閉原則
  3. 裝飾者和被裝飾者有相同的父類(如栗子中的Coffee)
  4. 可以用多個裝飾器裝飾同一個對象。(見運行類)
  5. 裝飾者可以在被裝飾者的行爲之前或之後動態的加上自己的行爲。(參考裝飾實現)
  6. 組合比繼承更加的靈活(上面的coffee代理)

擴展

到現在,我們已經實現了一個自己的裝飾器,我們來看看jdk中用到的裝飾器實現.

IO

我們可以查看FilterInputStream:


它的主要是實現者爲BufferedInputStream:

所以我們經常可以使用BufferedInputStream裝飾一個InputStream,比如FileInputStream:
new BufferedInputStream(FileInputStream);
這就是裝飾器模式的典型應用。

tomcat

在tomcat的HttpServletRequest的內部實現代碼中,RequestFacde繼承自HttpServlet,而它內部的實現也是通過代理Request對象,而Request對象繼承自HttpServlet,Request內部代理了org.apache.coyote.Request來實現的。

總結

裝飾器模式充分展示了組合的靈活。利用它來實現擴展。它同時也是開閉原則的體現。 如果相對某個類實現運行時功能動態的擴展。 這個時候你就可以考慮使用裝飾者模式!

關注我,這裏只有乾貨!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章