1.初識裝飾模式
動態(tài)地給一個對象添加一些額外的職責(zé)。就增加功能來說,裝飾模式比生成子類更為靈活。
- Component:
組件對象的接口,可以給這些對象動態(tài)的添加職責(zé)。
ConcreteComponent:
具體的組件對象,實現(xiàn)組件對象接口,通常就是被裝飾器裝飾的原始對象,也就是可以給這個對象添加職責(zé)。
Decorator:
所有裝飾器的抽象父類,需要定義一個與組件接口一致的接口,并持有一個Component對象,其實就是持有一個被裝飾的對象。注意這個被裝飾的對象不一定是最原始的那個對象了,也可能是被其它裝飾器裝飾過后的對象,反正都是實現(xiàn)的同一個接口,也就是同一類型。
ConcreteDecorator:
實際的裝飾器對象,實現(xiàn)具體要向被裝飾對象添加的功能。
2.體會裝飾模式
2.1 場景問題——獎金計算
2.1.1 復(fù)雜的獎金計算
獎金計算是相對復(fù)雜的功能,尤其是對于業(yè)務(wù)部門的獎金計算方式,是非常復(fù)雜的,除了業(yè)務(wù)功能復(fù)雜外,另外一個麻煩之處是計算方式還經(jīng)常需要變動,因為業(yè)務(wù)部門經(jīng)常通過調(diào)整獎金的計算方式來激勵士氣。
先從業(yè)務(wù)上看看現(xiàn)有的獎金計算方式的復(fù)雜性:
- 1)首先是獎金分類:對于個人,大致有個人當(dāng)月業(yè)務(wù)獎金、個人累計獎金、個人業(yè)務(wù)增長獎金、及時回款獎金、限時成交加碼獎金等等;
- 2)對于業(yè)務(wù)主管或者是業(yè)務(wù)經(jīng)理,除了個人獎金外,還有:團(tuán)隊累計獎金、團(tuán)隊業(yè)務(wù)增長獎金、團(tuán)隊盈利獎金等等。
- 3)其次是計算獎金的金額,又有這么幾個基數(shù):銷售額、銷售毛利、實際回款、業(yè)務(wù)成本、獎金基數(shù)等等;
- 4)另外一個就是計算的公式,針對不同的人、不同的獎金類別、不同的計算獎金的金額,計算的公式是不同的,就算是同一個公式,里面計算的比例參數(shù)也有可能是不同的。
2.1.2 簡化后的獎金計算體系
看了上面獎金計算的問題,所幸我們只是來學(xué)習(xí)設(shè)計模式,并不是真的要去實現(xiàn)整個獎金計算體系的業(yè)務(wù),因此也沒有必要把所有的計算業(yè)務(wù)都羅列在這里,為了后面演示的需要,簡化一下,演示用的獎金計算體系如下:
- 1)每個人當(dāng)月業(yè)務(wù)獎金 = 當(dāng)月銷售額 X 3%
- 2)每個人累計獎金 = 總的回款額 X 0.1%
- 3)團(tuán)隊獎金 = 團(tuán)隊總銷售額 X 1%
2.2 不用模式的解決方案
有何問題:
對于獎金計算,光是計算方式復(fù)雜,也就罷了,不過是實現(xiàn)起來會困難
點,相對而言還是比較好解決的,不過是用程序把已有的算法表達(dá)出來。
最痛苦的是,這些獎金的計算方式,經(jīng)常發(fā)生變動,幾乎是每個季度都會
有小調(diào)整,每年都有大調(diào)整,這就要求軟件的實現(xiàn)要足夠靈活,要能夠很快進(jìn)行相應(yīng)調(diào)整和修改,否則就不能滿足實際業(yè)務(wù)的需要。
舉個簡單的例子來說,現(xiàn)在根據(jù)業(yè)務(wù)需要,需要增加一個“環(huán)比增長獎金”,就是本月的銷售額比上個月有增加,而且要達(dá)到一定的比例,當(dāng)然增長比例越高,獎金比例越大。那么軟件就必須要重新實現(xiàn)這么個功能,并正確的添加到系統(tǒng)中去。過了兩個月,業(yè)務(wù)獎勵的策略發(fā)生了變化,不再需要這個獎金了,或者是另外換了一個新的獎金方式了,那么軟件就需要把這個功能從軟件中去掉,然后再實現(xiàn)新的功能。
那么上面的要求該如何實現(xiàn)呢?
很明顯,一種方案是通過繼承來擴(kuò)展功能;另外一種方案就是到計算獎金的對象里面,添加或者刪除新的功能,并在計算獎金的時候,調(diào)用新的功能或是不調(diào)用某些去掉的功能,這種方案會嚴(yán)重違反開-閉原則。
還有一個問題,就是在運行期間,不同人員參與的獎金計算方式也是不同的,舉例來說:如果是業(yè)務(wù)經(jīng)理,除了參與個人計算部分外,還要參加團(tuán)隊獎金的計算,這就意味著需要在運行期間動態(tài)來組合需要計算的部分,也就是會有一堆的if-else。
總結(jié)一下,獎金計算面臨如下問題:
- 1)計算邏輯復(fù)雜
- 2)要有足夠靈活性,可以方便的增加或者減少功能
- 3)要能動態(tài)的組合計算方式,不同的人參與的計算不同
上面描述的獎金計算的問題,絕對沒有任何夸大成分,相反已經(jīng)簡化不少了,還有更多麻煩沒有寫上來,畢竟我們的重點在設(shè)計模式,而不是業(yè)務(wù)。
把上面的問題抽象一下,設(shè)若有一個計算獎金的對象,現(xiàn)在需要能夠靈活的給它增加和減少功能,還需要能夠動態(tài)的組合功能,每個功能就相當(dāng)于在計算獎金的某個部分。
現(xiàn)在的問題就是:如何才能夠透明的給一個對象增加功能,并實現(xiàn)功能的動態(tài)組合呢?
2.3 使用模式的解決方案
2.3.1 使用模式來解決的思路
雖然經(jīng)過簡化,業(yè)務(wù)簡單了很多,但是需要解決的問題不會少,還是要解決:要透明的給一個對象增加功能,并實現(xiàn)功能的動態(tài)組合。
所謂透明的給一個對象增加功能,換句話說就是要給一個對象增加功能,但是不能讓這個對象知道,也就是不能去改動這個對象。而實現(xiàn)了能夠給一個對象透明的增加功能,自然就能夠?qū)崿F(xiàn)功能的動態(tài)組合。
在裝飾模式的實現(xiàn)中,為了能夠和原來使用被裝飾對象的代碼實現(xiàn)無縫結(jié)合,是通過定義一個抽象類,讓這個類實現(xiàn)與被裝飾對象相同的接口,然后在具體實現(xiàn)類里面,轉(zhuǎn)調(diào)被裝飾的對象,在轉(zhuǎn)調(diào)的前后添加新的功能,這就實現(xiàn)了給被裝飾對象增加功能,這個思路跟“對象組合”非常類似。
在轉(zhuǎn)調(diào)的時候,如果覺得被裝飾的對象的功能不再需要了,還可以直接替換掉,也就是不再轉(zhuǎn)調(diào),而是在裝飾對象里面完全全新的實現(xiàn)。
2.3.2 使用模式來解決的類圖
畫個圖來說明獎金的計算過程
這個圖很好的揭示了裝飾模式的組合和調(diào)用過程,請仔細(xì)體會一下。
3.理解裝飾模式
3.1 認(rèn)識裝飾模式
3.1.1 裝飾模式的功能
裝飾模式能夠?qū)崿F(xiàn)動態(tài)的為對象添加功能,是從一個對象外部來給對象增加功能,相當(dāng)于是改變了對象的外觀。當(dāng)裝飾過后,從外部使用系統(tǒng)的角度看,就不再是使用原始的那個對象了,而是使用被一系列的裝飾器裝飾過后的對象。
這樣就能夠靈活的改變一個對象的功能,只要動態(tài)組合的裝飾器發(fā)生了改變,那么最終所得到的對象的功能也就發(fā)生了改變。
變相的還得到了另外一個好處,那就是裝飾器功能的復(fù)用,可以給一個對象多次增加同一個裝飾器,也可以用同一個裝飾器裝飾不同的對象。
3.1.2 對象組合
在面向?qū)ο笤O(shè)計中,有一條很基本的規(guī)則就是“盡量使用對象組合,而不是對象繼承”來擴(kuò)展和復(fù)用功能,裝飾模式的思考起點就是這個規(guī)則。
3.1.3 裝飾器
裝飾器實現(xiàn)了對被裝飾對象的某些裝飾功能,可以在裝飾器里面調(diào)用被裝飾對象的功能,獲取相應(yīng)的值,這其實是一種遞歸調(diào)用。
在裝飾器里不僅僅是可以給被裝飾對象增加功能,還可以根據(jù)需要選擇是否調(diào)用被裝飾對象的功能,如果不調(diào)用被裝飾對象的功能,那就變成完全重新實現(xiàn)了,相當(dāng)于動態(tài)修改了被裝飾對象的功能。
另外一點,各個裝飾器之間最好是完全獨立的功能,不要有依賴,這樣在進(jìn)行裝飾組合的時候,才沒有先后順序的限制,也就是先裝飾誰和后裝飾誰都應(yīng)該是一樣的,否則會大大降低裝飾器組合的靈活性。
3.1.4 裝飾器和組件類的關(guān)系
裝飾器是用來裝飾組件的,裝飾器一定要實現(xiàn)和組件類一致的接口,保證它們是同一個類型,并具有同一個外觀,這樣組合完成的裝飾才能夠遞歸的調(diào)用下去。
組件類是不知道裝飾器的存在的,裝飾器給組件添加功能是一種透明的包裝,組件類毫不知情。需要改變的是外部使用組件類的地方,現(xiàn)在需要使用包裝后的類,接口是一樣的,但是具體的實現(xiàn)類發(fā)生了改變。
3.1.5 退化形式
如果僅僅只是想要添加一個功能,就沒有必要再設(shè)計裝飾器的抽象類了,直接在裝飾器里面實現(xiàn)跟組件一樣的接口,然后實現(xiàn)相應(yīng)的裝飾功能就可以了。但是建議最好還是設(shè)計上裝飾器的抽象類,這樣有利于程序的擴(kuò)展。
3.2 Java中的裝飾模式應(yīng)用
3.2.1 Java中典型的裝飾模式應(yīng)用——I/O流
Java的I/O對象層次圖
查看上圖會發(fā)現(xiàn),它的結(jié)構(gòu)和裝飾模式的結(jié)構(gòu)幾乎是一樣的:
- 1)InputStream就相當(dāng)于裝飾模式中的 Component。
- 2)其實FileInputStream、ObjectInputStream、StringBufferInputStream這幾個對象是直接繼承了InputSream,還有幾個直接繼承InputStream的對象,比如: ByteArrayInputStream、PipedInputStream等。這些對象相當(dāng)于裝飾模式中的 ConcreteComponent,是可以被裝飾器裝飾的對象。
- 3)那么FilterInputStream就相當(dāng)于裝飾模式中的Decorator,而它的子類 DataInputStream、BufferedInputStream、LineNumberInputStream和 PushbackInputStream就相當(dāng)于裝飾模式中的ConcreteDecorator了。另外 FilterInputStream和它的子類對象的構(gòu)造器,都是傳入組件InputStream類型,這樣就完全符合前面講述的裝飾器的結(jié)構(gòu)了。
3.2.2 自己實現(xiàn)的I/O流的裝飾器——第一版
來個功能簡單點的,實現(xiàn)把英文加密存放吧,也談不上什么加密算法,就是把英文字母向后移動兩個位置,比如:a變成c,b變成d,以此類推,最后的y變成a,z就變成b,而且為了簡單,只處理小寫的,夠簡單的吧。
測試中可能會出現(xiàn)輸出一片空白,要把這個問題搞清楚,就需要把上面I/O流的內(nèi)部運行和基本實現(xiàn)搞明白,分開來看看具體的運行過程吧。
先看看成功輸出流中的內(nèi)容的寫法的運行過程:
- step1.當(dāng)執(zhí)行到“dout.write(”abcdxyz“.getBytes());”這句話的時候,會調(diào)用 DataOutputStream的write方法,把數(shù)據(jù)輸出到BufferedOutputStream中;
- step2.由于BufferedOutputStream流是一個帶緩存的流,它默認(rèn)緩存8192byte,也就是默認(rèn)流中的緩存數(shù)據(jù)到了8192byte,它才會自動輸出緩存中的數(shù)據(jù);
- step3.而目前要輸出的字節(jié)肯定不到8192byte,因此數(shù)據(jù)就被緩存在 BufferedOutputStream流中了,而不會被自動輸出
- step4.當(dāng)執(zhí)行到“dout.close();”這句話的時候:會調(diào)用關(guān)閉DataOutputStream流,這會轉(zhuǎn)調(diào)到傳入DataOutputStream中的流的close方法,也就是 BufferedOutputStream的close方法,而BufferedOutputStream的close方法繼承自FilterOutputStream,在FilterOutputStream的close方法實現(xiàn)里面,會先調(diào)用輸出流的方法flush,然后關(guān)閉流。也就是此時BufferedOutputStream流中緩存的數(shù)據(jù)會被強(qiáng)制輸出;
- step5.BufferedOutputStream流中緩存的數(shù)據(jù)被強(qiáng)制輸出到EncryptOutputStream流,也就我們自己實現(xiàn)的流,沒有緩存,經(jīng)過處理后繼續(xù)輸出;
- step6.EncryptOutputStream流會把數(shù)據(jù)輸出到FileOutputStream中, FileOutputStream會直接把數(shù)據(jù)輸出到文件中,因此,這種實現(xiàn)方式會輸出文件的內(nèi)容。
再來看看不能輸出流中的內(nèi)容的寫法的運行過程:
- step1.當(dāng)執(zhí)行到“dout.write(”abcdxyz“.getBytes());”這句話的時候,會調(diào)用 DataOutputStream的write方法,把數(shù)據(jù)輸出到EncryptOutputStream中;
- step2.EncryptOutputStream流,也就是我們自己實現(xiàn)的流,沒有緩存,經(jīng)過處理后繼續(xù)輸出,把數(shù)據(jù)輸出到BufferedOutputStream中;
- step3.由于BufferedOutputStream流是一個帶緩存的流,它默認(rèn)緩存8192byte,也就是默認(rèn)流中的緩存數(shù)據(jù)到了8192byte,它才會自動輸出緩存中的數(shù)據(jù);
- step4.而目前要輸出的字節(jié)肯定不到8192byte,因此數(shù)據(jù)就被緩存在BufferedOutputStream流中了,而不會被自動輸出
- step5.當(dāng)執(zhí)行到“dout.close();”這句話的時候:會調(diào)用關(guān)閉DataOutputStream流,這會轉(zhuǎn)調(diào)到傳入DataOutputStream流中的流的close方法,也就是EncryptOutputStream的 close方法,而EncryptOutputStream的close方法繼承自O(shè)utputStream,在 OutputStream的close方法實現(xiàn)里面,是個空方法,什么都沒有做。因此,這種實現(xiàn)方式?jīng)]有flush流的數(shù)據(jù),也就不會輸出文件的內(nèi)容,自然是一片空白了。
3.2.2 自己實現(xiàn)的I/O流的裝飾器——第二版
要讓我們寫的裝飾器跟其它Java中的裝飾器一樣用,最合理的方案就應(yīng)該是:讓我們的裝飾器繼承裝飾器的父類,也就是FilterOutputStream類,然后使用父類提供的功能來協(xié)助完成想要裝飾的功能。
3.3 裝飾模式和 AOP
可以使用裝飾模式做出類似AOP的效果。
3.4 裝飾模式的優(yōu)缺點
- 比繼承更靈活
- 更容易復(fù)用功能
- 簡化高層定義
- 會產(chǎn)生很多細(xì)粒度對象
4.思考裝飾模式
4.1 裝飾模式的本質(zhì)
裝飾模式的本質(zhì)是:動態(tài)組合
4.2 何時選用
- 1)如果需要在不影響其它對象的情況下,以動態(tài)、透明的方式給對象添加職責(zé),可以使用裝飾模式,這幾乎就是裝飾模式的主要功能
- 2)如果不合適使用子類來進(jìn)行擴(kuò)展的時候,可以考慮使用裝飾模式,因為裝飾模式是使用的“對象組合”的方式。所謂不適合用子類擴(kuò)展的方式,比如:擴(kuò)展功能需要的子類太多,造成子類數(shù)目呈爆炸性增長。