從咖啡到裝飾者模式

前言:使用裝飾者的目的(why)

裝飾者採用運行時擴展,遠比編譯時期的繼承威力大。它還能夠在不修改底層代碼的情況下,給你的或者別人的對象賦予新的職責。

先來杯咖啡

Boss的咖啡店與時俱進準備搞一套網上點咖啡系統,它們最初的代碼設計是這樣的:
在這裏插入圖片描述
咖啡要加調料的,例如:牛奶、豆漿、摩卡等,所以設計修改爲:
在這裏插入圖片描述
類太多,而且每個類有自己的cost()方法,一旦牛奶價格上漲,所有與之有關的類就得修改,維護很麻煩。
我們可以利用實例變量與繼承減少類,調料定義爲布爾型實例變量,判斷有無添加調料,代碼如下:

public abstract class Beverage {
    public String des="咖啡";

    public boolean milk;

    public boolean soy;

    public boolean mocha;

    public boolean whip;

    private double cost;

    private double milkCost=5.00;

    private double mochaCost=6.00;

    public String getDes() {
        return des;
    }

    public double cost(){
        if (isMocha()) {
            cost+=mochaCost;
        }else if (isMilk()){
            cost+=milkCost;
        }
        return cost;
     }

    public boolean isMilk() {
        return milk;
    }

    public void setMilk(boolean milk) {
        this.milk = milk;
    }

    public boolean isSoy() {
        return soy;
    }

    public void setSoy(boolean soy) {
        this.soy = soy;
    }

    public boolean isMocha() {
        return mocha;
    }

    public void setMocha(boolean mocha) {
        this.mocha = mocha;
    }

    public boolean isWhip() {
        return whip;
    }

    public void setWhip(boolean whip) {
        this.whip = whip;
    }
}

此時父類的cost()方法不再是抽象的,它需要通過if-else語句判斷是否加了某種調料,從而加上調料費用。
而它的子類,還需要加上本身咖啡的費用,於是最終費用=調料+子類本身費用。只舉其中一個子類飲料的列子,代碼如下:

public class DarkRoast extends Beverage {

    private double darkRoastCost = 1.99;
    public DarkRoast() {
        des = "超優深焙";
    }

    @Override
    public double cost() {
        return darkRoastCost+super.cost();
    }
}

此時我點了杯"超優深焙"的咖啡,並加了牛奶作爲調料,價格爲1.99+5=6.99,運行代碼如下:

public class DecorateTest {
    public static void main(String[] args) {
        DarkRoast darkRoast = new DarkRoast();
        darkRoast.setMilk(true);
        System.out.println(darkRoast.getDes());
        System.out.println(darkRoast.cost());
    }
}

運行結果如預料:
在這裏插入圖片描述
然而,當需求改變,比如調料價格改變、添加新調料、新飲料等,代碼也得隨之改變。我們需要遵循開放-關閉原則:類應該對擴展開放,對修改關閉。使用裝飾者模式就是個好例子。

什麼是裝飾者模式

裝飾者模式動態的將責任附加到對象上。若要擴展功能,裝飾者提供了比繼承更有彈性的方案。類圖如下:
在這裏插入圖片描述
我們的咖啡點餐系統代碼套用此類圖:
在這裏插入圖片描述
抽象組件:飲料基類Beverage的代碼不需要改變原始設計,如下:

public abstract class Beverage {
    public String des="咖啡";
    
    public String getDes() {
        return des;
    }

    public abstract double cost();
}

爲了讓調料抽象裝飾者CondimentDecorator能夠取代飲料Beverage,所以CondimentDecorator擴展自Beverage,使用繼承是爲了有正確的類型,而不是繼承它的行爲。getDes()定義爲抽象方法,由子類重新實現,抽象類代碼如下:

public abstract class CondimentDecorator extends Beverage{
    
    public abstract String getDes();
    
}

具體組件飲料的代碼,使用構造器初始化飲料描述,des實例變量繼承自基類Beverage,並實現cost()方法。代碼如下:

public class DarkRoast extends Beverage {

    public DarkRoast() {
        des = "超優深焙";
    }
    @Override
    public double cost() {
        return 1.99;
    }
}

我們點了摩卡Mocha(具體裝飾者)口味的,具體裝飾者代碼如下:

public class Mocha extends CondimentDecorator {
    //定義一個實例變量記錄被裝飾者
    Beverage beverage;
    //使用構造器來記錄被裝飾者
    public Mocha(Beverage beverage) {
        this.beverage = beverage;
    }
    @Override
    public String getDes() {
        return beverage.getDes() + " Mocha";
    }
    /**
     * 首先把調用委託給被裝飾着計算費用,再加上Mocha的費用
     * @return
     */
    @Override
    public double cost() {
        return beverage.cost() + 0.2;
    }
}

我們還加了豆漿(具體裝飾者),代碼如下:

public class Soy extends CondimentDecorator {
    //定義一個實例變量記錄被裝飾者
    Beverage beverage;
    //使用構造器來記錄被裝飾者
    public Soy(Beverage beverage) {
        this.beverage = beverage;
    }
    @Override
    public String getDes() {
        return beverage.getDes() + " Soy";
    }
    @Override
    public double cost() {
        return beverage.cost() + 0.15;
    }
}

我們點餐完畢,運行程序代碼:

public class StartTest {
    public static void main(String[] args) {
        Beverage darkRoast = new DarkRoast();
        Soy soy = new Soy(darkRoast);
        Mocha mocha = new Mocha(soy);
        System.out.println(mocha.getDes());
        System.out.println(mocha.cost());
    }
}

運行結果如下:
在這裏插入圖片描述

裝飾者模式典型案例:java I/O

和上面咖啡的設計相比,java.io並沒有多大差異。
在這裏插入圖片描述
現在我們應用下裝飾者把輸入流內所有大寫字符轉換成小寫。具體裝飾者代碼如下:

public class LowerCaseInputStream extends FilterInputStream {

    /**
     * Creates a <code>FilterInputStream</code>
     * by assigning the  argument <code>in</code>
     * to the field <code>this.in</code> so as
     * to remember it for later use.
     *
     * @param in the underlying input stream, or <code>null</code> if
     *           this instance is to be created without an underlying stream.
     */
    protected LowerCaseInputStream(InputStream in) {
        super(in);
    }

    @Override
    public int read() throws IOException {
        int c = super.read();
        return (c==-1?c:Character.toLowerCase((char)c));
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        int result = super.read(b, off, len);
        for(int i = off;i<len+result;i++) {
            b[i] = (byte)Character.toLowerCase((char)b[i]);
        }
        return result;
    }
}

運行代碼如下:

public class InputTest {
    public static void main(String[] args) throws IOException {
        int c;
        InputStream lowerCaseInputStream = new LowerCaseInputStream(new BufferedInputStream(new FileInputStream("D:/test.txt")));
        System.out.println(lowerCaseInputStream.read());
        while ((c=lowerCaseInputStream.read())>=0) {
            System.out.print((char)c);
        }
        lowerCaseInputStream.close();
    }
}

D盤的test.txt內容爲:OWeN_JaMES,運行代碼,結果如下:
在這裏插入圖片描述

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