【結構型模式十一】裝飾者(Decorator)

1 場景問題#

1.1 復雜的獎金計算##

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

獎金計算是相對復雜的功能,尤其是對于業(yè)務部門的獎金計算方式,是非常復雜的李皇,除了業(yè)務功能復雜外,另外一個麻煩之處是計算方式還經常需要變動宙枷,因為業(yè)務部門經常通過調整獎金的計算方式來激勵士氣掉房。

先從業(yè)務上看看現有的獎金計算方式的復雜性:

首先是獎金分類:對于個人,大致有個人當月業(yè)務獎金慰丛、個人累計獎金圃阳、個人業(yè)務增長獎金、及時回款獎金璧帝、限時成交加碼獎金等等捍岳;

對于業(yè)務主管或者是業(yè)務經理,除了個人獎金外睬隶,還有:團隊累計獎金锣夹、團隊業(yè)務增長獎金、團隊盈利獎金等等苏潜。

其次是計算獎金的金額银萍,又有這么幾個基數:銷售額、銷售毛利恤左、實際回款贴唇、業(yè)務成本搀绣、獎金基數等等;

另外一個就是計算的公式戳气,針對不同的人链患、不同的獎金類別、不同的計算獎金的金額瓶您,計算的公式是不同的麻捻,就算是同一個公式,里面計算的比例參數也有可能是不同的呀袱。

1.2 簡化后的獎金計算體系##

看了上面獎金計算的問題贸毕,所幸我們只是來學習設計模式,并不是真的要去實現整個獎金計算體系的業(yè)務夜赵,因此也沒有必要把所有的計算業(yè)務都羅列在這里明棍,為了后面演示的需要,簡化一下寇僧,演示用的獎金計算體系如下:

  1. 每個人當月業(yè)務獎金 = 當月銷售額 X 3%

  2. 每個人累計獎金 = 總的回款額 X 0.1%

  3. 團隊獎金 = 團隊總銷售額 X 1%

1.3 不用模式的解決方案##

一個人的獎金分成很多個部分击蹲,要實現獎金計算,主要就是要按照各個獎金計算的規(guī)則婉宰,把這個人可以獲取的每部分獎金計算出來歌豺,然后計算一個總和,這就是這個人可以得到的獎金心包。

  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);  
      }  
}
  1. 按照獎金計算的規(guī)則,實現獎金計算炉爆,示例代碼如下:
/** 
   * 計算獎金的對象 
   */  
public class Prize {  
      /** 
       * 計算某人在某段時間內的獎金堕虹,有些參數在演示中并不會使用, 
       * 但是在實際業(yè)務實現上是會用的芬首,為了表示這是個具體的業(yè)務方法赴捞, 
       * 因此這些參數被保留了 
       * @param user 被計算獎金的人員 
       * @param begin 計算獎金的開始時間 
       * @param end 計算獎金的結束時間 
       * @return 某人在某段時間內的獎金 
       */  
      public  double calcPrize(String user,Date begin,Date end){  
          double prize = 0.0;   
          //計算當月業(yè)務獎金,所有人都會計算  
          prize = this.monthPrize(user, begin, end);  
          //計算累計獎金  
          prize += this.sumPrize(user, begin, end);  
         
          //需要判斷該人員是普通人員還是業(yè)務經理郁稍,團隊獎金只有業(yè)務經理才有  
          if(this.isManager(user)){  
              prize += this.groupPrize(user, begin, end);  
          }  
          return prize;  
      }  
 
      /** 
       * 計算某人的當月業(yè)務獎金赦政,參數重復,就不再注釋了 
       */  
      private double monthPrize(String user, Date begin, Date end) {  
          //計算當月業(yè)務獎金,按照人員去獲取當月的業(yè)務額耀怜,然后再乘以3%  
          double prize = TempDB.mapMonthSaleMoney.get(user) * 0.03;  
          System.out.println(user+"當月業(yè)務獎金"+prize);  
          return prize;  
      }  
 
      /** 
       * 計算某人的累計獎金恢着,參數重復桐愉,就不再注釋了 
       */  
      public double sumPrize(String user, Date begin, Date end) {  
          //計算累計獎金,其實應該按照人員去獲取累計的業(yè)務額,然后再乘以0.1%  
          //簡單演示一下掰派,假定大家的累計業(yè)務額都是1000000元  
          double prize = 1000000 * 0.001;  
          System.out.println(user+"累計獎金"+prize);  
          return prize;  
      }     
 
      /** 
       * 判斷人員是普通人員還是業(yè)務經理 
       * @param user 被判斷的人員 
       * @return true表示是業(yè)務經理,false表示是普通人員 
       */  
      private boolean isManager(String user){  
          //應該從數據庫中獲取人員對應的職務  
          //為了演示从诲,簡單點判斷,只有王五是經理  
          if("王五".equals(user)){  
              return true;              
          }  
          return false;  
      }  
      /** 
       * 計算當月團隊業(yè)務獎碗淌,參數重復,就不再注釋了 
       */  
      public double groupPrize(String user, Date begin, Date end) {  
          //計算當月團隊業(yè)務獎金抖锥,先計算出團隊總的業(yè)務額亿眠,然后再乘以1%,  
          //假設都是一個團隊的  
          double group = 0.0;  
          for(double d : TempDB.mapMonthSaleMoney.values()){  
              group += d;  
          }  
          double prize = group * 0.01;  
          System.out.println(user+"當月團隊業(yè)務獎金"+prize);  
          return prize;  
      }  
}  
  1. 寫個客戶端來測試一下磅废,看看是否能正確地計算獎金纳像,示例代碼如下:
public class Client {  
      public static void main(String[] args) {  
          //先創(chuàng)建計算獎金的對象  
          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);  
      }
}  

測試運行的結果如下:

張三當月業(yè)務獎金300.0  
張三累計獎金1000.0  
==========張三應得獎金:1300.0  
李四當月業(yè)務獎金600.0  
李四累計獎金1000.0  
==========李四應得獎金:1600.0  
王五當月業(yè)務獎金900.0  
王五累計獎金1000.0  
王五當月團隊業(yè)務獎金600.0  
==========王經理應得獎金:2500.0 

1.4 有何問題##

看了上面的實現拯勉,挺簡單的嘛竟趾,就是計算方式麻煩點,每個規(guī)則都要實現宫峦。真的很簡單嗎岔帽?仔細想想,有沒有什么問題导绷?

對于獎金計算犀勒,光是計算方式復雜,也就罷了妥曲,不過是實現起來會困難點贾费,相對而言還是比較好解決的,不過是用程序把已有的算法表達出來檐盟。

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

舉個簡單的例子來說壳坪,現在根據業(yè)務需要舶得,需要增加一個“環(huán)比增長獎金”,就是本月的銷售額比上個月有增加爽蝴,而且要達到一定的比例沐批,當然增長比例越高纫骑,獎金比例越大。那么軟件就必須要重新實現這么個功能九孩,并正確的添加到系統(tǒng)中去先馆。過了兩個月,業(yè)務獎勵的策略發(fā)生了變化躺彬,不再需要這個獎金了煤墙,或者是另外換了一個新的獎金方式了,那么軟件就需要把這個功能從軟件中去掉宪拥,然后再實現新的功能仿野。

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

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

還有一個問題亿扁,就是在運行期間,不同人員參與的獎金計算方式也是不同的鸟廓,舉例來說:如果是業(yè)務經理魏烫,除了參與個人計算部分外,還要參加團隊獎金的計算肝箱,這就意味著需要在運行期間動態(tài)來組合需要計算的部分哄褒,也就是會有一堆的if-else。

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

  1. 計算邏輯復雜呐赡;

  2. 要有足夠靈活性,可以方便的增加或者減少功能骏融;

  3. 要能動態(tài)的組合計算方式链嘀,不同的人參與的計算不同;

上面描述的獎金計算的問題档玻,絕對沒有任何夸大成分怀泊,相反已經簡化不少了,還有更多麻煩沒有寫上來误趴,畢竟我們的重點在設計模式霹琼,而不是業(yè)務。

把上面的問題抽象一下,設若有一個計算獎金的對象枣申,現在需要能夠靈活的給它增加和減少功能售葡,還需要能夠動態(tài)的組合功能,每個功能就相當于在計算獎金的某個部分忠藤。

現在的問題就是:如何才能夠透明的給一個對象增加功能挟伙,并實現功能的動態(tài)組合呢?

2 解決方案#

2.1 裝飾模式來解決##

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

  1. 裝飾模式定義
裝飾模式定義
  1. 應用裝飾模式來解決的思路

雖然經過簡化榨咐,業(yè)務簡單了很多介却,但是需要解決的問題不會少,還是要解決:要透明的給一個對象增加功能祭芦,并實現功能的動態(tài)組合筷笨。

所謂透明的給一個對象增加功能憔鬼,換句話說就是要給一個對象增加功能龟劲,但是不能讓這個對象知道,也就是不能去改動這個對象轴或。而實現了能夠給一個對象透明的增加功能昌跌,自然就能夠實現功能的動態(tài)組合,比如原來的對象有A功能照雁,現在透明的給它增加了一個B功能蚕愤,是不是就相當于動態(tài)組合了A和B功能呢。

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

在裝飾模式的實現中饵蒂,為了能夠和原來使用被裝飾對象的代碼實現無縫結合,是通過定義一個抽象類酱讶,讓這個類實現與被裝飾對象相同的接口退盯,然后在具體實現類里面,轉調被裝飾的對象,在轉調的前后添加新的功能得问,這就實現了給被裝飾對象增加功能囤攀,這個思路跟“對象組合”非常類似

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

2.2 模式結構和說明##

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

裝飾模式的結構圖

Component:組件對象的接口,可以給這些對象動態(tài)的添加職責蝌蹂。

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

Decorator:所有裝飾器的抽象父類,需要定義一個與組件接口一致的接口呕缭,并持有一個Component對象饱亿,其實就是持有一個被裝飾的對象。注意给郊,這個被裝飾的對象不一定是最原始的那個對象了牡肉,也可能是被其它裝飾器裝飾過后的對象,反正都是實現的同一個接口淆九,也就是同一類型统锤。

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

2.3 裝飾模式示例代碼##

  1. 先來看看組件對象的接口定義饲窿,示例代碼如下:
/** 
   * 組件對象的接口,可以給這些對象動態(tài)的添加職責 
   */  
public abstract class Component {  
      /** 
       * 示例方法 
       */  
      public abstract void operation();  
}
  1. 定義了接口焕蹄,那就看看具體組件實現對象示意吧逾雄,示例代碼如下:
/** 
   * 具體實現組件對象接口的對象 
   */  
public class ConcreteComponent extends Component {  
      public void operation() {  
          //相應的功能處理  
      }  
}  
  1. 接下來看看抽象的裝飾器對象,示例代碼如下:
/** 
   * 裝飾器接口擦盾,維持一個指向組件對象的接口對象嘲驾,并定義一個與組件接口一致的接口 
   */  
public abstract class Decorator extends Component {  
      /** 
       * 持有組件對象 
       */  
      protected Component component;  
      /** 
       * 構造方法,傳入組件對象 
       * @param component 組件對象 
       */  
      public Decorator(Component component) {  
          this.component = component;  
      }  
      public void operation() {  
          //轉發(fā)請求給組件對象迹卢,可以在轉發(fā)前后執(zhí)行一些附加動作  
          component.operation();
      }
}  
  1. 該來看看具體的裝飾器實現對象了辽故,這里有兩個示意對象,一個示意了添加狀態(tài)腐碱,一個示意了添加職責誊垢。先看添加了狀態(tài)的示意對象吧掉弛,示例代碼如下:
/** 
   * 裝飾器的具體實現對象,向組件對象添加職責 
   */  
public class ConcreteDecoratorA extends Decorator {  
      public ConcreteDecoratorA(Component component) {  
          super(component);  
      }  
      /** 
       * 添加的狀態(tài) 
       */  
      private String addedState;    
      public String getAddedState() {  
          return addedState;  
      }  
      public void setAddedState(String addedState) {  
          this.addedState = addedState;  
      }  
      public void operation() {  
          //調用父類的方法喂走,可以在調用前后執(zhí)行一些附加動作  
          //在這里進行處理的時候殃饿,可以使用添加的狀態(tài)  
          super.operation();  
      }  
}  
  1. 接下來看看添加職責的示意對象,示例代碼如下:
/** 
   * 裝飾器的具體實現對象芋肠,向組件對象添加職責 
   */  
public class ConcreteDecoratorB extends Decorator {  
      public ConcreteDecoratorB(Component component) {  
          super(component);  
      }  
      /** 
       * 需要添加的職責 
       */  
      private void addedBehavior() {  
          //需要添加的職責實現  
      }  
      public void operation() {  
          //調用父類的方法乎芳,可以在調用前后執(zhí)行一些附加動作  
          super.operation();
          addedBehavior();
      }
}

2.4 使用裝飾模式重寫示例##

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

首先需要定義一個組件對象的接口,在這個接口里面定義計算獎金的業(yè)務方法睡汹,因為外部就是使用這個接口來操作裝飾模式構成的對象結構中的對象

需要添加一個基本的實現組件接口的對象肴甸,可以讓它返回獎金為0就可以了

把各個計算獎金的規(guī)則當作裝飾器對象,需要為它們定義一個統(tǒng)一的抽象的裝飾器對象囚巴,好約束各個具體的裝飾器的接口

把各個計算獎金的規(guī)則實現成為具體的裝飾器對象

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

使用裝飾模式重寫示例的程序結構示意圖
  1. 計算獎金的組件接口和基本的實現對象

在計算獎金的組件接口中彤叉,需要定義原本的業(yè)務方法庶柿,也就是實現獎金計算的方法,示例代碼如下:

/** 
   * 計算獎金的組件接口 
   */  
public abstract class Component {  
      /** 
       * 計算某人在某段時間內的獎金姆坚,有些參數在演示中并不會使用澳泵, 
       * 但是在實際業(yè)務實現上是會用的实愚,為了表示這是個具體的業(yè)務方法兼呵, 
       * 因此這些參數被保留了 
       * @param user 被計算獎金的人員 
       * @param begin 計算獎金的開始時間 
       * @param end 計算獎金的結束時間 
       * @return 某人在某段時間內的獎金 
       */  
      public abstract double calcPrize(String user, Date begin, Date end);  
}

為這個業(yè)務接口提供一個基本的實現,示例代碼如下:

/** 
   * 基本的實現計算獎金的類腊敲,也是被裝飾器裝飾的對象 
   */  
public class ConcreteComponent extends Component{  
      public double calcPrize(String user, Date begin, Date end) {  
          //只是一個默認的實現击喂,默認沒有獎金  
          return 0;  
      }  
} 
  1. 定義抽象的裝飾器

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

/** 
   * 裝飾器的接口凌彬,需要跟被裝飾的對象實現同樣的接口 
   */  
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);  
      }  
}
  1. 定義一系列的裝飾器對象

用一個具體的裝飾器對象,來實現一條計算獎金的規(guī)則循衰,現在有三條計算獎金的規(guī)則铲敛,那就對應有三個裝飾器對象來實現,依次來看看它們的實現会钝。

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

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

/** 
   * 裝飾器對象先鱼,計算當月業(yè)務獎金 
   */  
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:然后計算當月業(yè)務獎金,按人員和時間去獲取當月業(yè)務額俭正,然后再乘以3%  
          double prize = TempDB.mapMonthSaleMoney.get(user) * 0.03;  
          System.out.println(user+"當月業(yè)務獎金"+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:然后計算累計獎金,其實應按人員去獲取累計的業(yè)務額掸读,然后再乘以0.1%  
          //簡單演示一下,假定大家的累計業(yè)務額都是1000000元  
          double prize = 1000000 * 0.001;  
          System.out.println(user+"累計獎金"+prize);  
          return money + prize;  
      }  
}

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

/** 
   * 裝飾器對象寺枉,計算當月團隊業(yè)務獎金 
   */  
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:然后計算當月團隊業(yè)務獎金,先計算出團隊總的業(yè)務額绷落,然后再乘以1%  
          //假設都是一個團隊的  
          double group = 0.0;  
          for(double d : TempDB.mapMonthSaleMoney.values()){  
              group += d;  
          }  
          double prize = group * 0.01;  
          System.out.println(user+"當月團隊業(yè)務獎金"+prize);  
          return money + prize;  
      }  
}
  1. 使用裝飾器的客戶端

使用裝飾器的客戶端姥闪,首先需要創(chuàng)建被裝飾的對象,然后創(chuàng)建需要的裝飾對象砌烁,接下來重要的工作就是組合裝飾對象筐喳,依次對前面的對象進行裝飾。

有很多類似的例子函喉,比如生活中的裝修避归,就拿裝飾墻壁來說吧:沒有裝飾前是原始的磚墻,這就好比是被裝飾的對象管呵,首先需要刷膩子梳毙,把墻找平,這就好比對原始的磚墻進行了一次裝飾捐下,而刷的膩子就好比是一個裝飾器對象账锹;好了,裝飾一回了坷襟,接下來該刷墻面漆了奸柬,這又好比裝飾了一回,刷的墻面漆就好比是又一個裝飾器對象婴程,而且這回被裝飾的對象不是原始的磚墻了廓奕,而是被膩子裝飾器裝飾過后的墻面,也就是說后面的裝飾器是在前面的裝飾器裝飾過后的基礎之上档叔,繼續(xù)裝飾的桌粉,類似于一層一層疊加的功能。

同樣的道理衙四,計算獎金也是這樣铃肯,先創(chuàng)建基本的獎金對象,然后組合需要計算的獎金類型届搁,依次組合計算缘薛,最后的結果就是總的獎金窍育。示例代碼如下:

/** 
   * 使用裝飾模式的客戶端 
   */  
public class Client {  
      public static void main(String[] args) {  
          //先創(chuàng)建計算基本獎金的類,這也是被裝飾的對象  
          Component c1 = new ConcreteComponent();  
         
          //然后對計算的基本獎金進行裝飾宴胧,這里要組合各個裝飾  
          //說明漱抓,各個裝飾者之間最好是不要有先后順序的限制,  
          //也就是先裝飾誰和后裝飾誰都應該是一樣的  
         
          //先組合普通業(yè)務人員的獎金計算  
          Decorator d1 = new MonthPrizeDecorator(c1);  
          Decorator d2 = new SumPrizeDecorator(d1);     
         
          //注意:這里只需使用最后組合好的對象調用業(yè)務方法即可恕齐,會依次調用回去  
          //日期對象都沒有用上乞娄,所以傳null就可以了  
          double zs = d2.calcPrize("張三",null,null);         
          System.out.println("==========張三應得獎金:"+zs);  
          double ls = d2.calcPrize("李四",null,null);  
          System.out.println("==========李四應得獎金:"+ls);  
         
          //如果是業(yè)務經理,還需要一個計算團隊的獎金計算  
          Decorator d3 = new GroupPrizeDecorator(d2);  
          double ww = d3.calcPrize("王五",null,null);  
          System.out.println("==========王經理應得獎金:"+ww);  
      }  
}  

測試一下显歧,看看結果仪或,示例如下:

張三當月業(yè)務獎金300.0  
張三累計獎金1000.0  
==========張三應得獎金:1300.0  
李四當月業(yè)務獎金600.0  
李四累計獎金1000.0  
==========李四應得獎金:1600.0  
王五當月業(yè)務獎金900.0  
王五累計獎金1000.0  
王五當月團隊業(yè)務獎金600.0  
==========王經理應得獎金:2500.0 

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

裝飾模式示例的組合和調用過程示意圖

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

如同上面的示例添忘,對于基本的計算獎金的對象而言,由于計算獎金的邏輯太過于復雜若锁,而且需要在不同的情況下進行不同的運算搁骑,為了靈活性,把多種計算獎金的方式分散到不同的裝飾器對象里面又固,采用動態(tài)組合的方式仲器,來給基本的計算獎金的對象增添計算獎金的功能,每個裝飾器相當于計算獎金的一個部分口予。

這種方式明顯比為基本的計算獎金的對象增加子類來得更靈活娄周,因為裝飾模式的起源點是采用對象組合的方式涕侈,然后在組合的時候順便增加些功能沪停。為了達到一層一層組裝的效果,裝飾模式還要求裝飾器要實現與被裝飾對象相同的業(yè)務接口裳涛,這樣才能以同一種方式依次組合下去木张。

靈活性還體現在動態(tài)上,如果是繼承的方式端三,那么所有的類實例都有這個功能了舷礼,而采用裝飾模式,可以動態(tài)的為某幾個對象實例添加功能郊闯,而不是對整個類添加功能妻献。比如上面示例中蛛株,客戶端測試的時候,對張三李四就只是組合了兩個功能育拨,對王五就組合了三個功能谨履,但是原始的計算獎金的類都是一樣的,只是動態(tài)的為它增加的功能不同而已熬丧。

3 模式講解#

3.1 認識裝飾模式##

  1. 模式功能

裝飾模式能夠實現動態(tài)的為對象添加功能笋粟,是從一個對象外部來給對象增加功能,相當于是改變了對象的外觀析蝴。當裝飾過后害捕,從外部使用系統(tǒng)的角度看,就不再是使用原始的那個對象了闷畸,而是使用被一系列的裝飾器裝飾過后的對象尝盼。

這樣就能夠靈活的改變一個對象的功能,只要動態(tài)組合的裝飾器發(fā)生了改變佑菩,那么最終所得到的對象的功能也就發(fā)生了改變东涡。

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

  1. 對象組合

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

其實,現在在面向對象設計中菇夸,有一條很基本的規(guī)則就是“盡量使用對象組合琼富,而不是對象繼承”來擴展和復用功能。裝飾模式的思考起點就是這個規(guī)則庄新,可能有些朋友還不太熟悉什么是“對象組合”鞠眉,下面介紹一下“對象組合”。

什么是對象組合

直接舉例來說吧择诈,假若有一個對象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 {
      /**
       * 創(chuàng)建A對象的實例
       */
      private A a = new A();

      public void a1(){
          //轉調A對象的功能
          a.a1();
      }
      public void c11(){
          System.out.println("now in C2.c11");
      }
}

大家想想,在轉調前后是不是還可以做些功能處理呢煮剧?對于A對象是不是透明的呢斥滤?對象組合是不是也很簡單,而且更靈活了

首先可以有選擇的復用功能勉盅,不是所有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");
      }
}

最后再說一點胶背,就是關于對象組合中,何時創(chuàng)建被組合對象的實例

一種方案是在屬性上直接定義并創(chuàng)建需要組合的對象實例喘先;

另外一種方案是在屬性上定義一個變量钳吟,來表示持有被組合對象的實例,具體實例從外部傳入窘拯,也可以通過IoC/DI容器來注入红且;

public class C4 {
      //示例直接在屬性上創(chuàng)建需要組合的對象
      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");
      }
}
  1. 裝飾器

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

在裝飾器里不僅僅是可以給被裝飾對象增加功能砂轻,還可以根據需要選擇是否調用被裝飾對象的功能,如果不調用被裝飾對象的功能斤吐,那就變成完全重新實現了搔涝,相當于動態(tài)修改了被裝飾對象的功能厨喂。

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

  1. 裝飾器和組件類的關系

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

組件類是不知道裝飾器的存在的,裝飾器給組件添加功能是一種透明的包裝籍铁,組件類毫不知情涡上。需要改變的是外部使用組件類的地方,現在需要使用包裝后的類拒名,接口是一樣的吩愧,但是具體的實現類發(fā)生了改變。

  1. 退化形式

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

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();
          }        
      }
}

仔細觀察上面的代碼,會發(fā)現最里層是一個FileInputStream對象咒唆,然后把它傳遞給一個BufferedInputStream對象届垫,經過BufferedInputStream處理過后,再把處理過后的對象傳遞給了DataInputStream對象進行處理全释,這個過程其實就是裝飾器的組裝過程装处,FileInputStream對象相當于原始的被裝飾的對象,而BufferedInputStream對象和DataInputStream對象則相當于裝飾器浸船。

可能有朋友會問妄迁,裝飾器和具體的組件類是要實現同樣的接口的寝蹈,上面這些類是這樣嗎?看看Java的I/O對象層次圖吧登淘,由于Java的I/O對象眾多箫老,因此只是畫出了InputStream的部分,而且由于圖的大小關系黔州,也只是表現出了部分的流耍鬓,具體如圖4所示:

Java的I/O的InputStream部分對象層次圖

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

InputStream就相當于裝飾模式中的Component流妻。

其實FileInputStream牲蜀、ObjectInputStream、StringBufferInputStream這幾個對象是直接繼承了InputStream合冀,還有幾個直接繼承InputStream的對象各薇,比如:ByteArrayInputStream、PipedInputStream等君躺。這些對象相當于裝飾模式中的ConcreteComponent峭判,是可以被裝飾器裝飾的對象。

那么FilterInputStream就相當于裝飾模式中的Decorator棕叫,而它的子類DataInputStream林螃、BufferedInputStream、LineNumberInputStream和PushbackInputStream就相當于裝飾模式中的ConcreteDecorator了俺泣。

另外FilterInputStream和它的子類對象的構造器疗认,都是傳入組件InputStream類型,這樣就完全符合前面講述的裝飾器的結構了伏钠。

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

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

  1. 自己實現的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 {
          //先統(tǒng)一向后移動兩位
          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();
      }
}

運行一下,打開生成的文件图贸,看看結果蹂季,結果示例如下:

cdefzab

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

再試試看涕滋,不是說裝飾器可以隨意組合嗎,換一個組合方式看看净神,比如把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)先看看成功輸出流中的內容的寫法的運行過程:

當執(zhí)行到“dout.write("abcdxyz".getBytes());”這句話的時候,會調用DataOutputStream的write方法渣玲,把數據輸出到BufferedOutputStream中逗概;

由于BufferedOutputStream流是一個帶緩存的流,它默認緩存8192byte忘衍,也就是默認流中的緩存數據到了8192byte逾苫,它才會自動輸出緩存中的數據;而目前要輸出的字節(jié)肯定不到8192byte枚钓,因此數據就被緩存在BufferedOutputStream流中了铅搓,而不會被自動輸出;

當執(zhí)行到“dout.close();”這句話的時候:會調用關閉DataOutputStream流搀捷,這會轉調到傳入DataOutputStream中的流的close方法星掰,也就是BufferedOutputStream的close方法,而BufferedOutputStream的close方法繼承自FilterOutputStream嫩舟,在FilterOutputStream的close方法實現里面氢烘,會先調用輸出流的方法flush,然后關閉流至壤。也就是此時BufferedOutputStream流中緩存的數據會被強制輸出威始;

BufferedOutputStream流中緩存的數據被強制輸出到EncryptOutputStream流,也就是我們自己實現的流像街,沒有緩存黎棠,經過處理后繼續(xù)輸出;

EncryptOutputStream流會把數據輸出到FileOutputStream中镰绎,FileOutputStream會直接把數據輸出到文件中脓斩,因此,這種實現方式會輸出文件的內容畴栖。

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

當執(zhí)行到“dout.write("abcdxyz".getBytes());”這句話的時候随静,會調用DataOutputStream的write方法,把數據輸出到EncryptOutputStream中吗讶;

EncryptOutputStream流燎猛,也就是我們自己實現的流,沒有緩存照皆,經過處理后繼續(xù)輸出重绷,把數據輸出到BufferedOutputStream中;

由于BufferedOutputStream流是一個帶緩存的流膜毁,它默認緩存8192byte昭卓,也就是默認流中的緩存數據到了8192byte愤钾,它才會自動輸出緩存中的數據;而目前要輸出的字節(jié)肯定不到8192byte候醒,因此數據就被緩存在BufferedOutputStream流中了能颁,而不會被自動輸出;

當執(zhí)行到“dout.close();”這句話的時候:會調用關閉DataOutputStream流倒淫,這會轉調到傳入DataOutputStream流中的流的close方法伙菊,也就是EncryptOutputStream的close方法,而EncryptOutputStream的close方法繼承自OutputStream昌简,在OutputStream的close方法實現里面占业,是個空方法绒怨,什么都沒有做纯赎。因此,這種實現方式沒有flush流的數據南蹂,也就不會輸出文件的內容犬金,自然是一片空白了。

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

要讓我們寫的裝飾器跟其它Java中的裝飾器一樣用六剥,最合理的方案就應該是:讓我們的裝飾器繼承裝飾器的父類晚顷,也就是FilterOutputStream類,然后使用父類提供的功能來協(xié)助完成想要裝飾的功能疗疟。示例代碼如下:

public class EncryptOutputStream2  extends FilterOutputStream{
      private OutputStream os = null;
      public EncryptOutputStream2(OutputStream os){
         //調用父類的構造方法
         super(os);
      }
      public void write(int a) throws IOException {
         //先統(tǒng)一向后移動兩位
         a = a+2;
         //97是小寫的a的碼值
         if(a >= (97+26)){
             //如果大于该默,表示已經是y或者z了,減去26就回到a或者b了
             a = a-26;
         }
         //調用父類的方法
         super.write(a);
      }
}

再測試看看策彤,是不是跟其它的裝飾器一樣栓袖,可以隨便換位了呢?

3.3 裝飾模式和AOP##

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

  1. 什么是AOP——面向方面編程

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

在面向對象開發(fā)中擦囊,考慮系統(tǒng)的角度通常是縱向的违霞,比如我們經常畫出的如下的系統(tǒng)架構圖,默認都是從上到下瞬场,上層依賴于下層买鸽,如圖5所示:

系統(tǒng)架構圖示例圖

而在每個模塊內部呢?就拿大家都熟悉的三層架構來說泌类,也是從上到下來考慮的癞谒,通常是表現層調用邏輯層底燎,邏輯層調用數據層,如圖6所示:

三層架構示意圖

慢慢的弹砚,越來越多的人發(fā)現双仍,在各個模塊之中,存在一些共性的功能桌吃,比如日志管理朱沃、事務管理等等,如圖7所示:

共性功能示意圖

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

AOP為開發(fā)者提供了一種描述橫切關注點的機制失暴,并能夠自動將橫切關注點織入到面向對象的軟件系統(tǒng)中,從而實現了橫切關注點的模塊化微饥。

AOP能夠將那些與業(yè)務無關逗扒,卻為業(yè)務模塊所共同調用的邏輯或責任,例如事務處理欠橘、日志管理矩肩、權限控制等,封裝起來肃续,便于減少系統(tǒng)的重復代碼黍檩,降低模塊間的耦合度,并有利于未來的可操作性和可維護性痹升。

AOP之所以強大建炫,就是因為它能夠自動把橫切關注點的功能模塊,自動織入回到軟件系統(tǒng)中疼蛾,這是什么意思呢肛跌?

先看看沒有AOP,在常規(guī)的面向對象系統(tǒng)中察郁,對這種共性的功能如何處理衍慎,大都是把這些功能提煉出來,然后在需要用到的地方進行調用皮钠,只畫調用通用日志的公共模塊稳捆,其它的類似,就不去畫了麦轰,如圖8所示:

調用公共功能示意圖

看清楚乔夯,是從應用模塊中主動去調用公共模塊砖织,也就是應用模塊要很清楚公共模塊的功能,還有具體的調用方法才行末荐,應用模塊是依賴于公共模塊的侧纯,是耦合的,這樣一來甲脏,要想修改公共模塊就會很困難了眶熬,牽一而發(fā)百。

看看有了AOP會怎樣块请,還是畫個圖來說明娜氏,如圖9所示:

AOP的調用示意圖

乍一看,跟上面不用AOP沒有什么區(qū)別嘛墩新,真的嗎贸弥?看得仔細點,有一個非常非常大的改變抖棘,就是所有的箭頭方向反過來了茂腥,原來是應用系統(tǒng)主動去調用各個公共模塊的,現在變成了各個公共模塊主動織入回到應用系統(tǒng)切省。

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

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

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

  1. 使用裝飾模式做出類似AOP的效果

下面來演示一下使用裝飾模式肥哎,把一些公共的功能辽俗,比如權限控制,日志記錄篡诽,透明的添加回到業(yè)務功能模塊中去榆苞,做出類似AOP的效果。

(1)首先定義業(yè)務接口

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

/** 
   * 商品銷售管理的業(yè)務接口 
   */  
public interface GoodsSaleEbi {  
      /** 
       * 保存銷售信息,本來銷售數據應該是多條呈枉,太麻煩了趁尼,為了演示,簡單點 
       * @param user 操作人員 
       * @param customer 客戶 
       * @param saleModel 銷售數據 
       * @return 是否保存成功 
       */  
      public boolean sale(String user, String customer, SaleModel saleModel);  
}  

順便把封裝業(yè)務數據的對象也定義出來猖辫,很簡單酥泞,示例代碼如下:

/** 
   * 封裝銷售單的數據,簡單的示意一些 
   */  
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)定義基本的業(yè)務實現對象啃憎,示例代碼如下:

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)實現權限控制的裝飾器

先檢查是否有運行的權限,如果有就繼續(xù)調用贩毕,如果沒有悯许,就不遞歸調用了,而是輸出沒有權限的提示辉阶,示例代碼如下:

/** 
   * 實現權限控制 
   */  
public class CheckDecorator extends Decorator{  
      public CheckDecorator(GoodsSaleEbi ebi){  
          super(ebi);  
      }  
      public boolean sale(String user, String customer, SaleModel saleModel) {  
          //簡單點先壕,只讓張三執(zhí)行這個功能  
          if(!"張三".equals(user)){  
              System.out.println("對不起"+user+",你沒有保存銷售單的權限");  
              //就不再調用被裝飾對象的功能了  
              return false;  
          }else{  
              return this.ebi.sale(user,customer,saleModel);  
          }         
      }  
}  

(5)實現日志記錄的裝飾器睛藻,就是在功能執(zhí)行完成后記錄日志即可启上,示例代碼如下:

/** 
   * 實現日志記錄 
   */  
public class LogDecorator extends Decorator{  
      public LogDecorator(GoodsSaleEbi ebi){  
          super(ebi);  
      }  
      public boolean sale(String user,String customer, SaleModel saleModel) {  
          //執(zhí)行業(yè)務功能  
          boolean f = this.ebi.sale(user, customer, saleModel);  
 
          //在執(zhí)行業(yè)務功能過后,記錄日志  
          DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");  
          System.out.println("日志記錄:"+user+"于"+df.format(new Date())+"時保存了一條銷售記錄店印,客戶是"+customer+",購買記錄是"+saleModel);  
          return f;  
      }  
}

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

在組合的時候冈在,權限控制應該是最先被執(zhí)行的,所以把它組合在最外面按摘,日志記錄的裝飾器會先調用原始的業(yè)務對象包券,所以把日志記錄的裝飾器組合在中間纫谅。

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

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

public class Client {  
      public static void main(String[] args) {  
          //得到業(yè)務接口亮元,組合裝飾器  
          GoodsSaleEbi ebi = new CheckDecorator(  
                  new LogDecorator(  
                  new GoodsSaleEbo()));  
          //準備測試數據  
          SaleModel saleModel = new SaleModel();  
          saleModel.setGoods("Moto手機");  
          saleModel.setSaleNum(2);  
          //調用業(yè)務功能  
          ebi.sale("張三","張三豐", saleModel);  
          ebi.sale("李四","張三豐", saleModel);  
      }  
}
運行結果如圖

好好體會一下猛计,是不是也在沒有驚動原始業(yè)務對象的情況下,給它織入了新的功能呢爆捞?也就是說是在原始業(yè)務不知情的情況下奉瘤,給原始業(yè)務對象透明的增加了新功能,從而模擬實現了AOP的功能煮甥。

事實上盗温,這種做法,完全可以應用在項目開發(fā)上成肘,在后期為項目的業(yè)務對象添加數據檢查卖局、權限控制、日志記錄等功能艇劫,就不需要在業(yè)務對象上去處理這些功能了吼驶,業(yè)務對象可以更專注于具體業(yè)務的處理。

3.4 裝飾模式的優(yōu)缺點##

  1. 比繼承更靈活

從為對象添加功能的角度來看店煞,裝飾模式比繼承來得更靈活。繼承是靜態(tài)的风钻,而且一旦繼承是所有子類都有一樣的功能顷蟀。而裝飾模式采用把功能分離到每個裝飾器當中,然后通過對象組合的方式骡技,在運行時動態(tài)的組合功能鸣个,每個被裝飾的對象,最終有哪些功能布朦,是由運行期動態(tài)組合的功能來決定的囤萤。

  1. 更容易復用功能

裝飾模式把一系列復雜的功能,分散到每個裝飾器當中是趴,一般一個裝飾器只實現一個功能涛舍,這樣實現裝飾器變得簡單,更重要的是這樣有利于裝飾器功能的復用唆途,可以給一個對象增加多個同樣的裝飾器富雅,也可以把一個裝飾器用來裝飾不同的對象掸驱,從而復用裝飾器的功能。

  1. 簡化高層定義

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

  1. 會產生很多細粒度對象

前面說了待秃,裝飾模式是把一系列復雜的功能,分散到每個裝飾器當中瓜晤,一般一個裝飾器只實現一個功能锥余,這樣會產生很多細粒度的對象,而且功能越復雜痢掠,需要的細粒度對象越多驱犹。

3.5 思考裝飾模式##

  1. 裝飾模式的本質

裝飾模式的本質:功能細化,動態(tài)組合足画。

動態(tài)是手段雄驹,組合才是目的。這里的組合有兩個意思淹辞,一個是動態(tài)功能的組合医舆,也就是動態(tài)進行裝飾器的組合;另外一個是指對象組合象缀,通過對象組合來實現為被裝飾對象透明的增加功能蔬将。

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

總之毙石,裝飾模式是通過把復雜功能簡單化,分散化颓遏,然后在運行期間徐矩,根據需要來動態(tài)組合的這么一個模式

  1. 何時選用裝飾模式

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

如果需要在不影響其它對象的情況下滤灯,以動態(tài)、透明的方式給對象添加職責,可以使用裝飾模式力喷,這幾乎就是裝飾模式的主要功能

如果不合適使用子類來進行擴展的時候刽漂,可以考慮使用裝飾模式,因為裝飾模式是使用的“對象組合”的方式弟孟。所謂不適合用子類擴展的方式贝咙,比如:擴展功能需要的子類太多,造成子類數目呈爆炸性增長拂募。

3.6 相關模式##

  1. 裝飾模式與適配器模式

這是兩個沒有什么關聯的模式庭猩,放到一起來說,是因為它們有一個共同的別名:Wrapper陈症。

這兩個模式功能上是不一樣的蔼水,適配器模式是用來改變接口的,而裝飾模式是用來改變對象功能的录肯。

  1. 裝飾模式與組合模式

這兩個模式有相似之處趴腋,都涉及到對象的遞歸調用,從某個角度來說论咏,可以把裝飾看成是只有一個組件的組合优炬。

但是它們的目的完全不一樣,裝飾模式是要動態(tài)的給對象增加功能厅贪;而組合模式是想要管理組合對象和葉子對象蠢护,為它們提供一個一致的操作接口給客戶端,方便客戶端的使用养涮。

  1. 裝飾模式與策略模式

這兩個模式可以組合使用葵硕。

策略模式也可以實現動態(tài)的改變對象的功能,但是策略模式只是一層選擇贯吓,也就是根據策略選擇一下具體的實現類而已懈凹。而裝飾模式不是一層,而是遞歸調用悄谐,無數層都可以蘸劈,只要組合好裝飾器的對象組合,那就可以依次調用下去尊沸,所以裝飾模式會更靈活

而且策略模式改變的是原始對象的功能贤惯,不像裝飾模式洼专,后面一個裝飾器,改變的是經過前一個裝飾器裝飾過后的對象孵构,也就是策略模式改變的是對象的內核屁商,而裝飾模式改變的是對象的外殼

這兩個模式可以組合使用,可以在一個具體的裝飾器里面使用策略模式蜡镶,來選擇更具體的實現方式雾袱。比如前面計算獎金的另外一個問題就是參與計算的基數不同,獎金的計算方式也是不同的官还。舉例來說:假設張三和李四參與同一個獎金的計算芹橡,張三的銷售總額是2萬元,而李四的銷售額是8萬元望伦,它們的計算公式是不一樣的林说,假設獎金的計算規(guī)則是,銷售額在5萬以下屯伞,統(tǒng)一3%腿箩,而5萬以上,5萬內是4%劣摇,超過部分是6%珠移。

參與同一個獎金的計算,這就意味著可以使用同一個裝飾器末融,但是在裝飾器的內部钧惧,不同條件下計算公式不一樣,那么怎么選擇具體的實現策略呢滑潘?自然使用策略模式就好了垢乙,也就是裝飾模式和策略模式組合來使用。

  1. 裝飾模式與模板方法模式

這是兩個功能上有相似點的模式语卤。

模板方法模式主要應用在算法骨架固定的情況追逮,那么要是算法步驟不固定呢,也就是一個相對動態(tài)的算法步驟粹舵,就可以使用裝飾模式了钮孵,因為在使用裝飾模式的時候,進行裝飾器的組裝眼滤,其實也相當于是一個調用算法步驟的組裝巴席,相當于是一個動態(tài)的算法骨架

既然裝飾模式可以實現動態(tài)的算法步驟的組裝和調用诅需,那么把這些算法步驟固定下來漾唉,那就是模板方法模式實現的功能了,因此裝飾模式可以模擬實現模板方法模式的功能堰塌。

但是請注意赵刑,僅僅只是可以模擬功能而已,兩個模式的設計目的场刑、原本的功能般此、本質思想等都是不一樣的。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市铐懊,隨后出現的幾起案子邀桑,更是在濱河造成了極大的恐慌,老刑警劉巖科乎,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件惫谤,死亡現場離奇詭異甲棍,居然都是意外死亡号杏,警方通過查閱死者的電腦和手機薪者,發(fā)現死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來玉吁,“玉大人照弥,你說我怎么就攤上這事〗保” “怎么了这揣?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長影斑。 經常有香客問我给赞,道長,這世上最難降的妖魔是什么矫户? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任片迅,我火速辦了婚禮,結果婚禮上皆辽,老公的妹妹穿的比我還像新娘柑蛇。我一直安慰自己,他們只是感情好驱闷,可當我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布耻台。 她就那樣靜靜地躺著,像睡著了一般空另。 火紅的嫁衣襯著肌膚如雪盆耽。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天扼菠,我揣著相機與錄音摄杂,去河邊找鬼。 笑死循榆,一個胖子當著我的面吹牛匙姜,可吹牛的內容都是我干的。 我是一名探鬼主播冯痢,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了浦楣?” 一聲冷哼從身側響起袖肥,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎振劳,沒想到半個月后椎组,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡历恐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年寸癌,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片弱贼。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡蒸苇,死狀恐怖,靈堂內的尸體忽然破棺而出吮旅,到底是詐尸還是另有隱情溪烤,我是刑警寧澤,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布庇勃,位于F島的核電站檬嘀,受9級特大地震影響,放射性物質發(fā)生泄漏责嚷。R本人自食惡果不足惜鸳兽,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望罕拂。 院中可真熱鬧揍异,春花似錦、人聲如沸聂受。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蛋济。三九已至棍鳖,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間碗旅,已是汗流浹背渡处。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留祟辟,地道東北人医瘫。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像旧困,于是被迫代替她去往敵國和親醇份。 傳聞我的和親對象是個殘疾皇子稼锅,可洞房花燭夜當晚...
    茶點故事閱讀 45,060評論 2 355

推薦閱讀更多精彩內容

  • 0.提前說明 模式選擇的方法1)模式的功能——看是否能解決問題2)模式的本質——看模式是否主要用來解決這類問題3)...
    王偵閱讀 1,054評論 0 1
  • 1.初識裝飾模式 動態(tài)地給一個對象添加一些額外的職責。就增加功能來說僚纷,裝飾模式比生成子類更為靈活矩距。 Compone...
    王偵閱讀 684評論 0 0
  • 【學習難度:★★★☆☆,使用頻率:★★★☆☆】直接出處:裝飾模式梳理和學習:https://github.com/...
    BruceOuyang閱讀 727評論 2 2
  • javascript設計模式與開發(fā)實踐 設計模式 每個設計模式我們需要從三點問題入手: 定義 作用 用法與實現 單...
    穿牛仔褲的蚊子閱讀 4,063評論 0 13
  • 人間有味是清歡怖竭,最是傷心不能言锥债。 三生石畔行經處,彼岸花前焚舊箋痊臭。 縱使才大驚天人哮肚,不知生前身后全。 莫若草木花間...
    095b62ead3cd閱讀 159評論 0 0