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

cover

本文原創(chuàng)地址:jsbintask的博客(食用效果最佳),轉(zhuǎn)載請注明出處!

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

前言

裝飾器模式是為了運(yùn)行時動態(tài)的擴(kuò)展一個類的功能。它謹(jǐn)遵開閉原則,它實(shí)現(xiàn)的關(guān)鍵在于繼承和組合的結(jié)合使用,解耦對象之間的關(guān)系。
各種設(shè)計模式學(xué)習(xí)地址:https://github.com/jsbintask22/design-pattern-learning

栗子

首先我們列舉一個案例,并且按照面向?qū)ο蟮乃枷雭韺?yīng)實(shí)體之間的關(guān)系。

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


Decorator

V1

針對上面的栗子,我們很容易就抽象出對應(yīng)的實(shí)現(xiàn),如上圖。接著,我們就要編寫對應(yīng)的類來實(shí)現(xiàn)對應(yīng)的功能。在這個例子中,主題當(dāng)然就是咖啡,并且它有一個屬性是名字,一個行為 價格,出于“面向?qū)ο蟆钡乃枷?,我們自然會設(shè)計出抽象類Coffee:

Decorator

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

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

接著,按照繼承的思想,我們要開始設(shè)計出具體的實(shí)現(xiàn)類,因為拿鐵,卡布奇洛,藍(lán)山搭配上不同的調(diào)料(上面三種)會有不同的價格,名字,所以我們至少得設(shè)計出 3 X 3 = 9 個類來分別對應(yīng)它們的名字和價格:

Decorator

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

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

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

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

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

接著,我們實(shí)現(xiàn)一種咖啡,藍(lán)山咖啡:

public class BuleCoffee extends Coffee {
    @Override
    public String getName() {
        StringBuilder name = new StringBuilder();
        name.append("藍(lán)山");
        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;
    }
}

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

這個時候,我們就得使用裝飾器模式來動態(tài)的擴(kuò)展類行為! 所以我們設(shè)計出V3版本。

V3

開閉原則

首先,我們需要了解一個面向?qū)ο蟮囊粋€基本設(shè)計原則:開閉原則,它指的是類應(yīng)該對修改關(guān)閉,對擴(kuò)展開放

怎么理解呢? 就比如我們上方說的:假如cofee和它的一眾實(shí)現(xiàn)拿鐵,卡布奇洛,藍(lán)山來自第三方類庫,并且這個類庫已經(jīng)很”適合“,”實(shí)用“了。 而我們?yōu)榱说玫郊尤氩煌{(diào)料的咖啡的名字和價格,我們就得修改這些實(shí)現(xiàn),而這樣的修改,總是免不了穩(wěn)定性的改變。對原本的系統(tǒng)來說也是一種風(fēng)險! 所以我們應(yīng)該 對修改關(guān)閉,對擴(kuò)展開放;

繼承和組合

遵循開閉原則,那我們就得對外擴(kuò)展,那怎么對外擴(kuò)展呢? 這也是裝飾器模式實(shí)現(xiàn)的關(guān)鍵,利用繼承和組合的結(jié)合; 現(xiàn)在我們可以考慮設(shè)計出一個裝飾類,它也繼承自coffee,并且它內(nèi)部有一個coffee的實(shí)例對象:

Decorator

現(xiàn)在,我們多了一個咖啡裝飾器: 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,所以,現(xiàn)在類圖就成了這樣:

Decorator

我們實(shí)現(xiàn)一個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();
    }
}

按同樣的方法可以實(shí)現(xiàn)出MintCoffeeDecorator,SugarCoffeeDecorator。接著我們寫一個測試類:

public class App {
    public static void main(String[] args) {
        // 得到一杯原始的藍(lán)山咖啡
        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());
    }
}
Decorator

從結(jié)果我們可以看出,隨著不斷加入各種調(diào)料,價格,名字都在改變! 這說明我們加入不同的調(diào)料,動態(tài)的改變了咖啡的名字和價格!

思考

從上面的最后的裝飾器模式的實(shí)現(xiàn)來看,我們可以得出以下結(jié)論:

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

擴(kuò)展

到現(xiàn)在,我們已經(jīng)實(shí)現(xiàn)了一個自己的裝飾器,我們來看看jdk中用到的裝飾器實(shí)現(xiàn).

IO

我們可以查看FilterInputStream:

Decorator

它的主要是實(shí)現(xiàn)者為BufferedInputStream:
Decorator

所以我們經(jīng)??梢允褂肂ufferedInputStream裝飾一個InputStream,比如FileInputStream:
new BufferedInputStream(FileInputStream);
這就是裝飾器模式的典型應(yīng)用。

tomcat

在tomcat的HttpServletRequest的內(nèi)部實(shí)現(xiàn)代碼中,RequestFacde繼承自HttpServlet,而它內(nèi)部的實(shí)現(xiàn)也是通過代理Request對象,而Request對象繼承自HttpServlet,Request內(nèi)部代理了org.apache.coyote.Request來實(shí)現(xiàn)的。

總結(jié)

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

關(guān)注我,這里只有干貨!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容