裝飾者模式

聲明 本文內容屬於《Head First 設計模式》閱讀筆記,文中涉及到的知識案例等直接或間接來源於該書。《Head First 設計模式》通過有趣的圖表+文字的形式,讓人自然學習設計模式,非常棒推薦閱讀


裝飾者模式概念

        動態地將責任附加到對象上。若要擴展功能,裝飾者提供了比繼承更有彈性的替代方案

要點說明

序號 說明
裝飾者與被裝飾者需要具有同一個超類(即:這兩者的某個祖先類要相同)。
程序需要的也只是超類,而不具體到哪一個子類,這樣一來該超類的任何子類都能被程序認可。
裝飾者中應持有被裝飾者實例,這樣一來,裝飾者裏面就能調用到被裝飾者的相關方法,進而進行裝飾。
裝飾者中持有被裝飾者時,聲明被裝飾者的類型時應聲明爲超類的類型(而不聲明爲某個具體的被裝飾者類)。這樣一來,(因爲裝飾者、被裝飾者都是這個超類的子類,所以)裝飾者不僅可以裝飾被裝飾者,裝飾者還可以裝飾裝飾者。
裝飾者應重寫(所有裝飾後)會受到影響的方法。
注:重寫的方法來自超類。

圖示(示例)說明

在這裏插入圖片描述


案例(輔助理解)

提示 裝飾者模式使用靈活,下面只是一個簡單的例子,在不同的情境下,可能有不同的表現形式。

情景(需求)介紹

       現有一家星巴茲咖啡館,裏面有各種咖啡(焦炒咖啡、低濃咖啡、濃縮咖啡、首選咖啡)以及各種調味料(牛奶、摩卡、豆漿、奶泡)。現在的需求是:客人在點咖啡時還可以點任意(任意種類、任意數量的)調料

從裝飾者模式出發

  1. 各種咖啡,各種調料的共同超類,爲飲料beverage。
  2. 各種咖啡爲飲料的直接實現,爲被裝飾者。
  3. 各種調料是飲料的附加添加品,爲裝飾者。
  4. 可以將裝飾者的共有邏輯進行抽取,抽取爲一個抽象類。

於是得到以下UML類圖:在這裏插入圖片描述提示:上圖中沒有顯示方法項,裝飾者應重寫裝飾後受影響的方法(重寫的方法來自超類)。

上述裝飾者模式示例中的幾個核心類及代表類

  • AbstractBeverage:抽象超類。

    import lombok.Getter;
    import lombok.Setter;
    
    /**
     * 飲料(的抽象定義)
     *
     * 注: 在java中的裝飾者模式,超類既可採用【抽象類】,亦可採用【接口】。
     *     不過一般都採用 抽象類。
     *
     * @author JustryDeng
     * @date 2019/12/3 11:45
     */
    public abstract class AbstractBeverage {
    
        /** 描述 */
        @Setter
        @Getter
        private String description;
    
        /**
         * 獲取飲料的價格
         *
         * @return  飲料的價格
         */
        public abstract double cost();
    }
    

    注:超類既可採用抽象類,亦可採用接口。不過一般都採用抽象類。

  • AbstractCondimentDecorator:裝飾者抽象類。

    import com.szlaozicl.designpattern.decorator.AbstractBeverage;
    
    /**
     * (裝飾飲料的)調料裝飾者(的抽象定義)
     *
     * @author JustryDeng
     * @date 2019/12/3 11:50
     */
    @SuppressWarnings("all")
    public abstract class AbstractCondimentDecorator extends AbstractBeverage {
    
        /** 被裝飾者 */
        protected AbstractBeverage decoratedObj;
    
        /**
         * 構造器
         *
         * @param decoratedObj
         *            被裝飾者
         * @date 2019/12/4 12:30
         */
        protected AbstractCondimentDecorator(AbstractBeverage decoratedObj) {
            this.decoratedObj = decoratedObj;
        }
    
        /**
         * 裝飾者應重寫 飲料的描述
         * 注: 之所以需要重寫飲料的描述, 是因爲原來的描述沒有對調料的介紹。
         * 特別注意: 裝飾者 應重寫 裝飾前後相關的方法。 如,這裏的描述、價格。
         *
         * @return 飲料的描述
         */
        @Override
        public abstract String getDescription();
    }
    

    注:對於所有裝飾者共有的特徵,可進行抽取,抽取爲抽象類或非抽象類均可。

  • Espresso:被裝飾者代表類。

    import com.szlaozicl.designpattern.decorator.AbstractBeverage;
    
    /**
     * 具體飲料 - 濃縮咖啡
     *
     * @author JustryDeng
     * @date 2019/12/3 12:03
     */
    public class Espresso extends AbstractBeverage {
    
        /** 無參構造 */
        public Espresso() {
            // 當前飲料的描述
            setDescription("濃縮咖啡Espresso");
        }
    
        @Override
        public double cost() {
            return 1.99;
        }
    }
    
  • MilkCondimentDecorator:裝飾者代表類。

    import com.szlaozicl.designpattern.decorator.AbstractBeverage;
    
    /**
     * 具體的調料(裝飾者) - 牛奶Milk
     *
     * @author JustryDeng
     * @date 2019/12/3 12:33
     */
    public class MilkCondimentDecorator extends AbstractCondimentDecorator {
    
        /** 構造 */
        public MilkCondimentDecorator(AbstractBeverage decoratedObj) {
            super(decoratedObj);
        }
    
        @Override
        public String getDescription() {
            // 被裝飾者 原來的description
            String oldDescription = decoratedObj.getDescription();
            // 對原來的description進行裝飾
            return oldDescription + " + 牛奶Milk";
        }
    
        @Override
        public double cost() {
            // 被裝飾者 原來的 價格
            double oldCost = decoratedObj.cost();
            // 對原來的價格進行裝飾
            return oldCost + 0.1;
        }
    }
    

測試一下

  • 測試類:

    import com.szlaozicl.designpattern.decorator.beverage.DarkRoast;
    import com.szlaozicl.designpattern.decorator.beverage.Decat;
    import com.szlaozicl.designpattern.decorator.beverage.Espresso;
    import com.szlaozicl.designpattern.decorator.decorator.MilkCondimentDecorator;
    import com.szlaozicl.designpattern.decorator.decorator.MochaCondimentDecorator;
    import com.szlaozicl.designpattern.decorator.decorator.SoyCondimentDecorator;
    import com.szlaozicl.designpattern.decorator.decorator.WhipCondimentDecorator;
    
    /**
     * 裝飾者模式 --- 測試
     *
     * @author JustryDeng
     * @date 2019/12/3 12:54
     */
    public class Test {
    
        /** 函數入口 */
        public static void main(String[] args) {
            System.out.println(" ------------- 測試一");
            testOne();
    
            System.out.println("\n ------------- 測試二");
            testTwo();
    
            System.out.println("\n ------------- 測試三");
            testThree();
        }
    
        /** 測試一 */
        private static void testOne() {
            AbstractBeverage beverage = new Espresso();
            System.err.println("裝飾前:");
            System.out.println("描述:" + beverage.getDescription());
            System.out.println("價格:" + beverage.cost());
    
            // 給Espresso裝飾 牛奶Milk
            beverage = new MilkCondimentDecorator(beverage);
            System.err.println("裝飾後:");
            System.out.println("描述:" + beverage.getDescription());
            System.out.println("價格:" + beverage.cost());
        }
    
        /** 測試二 */
        private static void testTwo() {
            AbstractBeverage beverage = new DarkRoast();
            System.err.println("裝飾前:");
            System.out.println("描述:" + beverage.getDescription());
            System.out.println("價格:" + beverage.cost());
    
            // 給DarkRoast裝飾 牛奶Milk
            beverage = new MilkCondimentDecorator(beverage);
            /*
             * 繼續裝飾 豆漿Soy
             *
             * 注:雖然直接裝飾的對象是MilkCondimentDecorator,但是由
             *    於MilkCondimentDecorator其實是對DarkRoast的裝飾,
             *    所以實際上裝飾的還是DarkRoast
             */
            beverage = new SoyCondimentDecorator(beverage);
            System.err.println("裝飾後:");
            System.out.println("描述:" + beverage.getDescription());
            System.out.println("價格:" + beverage.cost());
        }
    
        /** 測試三 */
        private static void testThree() {
            AbstractBeverage beverage = new Decat();
            System.err.println("裝飾前:");
            System.out.println("描述:" + beverage.getDescription());
            System.out.println("價格:" + beverage.cost());
    
            // 給DarkRoast裝飾 奶泡Whip
            beverage = new WhipCondimentDecorator(beverage);
            // 繼續裝飾 摩卡Mocha
            beverage = new MochaCondimentDecorator(beverage);
            // 繼續裝飾 奶泡Whip
            beverage = new WhipCondimentDecorator(beverage);
            System.err.println("裝飾後:");
            System.out.println("描述:" + beverage.getDescription());
            System.out.println("價格:" + beverage.cost());
        }
    }
    

運行測試類main方法,控制檯輸出:
在這裏插入圖片描述


拓展 - 自定義InputStream子類的裝飾者

        java.io包下大量運用了裝飾者模式(或在裝飾者模式基礎上進行了變形),如InputStream、OutputStream、Reader、Writer等。

  • 以InputStream爲例,其UML類圖部分如圖:
    在這裏插入圖片描述

  • 自定義一個大寫轉換爲小寫的裝飾者,並編寫測試類:

    import java.io.ByteArrayInputStream;
    import java.io.FilterInputStream;
    import java.io.IOException;
    import java.io.InputStream;
    
    /**
     * 對java.io包下的I/O進行自定義裝飾者 測試
     * <p>
     * 說明: java.io包下的I/O就大量用到了 裝飾者模式,
     * 這裏自己實現一個 InputStream的裝飾者 以作練習。
     *
     * @author JustryDeng
     * @date 2019/12/3 12:54
     */
    @SuppressWarnings("all")
    public class IOTest {
    
        /** 函數入口 */
        public static void main(String[] args) throws IOException {
            InputStream in = new LowerCaseInputStream(new ByteArrayInputStream("ABCDEFG".getBytes()));
            int c;
            while ((c = in.read()) > 0) {
                // 輸出結果爲: abcdefg
                System.out.print((char) c);
            }
        }
    
    }
    
    /**
     * 自定義InputStream子類的裝飾者
     *
     * @author JustryDeng
     * @date 2019/12/3 13:17
     */
    @SuppressWarnings("all")
    class LowerCaseInputStream extends FilterInputStream {
    
        public LowerCaseInputStream(InputStream in) {
            super(in);
        }
    
        /**
         * 從此輸入流中讀取下一個數據字節。返回一個 0 到 255 範圍內的 int 字節值。
         * 如果因爲已經到達流末尾而沒有字節可用,則返回 -1。
         */
        @Override
        public int read() throws IOException {
            int c = super.read();
            if (c == -1) {
                return c;
            }
            return Character.toLowerCase((char) c);
        }
    
    
        /**
         * 從此輸入流中將 len 個字節的數據讀入到這個 給定的byte數組中。
         */
        @Override
        public int read(byte[] b, int offset, int len) throws IOException {
            int result = super.read(b, offset, len);
            // 注: len爲允許一次讀取的最大字節數, 而result爲該次實際讀取的字節數, 所以得以result爲準
            // 注: 數據結果存儲在b中,只需要將b裏面的大寫字母轉換爲小寫即可
            for (int i = offset; i < offset + result; i++) {
                b[i] = (byte) Character.toLowerCase((char) b[i]);
            }
            return result;
        }
    }
    
  • 控制檯輸出:
    在這裏插入圖片描述


提示裝飾者模式可結合工廠模式/建造者模式進行優化。

裝飾者模式學習完畢 !


^_^ 如有不當之處,歡迎指正

^_^ 參考資料
        《Head First 設計模式》
Eric Freeman & Elisabeth Freeman with Kathy Sierra & Bert Bates著,O’Reilly Taiwan公司譯,UMLChina改編

^_^ 測試代碼託管鏈接
         https://github.com/JustryDeng…DesignPattern

^_^ 本文已經被收錄進《程序員成長筆記(六)》,筆者JustryDeng

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