#0 系列目錄#算法
#1 場景問題# ##1.1 複雜的獎金計算## 考慮這樣一個實際應用:就是如何實現靈活的獎金計算。數據庫
獎金計算是相對複雜的功能,尤爲是對於業務部門的獎金計算方式,是很是複雜的,除了業務功能複雜外,另一個麻煩之處是計算方式還常常須要變更,由於業務部門常常經過調整獎金的計算方式來激勵士氣。編程
先從業務上看看現有的獎金計算方式的複雜性:設計模式
首先是獎金分類:對於我的,大體有我的當月業務獎金、我的累計獎金、我的業務增加獎金、及時回款獎金、限時成交加碼獎金等等;緩存
對於業務主管或者是業務經理,除了我的獎金外,還有:團隊累計獎金、團隊業務增加獎金、團隊盈利獎金等等。架構
其次是計算獎金的金額,又有這麼幾個基數:銷售額、銷售毛利、實際回款、業務成本、獎金基數等等;app
另一個就是計算的公式,針對不一樣的人、不一樣的獎金類別、不一樣的計算獎金的金額,計算的公式是不一樣的,就算是同一個公式,裏面計算的比例參數也有多是不一樣的。模塊化
##1.2 簡化後的獎金計算體系## 看了上面獎金計算的問題,所幸咱們只是來學習設計模式,並非真的要去實現整個獎金計算體系的業務,所以也沒有必要把全部的計算業務都羅列在這裏,爲了後面演示的須要,簡化一下,演示用的獎金計算體系以下:學習
每一個人當月業務獎金 = 當月銷售額 X 3%測試
每一個人累計獎金 = 總的回款額 X 0.1%
團隊獎金 = 團隊總銷售額 X 1%
##1.3 不用模式的解決方案## 一我的的獎金分紅不少個部分,要實現獎金計算,主要就是要按照各個獎金計算的規則,把這我的能夠獲取的每部分獎金計算出來,而後計算一個總和,這就是這我的能夠獲得的獎金。
/** * 在內存中模擬數據庫,準備點測試數據,好計算獎金 */ 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); } }
/** * 計算獎金的對象 */ 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; } }
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); } }
測試運行的結果以下:
張三當月業務獎金300.0 張三累計獎金1000.0 ==========張三應得獎金:1300.0 李四當月業務獎金600.0 李四累計獎金1000.0 ==========李四應得獎金:1600.0 王五當月業務獎金900.0 王五累計獎金1000.0 王五當月團隊業務獎金600.0 ==========王經理應得獎金:2500.0
##1.4 有何問題## 看了上面的實現,挺簡單的嘛,就是計算方式麻煩點,每一個規則都要實現。真的很簡單嗎?仔細想一想,有沒有什麼問題?
對於獎金計算,光是計算方式複雜,也就罷了,不過是實現起來會困難點,相對而言仍是比較好解決的,不過是用程序把已有的算法表達出來。
最痛苦的是,這些獎金的計算方式,常常發生變更,幾乎是每一個季度都會有小調整,每一年都有大調整,這就要求軟件的實現要足夠靈活,要可以很快進行相應調整和修改,不然就不能知足實際業務的須要。
舉個簡單的例子來講,如今根據業務須要,須要增長一個「環比增加獎金」,就是本月的銷售額比上個月有增長,並且要達到必定的比例,固然增加比例越高,獎金比例越大。那麼軟件就必需要從新實現這麼個功能,並正確的添加到系統中去。過了兩個月,業務獎勵的策略發生了變化,再也不須要這個獎金了,或者是另外換了一個新的獎金方式了,那麼軟件就須要把這個功能從軟件中去掉,而後再實現新的功能。
那麼上面的要求該如何實現呢?
很明顯,一種方案是經過繼承來擴展功能
;另一種方案就是到計算獎金的對象裏面,添加或者刪除新的功能,並在計算獎金的時候,調用新的功能或是不調用某些去掉的功能,這種方案會嚴重違反開-閉原則
。
還有一個問題,就是在運行期間,不一樣人員參與的獎金計算方式也是不一樣的
,舉例來講:若是是業務經理,除了參與我的計算部分外,還要參加團隊獎金的計算,這就意味着須要在運行期間動態來組合須要計算的部分,也就是會有一堆的if-else。
總結一下,獎金計算面臨以下問題:
(1)計算邏輯複雜
(2)要有足夠靈活性,能夠方便的增長或者減小功能
(3)要能動態的組合計算方式,不一樣的人蔘與的計算不一樣
上面描述的獎金計算的問題,絕對沒有任何誇大成分,相反已經簡化很多了,還有更多麻煩沒有寫上來,畢竟咱們的重點在設計模式,而不是業務。
把上面的問題抽象一下,設如有一個計算獎金的對象,如今須要可以靈活的給它增長和減小功能,還須要可以動態的組合功能,每一個功能就至關於在計算獎金的某個部分。
如今的問題就是:如何纔可以透明的給一個對象增長功能,並實現功能的動態組合呢?
#2 解決方案# ##2.1 裝飾模式來解決## 用來解決上述問題的一個合理的解決方案,就是使用裝飾模式。那麼什麼是裝飾模式呢?
雖然通過簡化,業務簡單了不少,可是須要解決的問題不會少,仍是要解決:要透明的給一個對象增長功能,並實現功能的動態組合
。
所謂透明的給一個對象增長功能,換句話說就是要給一個對象增長功能,可是不能讓這個對象知道,也就是不能去改動這個對象
。而實現了可以給一個對象透明的增長功能,天然就可以實現功能的動態組合
,好比原來的對象有A功能,如今透明的給它增長了一個B功能,是否是就至關於動態組合了A和B功能呢。
要想實現透明的給一個對象增長功能,也就是要擴展對象的功能了,使用繼承啊,有人立刻提出了一個方案,但很快就被否決了,那要減小或者修改功能呢?事實上繼承是很是不靈活的複用方式
。那就用「對象組合」嘛,又有人提出新的方案來了,這個方案獲得了你們的贊同。
在裝飾模式的實現中,爲了可以和原來使用被裝飾對象的代碼實現無縫結合,是經過定義一個抽象類,讓這個類實現與被裝飾對象相同的接口,而後在具體實現類裏面,轉調被裝飾的對象,在轉調的先後添加新的功能,這就實現了給被裝飾對象增長功能,這個思路跟「對象組合」很是相似
。若是對「對象組合」不熟悉,請參見3.1的第2小節。 在轉調的時候,若是以爲被裝飾的對象的功能再也不須要了,還能夠直接替換掉,也就是再也不轉調,而是在裝飾對象裏面徹底全新的實現。
##2.2 模式結構和說明## 裝飾模式的結構如圖1所示:
Component:組件對象的接口,能夠給這些對象動態的添加職責。
ConcreteComponent:具體的組件對象,實現組件對象接口,一般就是被裝飾器裝飾的原始對象,也就是能夠給這個對象添加職責。
Decorator:全部裝飾器的抽象父類,須要定義一個與組件接口一致的接口,並持有一個Component對象,其實就是持有一個被裝飾的對象。注意,這個被裝飾的對象不必定是最原始的那個對象了,也多是被其它裝飾器裝飾事後的對象,反正都是實現的同一個接口,也就是同一類型。
ConcreteDecorator:實際的裝飾器對象,實現具體要向被裝飾對象添加的功能。
##2.3 裝飾模式示例代碼##
/** * 組件對象的接口,能夠給這些對象動態的添加職責 */ public abstract class Component { /** * 示例方法 */ public abstract void operation(); }
/** * 具體實現組件對象接口的對象 */ public class ConcreteComponent extends Component { public void operation() { //相應的功能處理 } }
/** * 裝飾器接口,維持一個指向組件對象的接口對象,並定義一個與組件接口一致的接口 */ public abstract class Decorator extends Component { /** * 持有組件對象 */ protected Component component; /** * 構造方法,傳入組件對象 * @param component 組件對象 */ public Decorator(Component component) { this.component = component; } public void operation() { //轉發請求給組件對象,能夠在轉發先後執行一些附加動做 component.operation(); } }
一個示意了添加狀態,一個示意了添加職責
。先看添加了狀態的示意對象吧,示例代碼以下:/** * 裝飾器的具體實現對象,向組件對象添加職責 */ 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(); } }
##2.4 使用裝飾模式重寫示例## 看完了裝飾模式的基本知識,該來考慮如何使用裝飾模式重寫前面的示例了。要使用裝飾模式來重寫前面的示例,大體會有以下改變:
首先須要定義一個組件對象的接口,在這個接口裏面定義計算獎金的業務方法,由於外部就是使用這個接口來操做裝飾模式構成的對象結構中的對象
須要添加一個基本的實現組件接口的對象,可讓它返回獎金爲0就能夠了
把各個計算獎金的規則看成裝飾器對象,須要爲它們定義一個統一的抽象的裝飾器對象,好約束各個具體的裝飾器的接口
把各個計算獎金的規則實現成爲具體的裝飾器對象
先看看如今示例的總體結構,好總體理解和把握示例,如圖2所示:
在計算獎金的組件接口中,須要定義本來的業務方法,也就是實現獎金計算的方法,示例代碼以下:
/** * 計算獎金的組件接口 */ 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; } }
在進一步定義裝飾器以前,先定義出各個裝飾器公共的父類,在這裏定義全部裝飾器對象須要實現的方法
。這個父類應該實現組件的接口,這樣才能保證裝飾後的對象仍然能夠繼續被裝飾
。示例代碼以下:
/** * 裝飾器的接口,須要跟被裝飾的對象實現一樣的接口 */ 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); } }
用一個具體的裝飾器對象,來實現一條計算獎金的規則,如今有三條計算獎金的規則,那就對應有三個裝飾器對象來實現,依次來看看它們的實現。
這些裝飾器涉及到的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; } }
使用裝飾器的客戶端,首先須要建立被裝飾的對象,而後建立須要的裝飾對象,接下來重要的工做就是組合裝飾對象
,依次對前面的對象進行裝飾。
有不少相似的例子,好比生活中的裝修,就拿裝飾牆壁來講吧:沒有裝飾前是原始的磚牆,這就比如是被裝飾的對象,首先須要刷膩子,把牆找平,這就比如對原始的磚牆進行了一次裝飾,而刷的膩子就比如是一個裝飾器對象;好了,裝飾一回了,接下來該刷牆面漆了,這又比如裝飾了一回,刷的牆面漆就比如是又一個裝飾器對象,並且這回被裝飾的對象不是原始的磚牆了,而是被膩子裝飾器裝飾事後的牆面,也就是說後面的裝飾器是在前面的裝飾器裝飾事後的基礎之上,繼續裝飾的,相似於一層一層疊加的功能。
一樣的道理,計算獎金也是這樣,先建立基本的獎金對象,而後組合須要計算的獎金類型,依次組合計算,最後的結果就是總的獎金。示例代碼以下:
/** * 使用裝飾模式的客戶端 */ 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); } }
測試一下,看看結果,示例以下:
張三當月業務獎金300.0 張三累計獎金1000.0 ==========張三應得獎金:1300.0 李四當月業務獎金600.0 李四累計獎金1000.0 ==========李四應得獎金:1600.0 王五當月業務獎金900.0 王五累計獎金1000.0 王五當月團隊業務獎金600.0 ==========王經理應得獎金:2500.0
當測試運行的時候會按照裝飾器的組合順序,依次調用相應的裝飾器來執行業務功能,是一個遞歸的調用方法,以業務經理「王五」的獎金計算作例子,畫個圖來講明獎金的計算過程吧,看看是如何調用的,如圖3所示:
這個圖很好的揭示了裝飾模式的組合和調用過程,請仔細體會一下。
如同上面的示例,對於基本的計算獎金的對象而言,因爲計算獎金的邏輯太過於複雜,並且須要在不一樣的狀況下進行不一樣的運算,爲了靈活性,把多種計算獎金的方式分散到不一樣的裝飾器對象裏面,採用動態組合的方式,來給基本的計算獎金的對象增添計算獎金的功能,每一個裝飾器至關於計算獎金的一個部分
。
這種方式明顯比爲基本的計算獎金的對象增長子類來得更靈活,由於裝飾模式的起源點是採用對象組合的方式,而後在組合的時候順便增長些功能。爲了達到一層一層組裝的效果,裝飾模式還要求裝飾器要實現與被裝飾對象相同的業務接口,這樣才能以同一種方式依次組合下去
。
靈活性還體如今動態上,若是是繼承的方式,那麼全部的類實例都有這個功能了,而採用裝飾模式,能夠動態的爲某幾個對象實例添加功能,而不是對整個類添加功能
。好比上面示例中,客戶端測試的時候,對張三李四就只是組合了兩個功能,對王五就組合了三個功能,可是原始的計算獎金的類都是同樣的,只是動態的爲它增長的功能不一樣而已。
#3 模式講解# ##3.1 認識裝飾模式##
裝飾模式可以實現動態的爲對象添加功能,是從一個對象外部來給對象增長功能,至關因而改變了對象的外觀
。當裝飾事後,從外部使用系統的角度看,就再也不是使用原始的那個對象了,而是使用被一系列的裝飾器裝飾事後的對象。
這樣就可以靈活的改變一個對象的功能,只要動態組合的裝飾器發生了改變,那麼最終所獲得的對象的功能也就發生了改變
。
變相的還獲得了另一個好處,那就是裝飾器功能的複用,能夠給一個對象屢次增長同一個裝飾器,也能夠用同一個裝飾器裝飾不一樣的對象
。
前面已經講到了,一個類的功能的擴展方式,能夠是繼承,也能夠是功能更強大、更靈活的對象組合的方式
。
其實,如今在面向對象設計中,有一條很基本的規則就是「儘可能使用對象組合,而不是對象繼承」來擴展和複用功能
。裝飾模式的思考起點就是這個規則,可能有些朋友還不太熟悉什麼是「對象組合」,下面介紹一下「對象組合」。
什麼是對象組合
直接舉例來講吧,倘若有一個對象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對象是否是透明的呢?對象組合是否是也很簡單,並且更靈活了
:
首先能夠
有選擇的複用功能,不是全部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.2 Java中的裝飾模式應用##
裝飾模式在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的部分,並且因爲圖的大小關係,也只是表現出了部分的流,具體如圖4所示:
查看上圖會發現,它的結構和裝飾模式的結構幾乎是同樣的:
InputStream就至關於裝飾模式中的Component。
其實FileInputStream、ObjectInputStream、StringBufferInputStream這幾個對象是直接繼承了InputStream,還有幾個直接繼承InputStream的對象,好比:ByteArrayInputStream、PipedInputStream等。這些對象至關於裝飾模式中的ConcreteComponent,是能夠被裝飾器裝飾的對象。
那麼FilterInputStream就至關於裝飾模式中的Decorator,而它的子類DataInputStream、BufferedInputStream、LineNumberInputStream和PushbackInputStream就至關於裝飾模式中的ConcreteDecorator了。
另外FilterInputStream和它的子類對象的構造器,都是傳入組件InputStream類型,這樣就徹底符合前面講述的裝飾器的結構了。
一樣的,輸出流部分也相似,就不去贅述了。
既然I/O流部分是採用裝飾模式實現的,也就是說,若是咱們想要添加新的功能的話,只須要實現新的裝飾器,而後在使用的時候,組合進去就能夠了,也就是說,咱們能夠自定義一個裝飾器,而後和JDK中已有的流的裝飾器一塊兒使用
。能行嗎?試試看吧,前面是按照輸入流來說述的,下面的示例按照輸出流來作,順便體會一下Java的輸入流和輸出流在結構上的類似性。
來個功能簡單點的,實現把英文加密存放吧,也談不上什麼加密算法,就是把英文字母向後移動兩個位置,好比: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(); } }
運行一下,打開生成的文件,看看結果,結果示例以下:
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)先看當作功輸出流中的內容的寫法的運行過程:
當執行到「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流的數據,也就不會輸出文件的內容,天然是一片空白了。
要讓咱們寫的裝飾器跟其它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); } }
再測試看看,是否是跟其它的裝飾器同樣,能夠隨便換位了呢?
##3.3 裝飾模式和AOP## 裝飾模式和AOP在思想上有共同之處。可能有些朋友還不太瞭解AOP,下面先簡單介紹一下AOP的基礎知識。
AOP是一種編程範式,提供從另外一個角度來考慮程序結構以完善面向對象編程(OOP)。
在面向對象開發中,考慮系統的角度一般是縱向的,好比咱們常常畫出的以下的系統架構圖,默認都是從上到下,上層依賴於下層,如圖5所示:
而在每一個模塊內部呢?就拿你們都熟悉的三層架構來講,也是從上到下來考慮的,一般是表現層調用邏輯層,邏輯層調用數據層,如圖6所示:
慢慢的,愈來愈多的人發現,在各個模塊之中,存在一些共性的功能,好比日誌管理、事務管理等等,如圖7所示:
這個時候,在思考這些共性功能的時候,是從橫向在思考問題,與一般面向對象的縱向思考角度不一樣
,很明顯,須要有新的解決方案,這個時候AOP站出來了。
AOP爲開發者提供了一種描述橫切關注點的機制,並可以自動將橫切關注點織入到面向對象的軟件系統中,從而實現了橫切關注點的模塊化。
AOP可以將那些與業務無關,卻爲業務模塊所共同調用的邏輯或責任,例如事務處理、日誌管理、權限控制等,封裝起來,便於減小系統的重複代碼,下降模塊間的耦合度,並有利於將來的可操做性和可維護性。
AOP之因此強大,就是由於它可以自動把橫切關注點的功能模塊,自動織入回到軟件系統中,這是什麼意思呢?
先看看沒有AOP,在常規的面向對象系統中,對這種共性的功能如何處理,大都是把這些功能提煉出來,而後在須要用到的地方進行調用,只畫調用通用日誌的公共模塊,其它的相似,就不去畫了,如圖8所示:
看清楚,是從應用模塊中主動去調用公共模塊,也就是應用模塊要很清楚公共模塊的功能,還有具體的調用方法才行,應用模塊是依賴於公共模塊的,是耦合的,這樣一來,要想修改公共模塊就會很困難了,牽一而發百。
看看有了AOP會怎樣,仍是畫個圖來講明,如圖9所示:
乍一看,跟上面不用AOP沒有什麼區別嘛,真的嗎?看得仔細點,有一個很是很是大的改變,就是全部的箭頭方向反過來了,原來是應用系統主動去調用各個公共模塊的,如今變成了各個公共模塊主動織入回到應用系統
。
不要小看這一點變化,這樣一來應用系統就不須要知道公共功能模塊,也就是應用系統和公共功能解耦了
。公共功能會在合適的時候,由外部織入回到應用系統中,至於誰來實現這樣的功能,以及如何實現再也不咱們的討論之列,咱們更關注這個思想。
若是按照裝飾模式來對比上述過程,業務功能對象就能夠被看做是被裝飾的對象,而各個公共的模塊就比如是裝飾器,能夠透明的來給業務功能對象增長功能。
因此從某個側面來講,裝飾模式和AOP要實現的功能是相似的,只不過AOP的實現方法不一樣,會更加靈活,更加可配置;另外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的功能。
事實上,這種作法,徹底能夠應用在項目開發上,在後期爲項目的業務對象添加數據檢查、權限控制、日誌記錄等功能,就不須要在業務對象上去處理這些功能了,業務對象能夠更專一於具體業務的處理。
##3.4 裝飾模式的優缺點##
從爲對象添加功能的角度來看,裝飾模式比繼承來得更靈活。繼承是靜態的,並且一旦繼承是全部子類都有同樣的功能
。而裝飾模式採用把功能分離到每一個裝飾器當中,而後經過對象組合的方式,在運行時動態的組合功能,每一個被裝飾的對象,最終有哪些功能,是由運行期動態組合的功能來決定的
。
裝飾模式把一系列複雜的功能,分散到每一個裝飾器當中,通常一個裝飾器只實現一個功能,這樣實現裝飾器變得簡單,更重要的是這樣有利於裝飾器功能的複用
,能夠給一個對象增長多個一樣的裝飾器,也能夠把一個裝飾器用來裝飾不一樣的對象,從而複用裝飾器的功能。
裝飾模式能夠經過組合裝飾器的方式,給對象增添任意多的功能
,所以在進行高層定義的時候,不用把全部的功能都定義出來,而是定義最基本的就能夠了,能夠在使用須要的時候,組合相應的裝飾器來完成須要的功能。
前面說了,裝飾模式是把一系列複雜的功能,分散到每一個裝飾器當中,通常一個裝飾器只實現一個功能,這樣會產生不少細粒度的對象,並且功能越複雜,須要的細粒度對象越多
。
##3.5 思考裝飾模式##
裝飾模式的本質:動態組合。
動態是手段,組合纔是目的
。這裏的組合有兩個意思,一個是動態功能的組合,也就是動態進行裝飾器的組合;另一個是指對象組合,經過對象組合來實現爲被裝飾對象透明的增長功能。
可是要注意,裝飾模式不只僅能夠增長功能,也能夠控制功能的訪問,能夠徹底實現新的功能,還能夠控制裝飾的功能是在被裝飾功能以前仍是以後來運行等。
總之,裝飾模式是經過把複雜功能簡單化,分散化,而後在運行期間,根據須要來動態組合的這麼一個模式
。
建議在以下狀況中,選用裝飾模式:
若是須要在不影響其它對象的狀況下,以動態、透明的方式給對象添加職責,可使用裝飾模式,這幾乎就是裝飾模式的主要功能
若是不合適使用子類來進行擴展的時候,能夠考慮使用裝飾模式,由於裝飾模式是使用的「對象組合」的方式。所謂不適合用子類擴展的方式,好比:擴展功能須要的子類太多,形成子類數目呈爆炸性增加。
##3.6 相關模式##
這是兩個沒有什麼關聯的模式,放到一塊兒來講,是由於它們有一個共同的別名:Wrapper。
這兩個模式功能上是不同的,適配器模式是用來改變接口的,而裝飾模式是用來改變對象功能的
。
這兩個模式有類似之處,都涉及到對象的遞歸調用,從某個角度來講,能夠把裝飾當作是隻有一個組件的組合
。
可是它們的目的徹底不同,裝飾模式是要動態的給對象增長功能
;而組合模式是想要管理組合對象和葉子對象,爲它們提供一個一致的操做接口給客戶端,方便客戶端的使用
。
這兩個模式能夠組合使用。
策略模式也能夠實現動態的改變對象的功能,可是策略模式只是一層選擇,也就是根據策略選擇一下具體的實現類而已
。而裝飾模式不是一層,而是遞歸調用,無數層均可以,只要組合好裝飾器的對象組合,那就能夠依次調用下去,因此裝飾模式會更靈活
。
並且策略模式改變的是原始對象的功能
,不像裝飾模式,後面一個裝飾器,改變的是通過前一個裝飾器裝飾事後的對象,也就是策略模式改變的是對象的內核,而裝飾模式改變的是對象的外殼
。
這兩個模式能夠組合使用,能夠在一個具體的裝飾器裏面使用策略模式,來選擇更具體的實現方式
。好比前面計算獎金的另一個問題就是參與計算的基數不一樣,獎金的計算方式也是不一樣的。舉例來講:假設張三和李四參與同一個獎金的計算,張三的銷售總額是2萬元,而李四的銷售額是8萬元,它們的計算公式是不同的,假設獎金的計算規則是,銷售額在5萬如下,統一3%,而5萬以上,5萬內是4%,超過部分是6%。
參與同一個獎金的計算,這就意味着可使用同一個裝飾器,可是在裝飾器的內部,不一樣條件下計算公式不同,那麼怎麼選擇具體的實現策略呢?天然使用策略模式就行了,也就是裝飾模式和策略模式組合來使用。
這是兩個功能上有類似點的模式。
模板方法模式主要應用在算法骨架固定的狀況,那麼要是算法步驟不固定呢,也就是一個相對動態的算法步驟,就可使用裝飾模式了,由於在使用裝飾模式的時候,進行裝飾器的組裝,其實也至關因而一個調用算法步驟的組裝,至關因而一個動態的算法骨架
。
既然裝飾模式能夠實現動態的算法步驟的組裝和調用,那麼把這些算法步驟固定下來,那就是模板方法模式實現的功能了,所以裝飾模式能夠模擬實現模板方法模式的功能
。
可是請注意,僅僅只是能夠模擬功能而已,兩個模式的設計目的、本來的功能、本質思想等都是不同的。