java設計模式--裝飾器模式

轉載 http://sishuok.com/forum/blogPost/list/5766.html

22.1 場景問題

22.1.1 複雜的獎金計算

   考慮這樣一個實際應用:就是如何實現靈活的獎金計算。

獎金計算是相對複雜的功能,尤其是對於業務部門的獎金計算方式,是非常複雜的,除了業務功能複雜外,另外一個麻煩之處是計算方式還經常需要變動,因爲業務部門經常通過調整獎金的計算方式來激勵士氣。

   先從業務上看看現有的獎金計算方式的複雜性:

首先是獎金分類:對於個人,大致有個人當月業務獎金、個人累計獎金、個人業務增長獎金、及時回款獎金、限時成交加碼獎金等等;
對於業務主管或者是業務經理,除了個人獎金外,還有:團隊累計獎金、團隊業務增長獎金、團隊盈利獎金等等。
其次是計算獎金的金額,又有這麼幾個基數:銷售額、銷售毛利、實際回款、業務成本、獎金基數等等;
另外一個就是計算的公式,針對不同的人、不同的獎金類別、不同的計算獎金的金額,計算的公式是不同的,就算是同一個公式,裏面計算的比例參數也有可能是不同的。

22.1.2 簡化後的獎金計算體系

看了上面獎金計算的問題,所幸我們只是來學習設計模式,並不是真的要去實現整個獎金計算體系的業務,因此也沒有必要把所有的計算業務都羅列在這裏,爲了後面演示的需要,簡化一下,演示用的獎金計算體系如下:

每個人當月業務獎金 = 當月銷售額 X  3%
每個人累計獎金 = 總的回款額 X  0.1%
團隊獎金 = 團隊總銷售額 X 1%

22.1.3 不用模式的解決方案

一個人的獎金分成很多個部分,要實現獎金計算,主要就是要按照各個獎金計算的規則,把這個人可以獲取的每部分獎金計算出來,然後計算一個總和,這就是這個人可以得到的獎金。

(1)爲了演示,先準備點測試數據,在內存中模擬數據庫,示例代碼如下:

/**

 * 在內存中模擬數據庫,準備點測試數據,好計算獎金

 */

public class TempDB {

    private TempDB(){

}

    /**

     * 記錄每個人的月度銷售額,只用了人員,月份沒有用

     */

    public static Map<String,Double> mapMonthSaleMoney =

new HashMap<String,Double>();

    static{

       //填充測試數據

       mapMonthSaleMoney.put("張三",10000.0);

       mapMonthSaleMoney.put("李四",20000.0);

       mapMonthSaleMoney.put("王五",30000.0);

    }

}

(2)按照獎金計算的規則,實現獎金計算,示例代碼如下:

/**

 * 計算獎金的對象

 */

public class Prize {

    /**

     * 計算某人在某段時間內的獎金,有些參數在演示中並不會使用,

     * 但是在實際業務實現上是會用的,爲了表示這是個具體的業務方法,

     * 因此這些參數被保留了

     * @param user 被計算獎金的人員

     * @param begin 計算獎金的開始時間

     * @param end 計算獎金的結束時間

     * @return 某人在某段時間內的獎金

     */

    public  double calcPrize(String user,Date begin,Date end){

       double prize = 0.0; 

       //計算當月業務獎金,所有人都會計算

       prize = this.monthPrize(user, begin, end);

       //計算累計獎金

       prize += this.sumPrize(user, begin, end);



       //需要判斷該人員是普通人員還是業務經理,團隊獎金只有業務經理纔有

       if(this.isManager(user)){

           prize += this.groupPrize(user, begin, end);

       }

       return prize;

    }



    /**

     * 計算某人的當月業務獎金,參數重複,就不再註釋了

     */

    private double monthPrize(String user, Date begin, Date end) {

       //計算當月業務獎金,按照人員去獲取當月的業務額,然後再乘以3%

       double prize = TempDB.mapMonthSaleMoney.get(user) * 0.03;

       System.out.println(user+"當月業務獎金"+prize);

       return prize;

    }



    /**

     * 計算某人的累計獎金,參數重複,就不再註釋了

     */

    public double sumPrize(String user, Date begin, Date end) {

       //計算累計獎金,其實應該按照人員去獲取累計的業務額,然後再乘以0.1%

       //簡單演示一下,假定大家的累計業務額都是1000000元

       double prize = 1000000 * 0.001;

       System.out.println(user+"累計獎金"+prize);

       return prize;

    }  



    /**

     * 判斷人員是普通人員還是業務經理

     * @param user 被判斷的人員

     * @return true表示是業務經理,false表示是普通人員

     */

    private boolean isManager(String user){

       //應該從數據庫中獲取人員對應的職務

       //爲了演示,簡單點判斷,只有王五是經理

       if("王五".equals(user)){

           return true;        

       }

       return false;

    }

    /**

     * 計算當月團隊業務獎,參數重複,就不再註釋了

     */

    public double groupPrize(String user, Date begin, Date end) {

       //計算當月團隊業務獎金,先計算出團隊總的業務額,然後再乘以1%,

//假設都是一個團隊的

       double group = 0.0;

       for(double d : TempDB.mapMonthSaleMoney.values()){

           group += d;

       }

       double prize = group * 0.01;

       System.out.println(user+"當月團隊業務獎金"+prize);

       return prize;

    }

}

(3)寫個客戶端來測試一下,看看是否能正確地計算獎金,示例代碼如下:

public class Client {

    public static void main(String[] args) {

       //先創建計算獎金的對象

       Prize p = new Prize();



       //日期對象都沒有用上,所以傳null就可以了

       double zs = p.calcPrize("張三",null,null);   

       System.out.println("==========張三應得獎金:"+zs);

       double ls = p.calcPrize("李四",null,null);

       System.out.println("==========李四應得獎金:"+ls);    

       double ww = p.calcPrize("王五",null,null);

       System.out.println("==========王經理應得獎金:"+ww);

    }

}

22.1.4 有何問題

看了上面的實現,挺簡單的嘛,就是計算方式麻煩點,每個規則都要實現。真的很簡單嗎?仔細想想,有沒有什麼問題?

對於獎金計算,光是計算方式複雜,也就罷了,不過是實現起來會困難點,相對而言還是比較好解決的,不過是用程序把已有的算法表達出來。

最痛苦的是,這些獎金的計算方式,經常發生變動,幾乎是每個季度都會有小調整,每年都有大調整,這就要求軟件的實現要足夠靈活,要能夠很快進行相應調整和修改,否則就不能滿足實際業務的需要。

   舉個簡單的例子來說,現在根據業務需要,需要增加一個“環比增長獎金”,就是本月的銷售額比上個月有增加,而且要達到一定的比例,當然增長比例越高,獎金比例越大。那麼軟件就必須要重新實現這麼個功能,並正確的添加到系統中去。過了兩個月,業務獎勵的策略發生了變化,不再需要這個獎金了,或者是另外換了一個新的獎金方式了,那麼軟件就需要把這個功能從軟件中去掉,然後再實現新的功能。

   那麼上面的要求該如何實現呢?

   很明顯,一種方案是通過繼承來擴展功能;另外一種方案就是到計算獎金的對象裏面,添加或者刪除新的功能,並在計算獎金的時候,調用新的功能或是不調用某些去掉的功能,這種方案會嚴重違反開-閉原則。

   還有一個問題,就是在運行期間,不同人員參與的獎金計算方式也是不同的,舉例來說:如果是業務經理,除了參與個人計算部分外,還要參加團隊獎金的計算,這就意味着需要在運行期間動態來組合需要計算的部分,也就是會有一堆的if-else。

   總結一下,獎金計算面臨如下問題:

(1)計算邏輯複雜
(2)要有足夠靈活性,可以方便的增加或者減少功能
(3)要能動態的組合計算方式,不同的人蔘與的計算不同

上面描述的獎金計算的問題,絕對沒有任何誇大成分,相反已經簡化不少了,還有更多麻煩沒有寫上來,畢竟我們的重點在設計模式,而不是業務。

把上面的問題抽象一下,設若有一個計算獎金的對象,現在需要能夠靈活的給它增加和減少功能,還需要能夠動態的組合功能,每個功能就相當於在計算獎金的某個部分。

   現在的問題就是:如何才能夠透明的給一個對象增加功能,並實現功能的動態組合呢?

22.2 解決方案

22.2.1 裝飾模式來解決

用來解決上述問題的一個合理的解決方案,就是使用裝飾模式。那麼什麼是裝飾模式呢?

(1)裝飾模式定義

這裏寫圖片描述
(2)應用裝飾模式來解決的思路

雖然經過簡化,業務簡單了很多,但是需要解決的問題不會少,還是要解決:要透明的給一個對象增加功能,並實現功能的動態組合。

所謂透明的給一個對象增加功能,換句話說就是要給一個對象增加功能,但是不能讓這個對象知道,也就是不能去改動這個對象。而實現了能夠給一個對象透明的增加功能,自然就能夠實現功能的動態組合,比如原來的對象有A功能,現在透明的給它增加了一個B功能,是不是就相當於動態組合了A和B功能呢。

要想實現透明的給一個對象增加功能,也就是要擴展對象的功能了,使用繼承啊,有人馬上提出了一個方案,但很快就被否決了,那要減少或者修改功能呢?事實上繼承是非常不靈活的複用方式。那就用“對象組合”嘛,又有人提出新的方案來了,這個方案得到了大家的贊同。

在裝飾模式的實現中,爲了能夠和原來使用被裝飾對象的代碼實現無縫結合,是通過定義一個抽象類,讓這個類實現與被裝飾對象相同的接口,然後在具體實現類裏面,轉調被裝飾的對象,在轉調的前後添加新的功能,這就實現了給被裝飾對象增加功能,這個思路跟“對象組合”非常類似。如果對“對象組合”不熟悉,請參見22.3.1的第2小節。

在轉調的時候,如果覺得被裝飾的對象的功能不再需要了,還可以直接替換掉,也就是不再轉調,而是在裝飾對象裏面完全全新的實現。

22.2.2 模式結構和說明

   裝飾模式的結構如圖22.1所示:

這裏寫圖片描述

Component:

   組件對象的接口,可以給這些對象動態的添加職責。

ConcreteComponent:

   具體的組件對象,實現組件對象接口,通常就是被裝飾器裝飾的原始對象,也就是可以給這個對象添加職責。

Decorator:

   所有裝飾器的抽象父類,需要定義一個與組件接口一致的接口,並持有一個Component對象,其實就是持有一個被裝飾的對象。

注意這個被裝飾的對象不一定是最原始的那個對象了,也可能是被其它裝飾器裝飾過後的對象,反正都是實現的同一個接口,也就是同一類型。

ConcreteDecorator:

   實際的裝飾器對象,實現具體要向被裝飾對象添加的功能。

22.2.3 裝飾模式示例代碼

(1)先來看看組件對象的接口定義,示例代碼如下:

/**

 * 組件對象的接口,可以給這些對象動態的添加職責

 */

public abstract class Component {

    /**

     * 示例方法

     */

    public abstract void operation();

}

(2)定義了接口,那就看看具體組件實現對象示意吧,示例代碼如下:

/**

 * 具體實現組件對象接口的對象

 */

public class ConcreteComponent extends Component {

    public void operation() {

       //相應的功能處理

    }

}

3)接下來看看抽象的裝飾器對象,示例代碼如下:

/**

 * 裝飾器接口,維持一個指向組件對象的接口對象,並定義一個與組件接口一致的接口

 */

public abstract  class Decorator extends Component {

    /**

     * 持有組件對象

     */

    protected Component component;

    /**

     * 構造方法,傳入組件對象

     * @param component 組件對象

     */

    public Decorator(Component component) {

       this.component = component;

    }

    public void operation() {

       //轉發請求給組件對象,可以在轉發前後執行一些附加動作

       component.operation();

    }

}

(4)該來看看具體的裝飾器實現對象了,這裏有兩個示意對象,一個示意了添加狀態,一個示意了添加職責。先看添加了狀態的示意對象吧,示例代碼如下:

/**

 * 裝飾器的具體實現對象,向組件對象添加狀態

 */

public class ConcreteDecoratorA extends Decorator {

    public ConcreteDecoratorA(Component component) {

       super(component);

    }

    /**

     * 添加的狀態

     */

    private String addedState; 

    public String getAddedState() {

       return addedState;

    }

    public void setAddedState(String addedState) {

       this.addedState = addedState;

    }

    public void operation() {

       //調用父類的方法,可以在調用前後執行一些附加動作

        //在這裏進行處理的時候,可以使用添加的狀態

       super.operation();

    }

}

接下來看看添加職責的示意對象,示例代碼如下:

/**

 * 裝飾器的具體實現對象,向組件對象添加職責

 */

public class ConcreteDecoratorB extends Decorator {

    public ConcreteDecoratorB(Component component) {

       super(component);

    }

    /**

     * 需要添加的職責

     */

    private void addedBehavior() {

       //需要添加的職責實現

    }

    public void operation() {

       //調用父類的方法,可以在調用前後執行一些附加動作

       super.operation();

       addedBehavior();

    }

}

22.2.4 使用裝飾模式重寫示例

   看完了裝飾模式的基本知識,該來考慮如何使用裝飾模式重寫前面的示例了。要使用裝飾模式來重寫前面的示例,大致會有如下改變:

首先需要定義一個組件對象的接口,在這個接口裏面定義計算獎金的業務方法,因爲外部就是使用這個接口來操作裝飾模式構成的對象結構中的對象
需要添加一個基本的實現組件接口的對象,可以讓它返回獎金爲0就可以了
把各個計算獎金的規則當作裝飾器對象,需要爲它們定義一個統一的抽象的裝飾器對象,好約束各個具體的裝飾器的接口
把各個計算獎金的規則實現成爲具體的裝飾器對象

先看看現在示例的整體結構,好整體理解和把握示例,如圖22.2所示:

這裏寫圖片描述

(1)計算獎金的組件接口和基本的實現對象

   在計算獎金的組件接口中,需要定義原本的業務方法,也就是實現獎金計算的方法,示例代碼如下:
/**

 * 計算獎金的組件接口

 */

public abstract class Component {

    /**

     * 計算某人在某段時間內的獎金,有些參數在演示中並不會使用,

     * 但是在實際業務實現上是會用的,爲了表示這是個具體的業務方法,

     * 因此這些參數被保留了

     * @param user 被計算獎金的人員

     * @param begin 計算獎金的開始時間

     * @param end 計算獎金的結束時間

     * @return 某人在某段時間內的獎金

     */

    public abstract double calcPrize(String user

,Date begin,Date end);

}

爲這個業務接口提供一個基本的實現,示例代碼如下:

/**

 * 基本的實現計算獎金的類,也是被裝飾器裝飾的對象

 */

public class ConcreteComponent extends Component{

    public double calcPrize(String user, Date begin, Date end) {

       //只是一個默認的實現,默認沒有獎金

       return 0;

    }

}

(2)定義抽象的裝飾器

在進一步定義裝飾器之前,先定義出各個裝飾器公共的父類,在這裏定義所有裝飾器對象需要實現的方法。這個父類應該實現組件的接口,這樣才能保證裝飾後的對象仍然可以繼續被裝飾。示例代碼如下:

/**

 * 裝飾器的接口,需要跟被裝飾的對象實現同樣的接口

 */

public abstract class Decorator extends Component{

    /**

     * 持有被裝飾的組件對象

     */

    protected Component c;

    /**

     * 通過構造方法傳入被裝飾的對象

     * @param c被裝飾的對象

     */

    public Decorator(Component c){

       this.c = c;

    }

    public double calcPrize(String user, Date begin, Date end) {

       //轉調組件對象的方法

       return c.calcPrize(user, begin, end);

    }

}

(3)定義一系列的裝飾器對象

   用一個具體的裝飾器對象,來實現一條計算獎金的規則,現在有三條計算獎金的規則,那就對應有三個裝飾器對象來實現,依次來看看它們的實現。

   這些裝飾器涉及到的TempDB跟以前一樣,這裏就不去贅述了。

首先來看實現計算當月業務獎金的裝飾器,示例代碼如下:

/**

 * 裝飾器對象,計算當月業務獎金

 */

public class MonthPrizeDecorator extends Decorator{

    public MonthPrizeDecorator(Component c){

       super(c);

    }  

    public double calcPrize(String user, Date begin, Date end) {

       //1:先獲取前面運算出來的獎金

       double money = super.calcPrize(user, begin, end);

       //2:然後計算當月業務獎金,按人員和時間去獲取當月業務額,然後再乘以3%

       double prize = TempDB.mapMonthSaleMoney.get(user) * 0.03;

       System.out.println(user+"當月業務獎金"+prize);

       return money + prize;

    }

}

接下來看實現計算累計獎金的裝飾器,示例代碼如下:

/**

 * 裝飾器對象,計算累計獎金

 */

public class SumPrizeDecorator extends Decorator{

    public SumPrizeDecorator(Component c){

       super(c);

    }  

    public double calcPrize(String user, Date begin, Date end) {

       //1:先獲取前面運算出來的獎金

       double money = super.calcPrize(user, begin, end);

       //2:然後計算累計獎金,其實應按人員去獲取累計的業務額,然後再乘以0.1%

       //簡單演示一下,假定大家的累計業務額都是1000000元

       double prize = 1000000 * 0.001;

       System.out.println(user+"累計獎金"+prize);

       return money + prize;

    }

}

接下來看實現計算當月團隊業務獎金的裝飾器,示例代碼如下:

/**

 * 裝飾器對象,計算當月團隊業務獎金

 */

public class GroupPrizeDecorator extends Decorator{

    public GroupPrizeDecorator(Component c){

       super(c);

    }

    public double calcPrize(String user, Date begin, Date end) {

       //1:先獲取前面運算出來的獎金

       double money = super.calcPrize(user, begin, end);

       //2:然後計算當月團隊業務獎金,先計算出團隊總的業務額,然後再乘以1%

       //假設都是一個團隊的

       double group = 0.0;

       for(double d : TempDB.mapMonthSaleMoney.values()){

           group += d;

       }

       double prize = group * 0.01;

       System.out.println(user+"當月團隊業務獎金"+prize);

       return money + prize;

    }

}

(4)使用裝飾器的客戶端

   使用裝飾器的客戶端,首先需要創建被裝飾的對象,然後創建需要的裝飾對象,接下來重要的工作就是組合裝飾對象,依次對前面的對象進行裝飾。

   有很多類似的例子,比如生活中的裝修,就拿裝飾牆壁來說吧:沒有裝飾前是原始的磚牆,這就好比是被裝飾的對象,首先需要刷膩子,把牆找平,這就好比對原始的磚牆進行了一次裝飾,而刷的膩子就好比是一個裝飾器對象;好了,裝飾一回了,接下來該刷牆面漆了,這又好比裝飾了一回,刷的牆面漆就好比是又一個裝飾器對象,而且這回被裝飾的對象不是原始的磚牆了,而是被膩子裝飾器裝飾過後的牆面,也就是說後面的裝飾器是在前面的裝飾器裝飾過後的基礎之上,繼續裝飾的,類似於一層一層疊加的功能。

   同樣的道理,計算獎金也是這樣,先創建基本的獎金對象,然後組合需要計算的獎金類型,依次組合計算,最後的結果就是總的獎金。示例代碼如下:
/**

 * 使用裝飾模式的客戶端

 */

public class Client {

    public static void main(String[] args) {

       //先創建計算基本獎金的類,這也是被裝飾的對象

       Component c1 = new ConcreteComponent();



       //然後對計算的基本獎金進行裝飾,這裏要組合各個裝飾

       //說明,各個裝飾者之間最好是不要有先後順序的限制,

//也就是先裝飾誰和後裝飾誰都應該是一樣的



       //先組合普通業務人員的獎金計算

       Decorator d1 = new MonthPrizeDecorator(c1);

       Decorator d2 = new SumPrizeDecorator(d1);



       //注意:這裏只需使用最後組合好的對象調用業務方法即可,會依次調用回去

       //日期對象都沒有用上,所以傳null就可以了

       double zs = d2.calcPrize("張三",null,null);      

       System.out.println("==========張三應得獎金:"+zs);

       double ls = d2.calcPrize("李四",null,null);

       System.out.println("==========李四應得獎金:"+ls);



       //如果是業務經理,還需要一個計算團隊的獎金計算

       Decorator d3 = new GroupPrizeDecorator(d2);

       double ww = d3.calcPrize("王五",null,null);

       System.out.println("==========王經理應得獎金:"+ww);

    }

}

當測試運行的時候會按照裝飾器的組合順序,依次調用相應的裝飾器來執行業務功能,是一個遞歸的調用方法,以業務經理“王五”的獎金計算做例子,畫個圖來說明獎金的計算過程吧,看看是如何調用的,如圖22.3所示:

這裏寫圖片描述
這個圖很好的揭示了裝飾模式的組合和調用過程,請仔細體會一下。

如同上面的示例,對於基本的計算獎金的對象而言,由於計算獎金的邏輯太過於複雜,而且需要在不同的情況下進行不同的運算,爲了靈活性,把多種計算獎金的方式分散到不同的裝飾器對象裏面,採用動態組合的方式,來給基本的計算獎金的對象增添計算獎金的功能,每個裝飾器相當於計算獎金的一個部分。

   這種方式明顯比爲基本的計算獎金的對象增加子類來得更靈活,因爲裝飾模式的起源點是採用對象組合的方式,然後在組合的時候順便增加些功能。爲了達到一層一層組裝的效果,裝飾模式還要求裝飾器要實現與被裝飾對象相同的業務接口,這樣才能以同一種方式依次組合下去。

   靈活性還體現在動態上,如果是繼承的方式,那麼所有的類實例都有這個功能了,而採用裝飾模式,可以動態的爲某幾個對象實例添加功能,而不是對整個類添加功能。比如上面示例中,客戶端測試的時候,對張三李四就只是組合了兩個功能,對王五就組合了三個功能,但是原始的計算獎金的類都是一樣的,只是動態的爲它增加的功能不同而已。

22.3 模式講解

22.3.1 認識裝飾模式

(1)模式功能

裝飾模式能夠實現動態的爲對象添加功能,是從一個對象外部來給對象增加功能,相當於是改變了對象的外觀。當裝飾過後,從外部使用系統的角度看,就不再是使用原始的那個對象了,而是使用被一系列的裝飾器裝飾過後的對象。

這樣就能夠靈活的改變一個對象的功能,只要動態組合的裝飾器發生了改變,那麼最終所得到的對象的功能也就發生了改變。

變相的還得到了另外一個好處,那就是裝飾器功能的複用,可以給一個對象多次增加同一個裝飾器,也可以用同一個裝飾器裝飾不同的對象。

(2)對象組合

前面已經講到了,一個類的功能的擴展方式,可以是繼承,也可以是功能更強大、更靈活的對象組合的方式。

其實,現在在面向對象設計中,有一條很基本的規則就是“儘量使用對象組合,而不是對象繼承”來擴展和複用功能。裝飾模式的思考起點就是這個規則,可能有些朋友還不太熟悉什麼是“對象組合”,下面介紹一下“對象組合”。

什麼是對象組合

直接舉例來說吧,假若有一個對象A,實現了一個a1的方法,而C1對象想要來擴展A的功能,給它增加一個c11的方法,那麼一個方案是繼承,A對象示例代碼如下:

public class A {

    public void a1(){

       System.out.println("now in A.a1");

    }

}

C1對象示例代碼如下

public class C1 extends A{

    public void c11(){

       System.out.println("now in C1.c11");

    }

}

另外一個方案就是使用對象組合,怎麼組合呢?就是在C1對象裏面不再繼承A對象了,而是去組合使用A對象的實例,通過轉調A對象的功能來實現A對象已有的功能,寫個新的對象C2來示範,示例代碼如下:

public class C2 {

    /**

     * 創建A對象的實例

     */

    private A a = new A();





    public void a1(){

       //轉調A對象的功能

       a.a1();

    }

    public void c11(){

       System.out.println("now in C2.c11");

    }

}

對象組合是不是也很簡單,而且更靈活了:

首先可以有選擇的複用功能,不是所有A的功能都會被複用,在C2中少調用幾個A定義的功能就可以了;
其次在轉調前後,可以實現一些功能處理,而且對於A對象是透明的,也就是A對象並不知道在a1方法處理的時候被追加了功能;
還有一個額外的好處,就是可以組合擁有多個對象的功能,假如還有一個對象B,而C2也想擁有B對象的功能,那很簡單,再增加一個方法,然後轉調B對象就好了,B對象示例如下:
public class B {

    public void b1(){

       System.out.println("now in B.b1");

    }

}

同時擁有A對象功能,B對象的功能,還有自己實現的功能的C3對象示例代碼如下:

public class C3 {

    private A a = new A();

    private B b = new B();



    public void a1(){

       //轉調A對象的功能

       a.a1();

    }

    public void b1(){

       //轉調B對象的功能

       b.b1();

    }

    public void c11(){

       System.out.println("now in C3.c11");

    }

}
最後再說一點,就是關於對象組合中,何時創建被組合對象的實例:

一種方案是在屬性上直接定義並創建需要組合的對象實例
另外一種方案是在屬性上定義一個變量,來表示持有被組合對象的實例,具體實例從外部傳入,也可以通過IoC/DI容器來注入

示例如下:

public class C4 {

    //示例直接在屬性上創建需要組合的對象

    private A a = new A();



    //示例通過外部傳入需要組合的對象

    private B b = null;

    public void setB(B b){

       this.b = b;

    }

    public void a1(){

       //轉調A對象的功能

       a.a1();

    }

    public void b1(){

       //轉調B對象的功能

       b.b1();

    }

    public void c11(){

       System.out.println("now in C4.c11");

    }

}

(3)裝飾器

   裝飾器實現了對被裝飾對象的某些裝飾功能,可以在裝飾器裏面調用被裝飾對象的功能,獲取相應的值,這其實是一種遞歸調用。

在裝飾器裏不僅僅是可以給被裝飾對象增加功能,還可以根據需要選擇是否調用被裝飾對象的功能,如果不調用被裝飾對象的功能,那就變成完全重新實現了,相當於動態修改了被裝飾對象的功能。

   另外一點,各個裝飾器之間最好是完全獨立的功能,不要有依賴,這樣在進行裝飾組合的時候,纔沒有先後順序的限制,也就是先裝飾誰和後裝飾誰都應該是一樣的,否則會大大降低裝飾器組合的靈活性。

(4)裝飾器和組件類的關係

   裝飾器是用來裝飾組件的,裝飾器一定要實現和組件類一致的接口,保證它們是同一個類型,並具有同一個外觀,這樣組合完成的裝飾才能夠遞歸的調用下去。

組件類是不知道裝飾器的存在的,裝飾器給組件添加功能是一種透明的包裝,組件類毫不知情。需要改變的是外部使用組件類的地方,現在需要使用包裝後的類,接口是一樣的,但是具體的實現類發生了改變。

(5)退化形式

如果僅僅只是想要添加一個功能,就沒有必要再設計裝飾器的抽象類了,直接在裝飾器裏面實現跟組件一樣的接口,然後實現相應的裝飾功能就可以了。但是建議最好還是設計上裝飾器的抽象類,這樣有利於程序的擴展。

22.3.2 Java中的裝飾模式應用

1:Java中典型的裝飾模式應用——I/O流

裝飾模式在Java中最典型的應用,就是I/O流,簡單回憶一下,如果使用流式操作讀取文件內容,會怎麼實現呢,簡單的代碼示例如下:

public class IOTest {

    public static void main(String[] args)throws Exception  {

       //流式讀取文件

       DataInputStream din = null;

       try{

           din = new DataInputStream(

              new BufferedInputStream(

                     new FileInputStream("IOTest.txt")

              )

           );

           //然後就可以獲取文件內容了

           byte bs []= new byte[din.available()];

           din.read(bs);

           String content = new String(bs);

           System.out.println("文件內容===="+content);

       }finally{

           din.close();

       }     

    }

}

仔細觀察上面的代碼,會發現最裏層是一個FileInputStream對象,然後把它傳遞給一個BufferedInputStream對象,經過BufferedInputStream處理過後,再把處理過後的對象傳遞給了DataInputStream對象進行處理,這個過程其實就是裝飾器的組裝過程,FileInputStream對象相當於原始的被裝飾的對象,而BufferedInputStream對象和DataInputStream對象則相當於裝飾器。

   可能有朋友會問,裝飾器和具體的組件類是要實現同樣的接口的,上面這些類是這樣嗎?看看Java的I/O對象層次圖吧,由於Java的I/O對象衆多,因此只是畫出了InputStream的部分,而且由於圖的大小關係,也只是表現出了部分的流,具體如圖22.4所示:

這裏寫圖片描述

查看上圖會發現,它的結構和裝飾模式的結構幾乎是一樣的:

InputStream就相當於裝飾模式中的Component。
其實FileInputStream、ObjectInputStream、StringBufferInputStream這幾個對象是直接繼承了InputSream,還有幾個直接繼承InputStream的對象,比如:ByteArrayInputStream、PipedInputStream等。這些對象相當於裝飾模式中的ConcreteComponent,是可以被裝飾器裝飾的對象。
那麼FilterInputStream就相當於裝飾模式中的Decorator,而它的子類DataInputStream、BufferedInputStream、LineNumberInputStream和PushbackInputStream就相當於裝飾模式中的ConcreteDecorator了。另外FilterInputStream和它的子類對象的構造器,都是傳入組件InputStream類型,這樣就完全符合前面講述的裝飾器的結構了。

   同樣的,輸出流部分也類似,就不去贅述了。

   既然I/O流部分是採用裝飾模式實現的,也就是說,如果我們想要添加新的功能的話,只需要實現新的裝飾器,然後在使用的時候,組合進去就可以了,也就是說,我們可以自定義一個裝飾器,然後和JDK中已有的流的裝飾器一起使用。能行嗎?試試看吧,前面是按照輸入流來講述的,下面的示例按照輸出流來做,順便體會一下Java的輸入流和輸出流在結構上的相似性。

2:自己實現的I/O流的裝飾器——第一版

   來個功能簡單點的,實現把英文加密存放吧,也談不上什麼加密算法,就是把英文字母向後移動兩個位置,比如:a變成c,b變成d,以此類推,最後的y變成a,z就變成b,而且爲了簡單,只處理小寫的,夠簡單的吧。

   好了,還是看看實現簡單的加密的代碼實現吧,示例代碼如下:
/**

 * 實現簡單的加密

 */

public class EncryptOutputStream  extends OutputStream{

    //持有被裝飾的對象

    private OutputStream os = null;

    public EncryptOutputStream(OutputStream os){

       this.os = os;

    }  

    public void write(int a) throws IOException {

       //先統一向後移動兩位

       a = a+2;

       //97是小寫的a的碼值

       if(a >= (97+26)){

           //如果大於,表示已經是y或者z了,減去26就回到a或者b了

           a = a-26;

       }

       this.os.write(a);

    }

}

測試一下看看,好用嗎?客戶端使用代碼示例如下:

public class Client {

    public static void main(String[] args) throws Exception {

       //流式輸出文件

       DataOutputStream dout = new DataOutputStream(

           new BufferedOutputStream(

              new EncryptOutputStream(

                  new FileOutputStream("MyEncrypt.txt"))));

       //然後就可以輸出內容了

       dout.write("abcdxyz".getBytes());

       dout.close();

    }

}

很好,是不是被加密了,雖然是明文的,但已經不是最初存放的內容了,一切顯得非常的完美。

再試試看,不是說裝飾器可以隨意組合嗎,換一個組合方式看看,比如把BufferedOutputStream和我們自己的裝飾器在組合的時候換個位,示例如下:

public class Client {

    public static void main(String[] args) throws Exception {

       //流式輸出文件

       DataOutputStream dout = new DataOutputStream(

           new EncryptOutputStream (

              new BufferedOutputStream(

                  new FileOutputStream("MyEncrypt.txt"))));

       dout.write("abcdxyz".getBytes());

       dout.close();

    }

}

再次運行,看看結果。壞了,出大問題了,這個時候輸出的文件一片空白,什麼都沒有。這是哪裏出了問題呢?

要把這個問題搞清楚,就需要把上面I/O流的內部運行和基本實現搞明白,分開來看看具體的運行過程吧。

(1)先看看成功輸出流中的內容的寫法的運行過程:

當執行到“dout.write("abcdxyz".getBytes());”這句話的時候,會調用DataOutputStream的write方法,把數據輸出到BufferedOutputStream中;
由於BufferedOutputStream流是一個帶緩存的流,它默認緩存8192byte,也就是默認流中的緩存數據到了8192byte,它纔會自動輸出緩存中的數據;
而目前要輸出的字節肯定不到8192byte,因此數據就被緩存在BufferedOutputStream流中了,而不會被自動輸出
當執行到“dout.close();”這句話的時候:會調用關閉DataOutputStream流,這會轉調到傳入DataOutputStream中的流的close方法,也就是BufferedOutputStream的close方法,而BufferedOutputStream的close方法繼承自FilterOutputStream,在FilterOutputStream的close方法實現裏面,會先調用輸出流的方法flush,然後關閉流。也就是此時BufferedOutputStream流中緩存的數據會被強制輸出;
BufferedOutputStream流中緩存的數據被強制輸出到EncryptOutputStream流,也就是我們自己實現的流,沒有緩存,經過處理後繼續輸出;
EncryptOutputStream流會把數據輸出到FileOutputStream中,FileOutputStream會直接把數據輸出到文件中,因此,這種實現方式會輸出文件的內容。

(2)再來看看不能輸出流中的內容的寫法的運行過程:

當執行到“dout.write("abcdxyz".getBytes());”這句話的時候,會調用DataOutputStream的write方法,把數據輸出到EncryptOutputStream中;
EncryptOutputStream流,也就是我們自己實現的流,沒有緩存,經過處理後繼續輸出,把數據輸出到BufferedOutputStream中;
由於BufferedOutputStream流是一個帶緩存的流,它默認緩存8192byte,也就是默認流中的緩存數據到了8192byte,它纔會自動輸出緩存中的數據;
而目前要輸出的字節肯定不到8192byte,因此數據就被緩存在BufferedOutputStream流中了,而不會被自動輸出
當執行到“dout.close();”這句話的時候:會調用關閉DataOutputStream流,這會轉調到傳入DataOutputStream流中的流的close方法,也就是EncryptOutputStream的close方法,而EncryptOutputStream的close方法繼承自OutputStream,在OutputStream的close方法實現裏面,是個空方法,什麼都沒有做。因此,這種實現方式沒有flush流的數據,也就不會輸出文件的內容,自然是一片空白了。

3:自己實現的I/O流的裝飾器——第二版

   要讓我們寫的裝飾器跟其它Java中的裝飾器一樣用,最合理的方案就應該是:讓我們的裝飾器繼承裝飾器的父類,也就是FilterOutputStream類,然後使用父類提供的功能來協助完成想要裝飾的功能。示例代碼如下:
public class EncryptOutputStream2  extends FilterOutputStream{

    private OutputStream os = null;

    public EncryptOutputStream2(OutputStream os){

       //調用父類的構造方法

       super(os);

    }

    public void write(int a) throws IOException {

       //先統一向後移動兩位

       a = a+2;

       //97是小寫的a的碼值

       if(a >= (97+26)){

           //如果大於,表示已經是y或者z了,減去26就回到a或者b了

           a = a-26;

       }

       //調用父類的方法

       super.write(a);

    }

}

22.3.3 裝飾模式和AOP

   裝飾模式和AOP在思想上有共同之處。可能有些朋友還不太瞭解AOP,下面先簡單介紹一下AOP的基礎知識。

1:什麼是AOP——面向方面編程

   AOP是一種編程範式,提供從另一個角度來考慮程序結構以完善面向對象編程(OOP)。

在面向對象開發中,考慮系統的角度通常是縱向的,比如我們經常畫出的如下的系統架構圖,默認都是從上到下,上層依賴於下層,如圖22.5所示:

這裏寫圖片描述

而在每個模塊內部呢?就拿大家都熟悉的三層架構來說,也是從上到下來考慮的,通常是表現層調用邏輯層,邏輯層調用數據層,如圖22.6所示:
這裏寫圖片描述

慢慢的,越來越多的人發現,在各個模塊之中,存在一些共性的功能,比如日誌管理、事務管理等等,如圖22.7所示:

這裏寫圖片描述

這個時候,在思考這些共性功能的時候,是從橫向在思考問題,與通常面向對象的縱向思考角度不同,很明顯,需要有新的解決方案,這個時候AOP站出來了。

AOP爲開發者提供了一種描述橫切關注點的機制,並能夠自動將橫切關注點織入到面向對象的軟件系統中,從而實現了橫切關注點的模塊化。

AOP能夠將那些與業務無關,卻爲業務模塊所共同調用的邏輯或責任,例如事務處理、日誌管理、權限控制等,封裝起來,便於減少系統的重複代碼,降低模塊間的耦合度,並有利於未來的可操作性和可維護性。

   AOP之所以強大,就是因爲它能夠自動把橫切關注點的功能模塊,自動織入回到軟件系統中,這是什麼意思呢?

先看看沒有AOP,在常規的面向對象系統中,對這種共性的功能如何處理,大都是把這些功能提煉出來,然後在需要用到的地方進行調用,只畫調用通用日誌的公共模塊,其它的類似,就不去畫了,如圖22.8所示

這裏寫圖片描述

看清楚,是從應用模塊中主動去調用公共模塊,也就是應用模塊要很清楚公共模塊的功能,還有具體的調用方法才行,應用模塊是依賴於公共模塊的,是耦合的,這樣一來,要想修改公共模塊就會很困難了,牽一而發百。

看看有了AOP會怎樣,還是畫個圖來說明,如圖22.9所示:

這裏寫圖片描述

乍一看,跟上面不用AOP沒有什麼區別嘛,真的嗎?看得仔細點,有一個非常非常大的改變,就是所有的箭頭方向反過來了,原來是應用系統主動去調用各個公共模塊的,現在變成了各個公共模塊主動織入回到應用系統。

不要小看這一點變化,這樣一來應用系統就不需要知道公共功能模塊,也就是應用系統和公共功能解耦了。公共功能會在合適的時候,由外部織入回到應用系統中,至於誰來實現這樣的功能,以及如何實現不再我們的討論之列,我們更關注這個思想。

如果按照裝飾模式來對比上述過程,業務功能對象就可以被看作是被裝飾的對象,而各個公共的模塊就好比是裝飾器,可以透明的來給業務功能對象增加功能。

所以從某個側面來說,裝飾模式和AOP要實現的功能是類似的,只不過AOP的實現方法不同,會更加靈活,更加可配置;另外AOP一個更重要的變化是思想上的變化——“主從換位”,讓原本主動調用的功能模塊變成了被動等待,甚至毫不知情的情況下被織入了很多新的功能。

2:使用裝飾模式做出類似AOP的效果

下面來演示一下使用裝飾模式,把一些公共的功能,比如權限控制,日誌記錄,透明的添加回到業務功能模塊中去,做出類似AOP的效果。

(1)首先定義業務接口

這個接口相當於裝飾模式的Component。注意這裏使用的是接口,而不像前面一樣使用的是抽象類,雖然使用抽象類的方式來定義組件是裝飾模式的標準實現方式,但是如果不需要爲子類提供公共的功能的話,也是可以實現成接口的,這點要先說明一下,免得有些朋友會認爲這就不是裝飾模式了,示例代碼如下:

/**

* 商品銷售管理的業務接口

*/

public interface GoodsSaleEbi {

    /**

     * 保存銷售信息,本來銷售數據應該是多條,太麻煩了,爲了演示,簡單點

     * @param user 操作人員

     * @param customer 客戶

     * @param saleModel 銷售數據

     * @return 是否保存成功

     */

    public boolean sale(String user,String customer,

SaleModel saleModel);

}

順便把封裝業務數據的對象也定義出來,很簡單,示例代碼如下:

/**

* 封裝銷售單的數據,簡單的示意一些

*/

public class SaleModel {

    /**

* 銷售的商品

*/

    private String goods;

    /**

* 銷售的數量

*/

    private int saleNum;

    public String getGoods() { 

return goods;

}

    public void setGoods(String goods) {

this.goods = goods; 

}

    public int getSaleNum() {

       return saleNum;

    }

    public void setSaleNum(int saleNum) {

       this.saleNum = saleNum;

    }

    public String toString(){

       return "商品名稱="+goods+",購買數量="+saleNum;

    }

}

(2)定義基本的業務實現對象,示例代碼如下:

public class GoodsSaleEbo implements GoodsSaleEbi{

    public boolean sale(String user,String customer,

SaleModel saleModel) {

       System.out.println(user+"保存了"

+customer+"購買 "+saleModel+" 的銷售數據");

       return true;

    }

}

(3)接下來該來實現公共功能了,把這些公共功能實現成爲裝飾器,那麼需要給它們定義一個抽象的父類,示例如下:

/**

* 裝飾器的接口,需要跟被裝飾的對象實現同樣的接口

*/

public abstract class Decorator implements GoodsSaleEbi{

    /**

* 持有被裝飾的組件對象

*/

    protected GoodsSaleEbi ebi;

    /**

     * 通過構造方法傳入被裝飾的對象

     * @param ebi被裝飾的對象

     */

    public Decorator(GoodsSaleEbi ebi){

       this.ebi = ebi;

    }

}

(4)實現權限控制的裝飾器

先檢查是否有運行的權限,如果有就繼續調用,如果沒有,就不遞歸調用了,而是輸出沒有權限的提示,示例代碼如下:

/**

 * 實現權限控制

 */

public class CheckDecorator extends Decorator{

    public CheckDecorator(GoodsSaleEbi ebi){

       super(ebi);

    }

    public boolean sale(String user,String customer

, SaleModel saleModel) {

       //簡單點,只讓張三執行這個功能

       if(!"張三".equals(user)){

           System.out.println("對不起"+user

+",你沒有保存銷售單的權限");

           //就不再調用被裝飾對象的功能了

           return false;

       }else{

           return this.ebi.sale(user, customer, saleModel);

        }      

    }

}

(5)實現日誌記錄的裝飾器,就是在功能執行完成後記錄日誌即可,示例代碼如下:

/**

* 實現日誌記錄

*/

public class LogDecorator extends Decorator{

    public LogDecorator(GoodsSaleEbi ebi){

       super(ebi);

    }

    public boolean sale(String user,String customer,

SaleModel saleModel) {

       //執行業務功能

       boolean f = this.ebi.sale(user, customer, saleModel);



       //在執行業務功能過後,記錄日誌

       DateFormat df =

new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");

       System.out.println("日誌記錄:"+user+"於"+

df.format(new Date())+"時保存了一條銷售記錄,客戶是"

+customer+",購買記錄是"+saleModel);

       return f;

    }

}

(6)組合使用這些裝飾器

在組合的時候,權限控制應該是最先被執行的,所以把它組合在最外面,日誌記錄的裝飾器會先調用原始的業務對象,所以把日誌記錄的裝飾器組合在中間。

前面講過,裝飾器之間最好不要有順序限制,但是在實際應用中,要根據具體的功能要求來,有需要的時候,也可以有順序的限制,但應該儘量避免這種情況。

此時客戶端測試代碼示例如下:

public class Client {

    public static void main(String[] args) {

       //得到業務接口,組合裝飾器

       GoodsSaleEbi ebi = new CheckDecorator(

new LogDecorator(

new GoodsSaleEbo()));

       //準備測試數據

       SaleModel saleModel = new SaleModel();

       saleModel.setGoods("Moto手機");

       saleModel.setSaleNum(2);

       //調用業務功能

       ebi.sale("張三","張三丰", saleModel);

       ebi.sale("李四","張三丰", saleModel);

    }

}
 好好體會一下,是不是也在沒有驚動原始業務對象的情況下,給它織入了新的功能呢?也就是說是在原始業務不知情的情況下,給原始業務對象透明的增加了新功能,從而模擬實現了AOP的功能。

事實上,這種做法,完全可以應用在項目開發上,在後期爲項目的業務對象添加數據檢查、權限控制、日誌記錄等功能,就不需要在業務對象上去處理這些功能了,業務對象可以更專注於具體業務的處理

22.3.4 裝飾模式的優缺點

l 比繼承更靈活
從爲對象添加功能的角度來看,裝飾模式比繼承來得更靈活。繼承是靜態的,而且一旦繼承是所有子類都有一樣的功能。而裝飾模式採用把功能分離到每個裝飾器當中,然後通過對象組合的方式,在運行時動態的組合功能,每個被裝飾的對象,最終有哪些功能,是由運行期動態組合的功能來決定的。

l 更容易複用功能
裝飾模式把一系列複雜的功能,分散到每個裝飾器當中,一般一個裝飾器只實現一個功能,這樣實現裝飾器變得簡單,更重要的是這樣有利於裝飾器功能的複用,可以給一個對象增加多個同樣的裝飾器,也可以把一個裝飾器用來裝飾不同的對象,從而複用裝飾器的功能。

l 簡化高層定義
裝飾模式可以通過組合裝飾器的方式,給對象增添任意多的功能,因此在進行高層定義的時候,不用把所有的功能都定義出來,而是定義最基本的就可以了,可以在使用需要的時候,組合相應的裝飾器來完成需要的功能。

l 會產生很多細粒度對象
前面說了,裝飾模式是把一系列複雜的功能,分散到每個裝飾器當中,一般一個裝飾器只實現一個功能,這樣會產生很多細粒度的對象,而且功能越複雜,需要的細粒度對象越多。

22.3.5 思考裝飾模式

1:裝飾模式的本質

裝飾模式的本質:動態組合。

   動態是手段,組合纔是目的。這裏的組合有兩個意思,一個是動態功能的組合,也就是動態進行裝飾器的組合;另外一個是指對象組合,通過對象組合來實現爲被裝飾對象透明的增加功能。

   但是要注意,裝飾模式不僅僅可以增加功能,也可以控制功能的訪問,可以完全實現新的功能,還可以控制裝飾的功能是在被裝飾功能之前還是之後來運行等。

   總之,裝飾模式是通過把複雜功能簡單化,分散化,然後在運行期間,根據需要來動態組合的這麼一個模式。

2:何時選用裝飾模式

建議在如下情況中,選用裝飾模式:

如果需要在不影響其它對象的情況下,以動態、透明的方式給對象添加職責,可以使用裝飾模式,這幾乎就是裝飾模式的主要功能
如果不合適使用子類來進行擴展的時候,可以考慮使用裝飾模式,因爲裝飾模式是使用的“對象組合”的方式。所謂不適合用子類擴展的方式,比如:擴展功能需要的子類太多,造成子類數目呈爆炸性增長。

22.3.6 相關模式

l 裝飾模式與適配器模式
這是兩個沒有什麼關聯的模式,放到一起來說,是因爲它們有一個共同的別名:Wrapper。
這兩個模式功能上是不一樣的,適配器模式是用來改變接口的,而裝飾模式是用來改變對象功能的。

l 裝飾模式與組合模式
這兩個模式有相似之處,都涉及到對象的遞歸調用,從某個角度來說,可以把裝飾看成是隻有一個組件的組合。
但是它們的目的完全不一樣,裝飾模式是要動態的給對象增加功能;而組合模式是想要管理組合對象和葉子對象,爲它們提供一個一致的操作接口給客戶端,方便客戶端的使用。

l 裝飾模式與策略模式
這兩個模式可以組合使用。
策略模式也可以實現動態的改變對象的功能,但是策略模式只是一層選擇,也就是根據策略選擇一下具體的實現類而已。而裝飾模式不是一層,而是遞歸調用,無數層都可以,只要組合好裝飾器的對象組合,那就可以依次調用下去,所以裝飾模式會更靈活。
而且策略模式改變的是原始對象的功能,不像裝飾模式,後面一個裝飾器,改變的是經過前一個裝飾器裝飾過後的對象,也就是策略模式改變的是對象的內核,而裝飾模式改變的是對象的外殼。
這兩個模式可以組合使用,可以在一個具體的裝飾器裏面使用策略模式,來選擇更具體的實現方式。比如前面計算獎金的另外一個問題就是參與計算的基數不同,獎金的計算方式也是不同的。舉例來說:假設張三和李四參與同一個獎金的計算,張三的銷售總額是2萬元,而李四的銷售額是8萬元,它們的計算公式是不一樣的,假設獎金的計算規則是,銷售額在5萬以下,統一3%,而5萬以上,5萬內是4%,超過部分是6%。
參與同一個獎金的計算,這就意味着可以使用同一個裝飾器,但是在裝飾器的內部,不同條件下計算公式不一樣,那麼怎麼選擇具體的實現策略呢?自然使用策略模式就好了,也就是裝飾模式和策略模式組合來使用。

l 裝飾模式與模板方法模式
這是兩個功能上有相似點的模式。
模板方法模式主要應用在算法骨架固定的情況,那麼要是算法步驟不固定呢,也就是一個相對動態的算法步驟,就可以使用裝飾模式了,因爲在使用裝飾模式的時候,進行裝飾器的組裝,其實也相當於是一個調用算法步驟的組裝,相當於是一個動態的算法骨架。
既然裝飾模式可以實現動態的算法步驟的組裝和調用,那麼把這些算法步驟固定下來,那就是模板方法模式實現的功能了,因此裝飾模式可以模擬實現模板方法模式的功能。
但是請注意,僅僅只是可以模擬功能而已,兩個模式的設計目的、原本的功能、本質思想等都是不一樣的。

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