3.3 裝飾模式和AOP
裝飾模式和AOP在思想上有共同之處。可能有些朋友還不太瞭解AOP,下面先簡單介紹一下AOP的基礎知識。
1:什麼是AOP——面向方面編程
AOP是一種編程範式,提供從另一個角度來考慮程序結構以完善面向對象編程(OOP)。
在面向對象開發中,考慮系統的角度通常是縱向的,比如我們經常畫出的如下的系統架構圖,默認都是從上到下,上層依賴於下層,如圖5所示:
圖5 系統架構圖示例圖
而在每個模塊內部呢?就拿大家都熟悉的三層架構來說,也是從上到下來考慮的,通常是表現層調用邏輯層,邏輯層調用數據層,如圖6所示:
圖6 三層架構示意圖
慢慢的,越來越多的人發現,在各個模塊之中,存在一些共性的功能,比如日誌管理、事務管理等等,如圖7所示:
圖7 共性功能示意圖
這個時候,在思考這些共性功能的時候,是從橫向在思考問題,與通常面向對象的縱向思考角度不同,很明顯,需要有新的解決方案,這個時候AOP站出來了。
AOP爲開發者提供了一種描述橫切關注點的機制,並能夠自動將橫切關注點織入到面向對象的軟件系統中,從而實現了橫切關注點的模塊化。
AOP能夠將那些與業務無關,卻爲業務模塊所共同調用的邏輯或責任,例如事務處理、日誌管理、權限控制等,封裝起來,便於減少系統的重複代碼,降低模塊間的耦合度,並有利於未來的可操作性和可維護性。
AOP之所以強大,就是因爲它能夠自動把橫切關注點的功能模塊,自動織入回到軟件系統中,這是什麼意思呢?
先看看沒有AOP,在常規的面向對象系統中,對這種共性的功能如何處理,大都是把這些功能提煉出來,然後在需要用到的地方進行調用,只畫調用通用日誌的公共模塊,其它的類似,就不去畫了,如圖8所示:
圖8 調用公共功能示意圖
看清楚,是從應用模塊中主動去調用公共模塊,也就是應用模塊要很清楚公共模塊的功能,還有具體的調用方法才行,應用模塊是依賴於公共模塊的,是耦合的,這樣一來,要想修改公共模塊就會很困難了,牽一而發百。
看看有了AOP會怎樣,還是畫個圖來說明,如圖9所示:
圖9 AOP的調用示意圖
乍一看,跟上面不用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的功能。
事實上,這種做法,完全可以應用在項目開發上,在後期爲項目的業務對象添加數據檢查、權限控制、日誌記錄等功能,就不需要在業務對象上去處理這些功能了,業務對象可以更專注於具體業務的處理。
3.4 裝飾模式的優缺點
- 比繼承更靈活
從爲對象添加功能的角度來看,裝飾模式比繼承來得更靈活。繼承是靜態的,而且一旦繼承是所有子類都有一樣的功能。而裝飾模式採用把功能分離到每個裝飾器當中,然後通過對象組合的方式,在運行時動態的組合功能,每個被裝飾的對象,最終有哪些功能,是由運行期動態組合的功能來決定的。 - 更容易複用功能
裝飾模式把一系列複雜的功能,分散到每個裝飾器當中,一般一個裝飾器只實現一個功能,這樣實現裝飾器變得簡單,更重要的是這樣有利於裝飾器功能的複用,可以給一個對象增加多個同樣的裝飾器,也可以把一個裝飾器用來裝飾不同的對象,從而複用裝飾器的功能。 - 簡化高層定義
裝飾模式可以通過組合裝飾器的方式,給對象增添任意多的功能,因此在進行高層定義的時候,不用把所有的功能都定義出來,而是定義最基本的就可以了,可以在使用需要的時候,組合相應的裝飾器來完成需要的功能。 - 會產生很多細粒度對象
前面說了,裝飾模式是把一系列複雜的功能,分散到每個裝飾器當中,一般一個裝飾器只實現一個功能,這樣會產生很多細粒度的對象,而且功能越複雜,需要的細粒度對象越多。
3.5 思考裝飾模式
1:裝飾模式的本質
裝飾模式的本質:動態組合。
動態是手段,組合纔是目的。這裏的組合有兩個意思,一個是動態功能的組合,也就是動態進行裝飾器的組合;另外一個是指對象組合,通過對象組合來實現爲被裝飾對象透明的增加功能。
但是要注意,裝飾模式不僅僅可以增加功能,也可以控制功能的訪問,可以完全實現新的功能,還可以控制裝飾的功能是在被裝飾功能之前還是之後來運行等。
總之,裝飾模式是通過把複雜功能簡單化,分散化,然後在運行期間,根據需要來動態組合的這麼一個模式。
2:何時選用裝飾模式
建議在如下情況中,選用裝飾模式:
- 如果需要在不影響其它對象的情況下,以動態、透明的方式給對象添加職責,可以使用裝飾模式,這幾乎就是裝飾模式的主要功能
- 如果不合適使用子類來進行擴展的時候,可以考慮使用裝飾模式,因爲裝飾模式是使用的“對象組合”的方式。所謂不適合用子類擴展的方式,比如:擴展功能需要的子類太多,造成子類數目呈爆炸性增長。
3.6 相關模式
- 裝飾模式與適配器模式
這是兩個沒有什麼關聯的模式,放到一起來說,是因爲它們有一個共同的別名:Wrapper。
這兩個模式功能上是不一樣的,適配器模式是用來改變接口的,而裝飾模式是用來改變對象功能的。 - 裝飾模式與組合模式
這兩個模式有相似之處,都涉及到對象的遞歸調用,從某個角度來說,可以把裝飾看成是隻有一個組件的組合。
但是它們的目的完全不一樣,裝飾模式是要動態的給對象增加功能;而組合模式是想要管理組合對象和葉子對象,爲它們提供一個一致的操作接口給客戶端,方便客戶端的使用。 - 裝飾模式與策略模式
這兩個模式可以組合使用。
策略模式也可以實現動態的改變對象的功能,但是策略模式只是一層選擇,也就是根據策略選擇一下具體的實現類而已。而裝飾模式不是一層,而是遞歸調用,無數層都可以,只要組合好裝飾器的對象組合,那就可以依次調用下去,所以裝飾模式會更靈活。
而且策略模式改變的是原始對象的功能,不像裝飾模式,後面一個裝飾器,改變的是經過前一個裝飾器裝飾過後的對象,也就是策略模式改變的是對象的內核,而裝飾模式改變的是對象的外殼。
這兩個模式可以組合使用,可以在一個具體的裝飾器裏面使用策略模式,來選擇更具體的實現方式。比如前面計算獎金的另外一個問題就是參與計算的基數不同,獎金的計算方式也是不同的。舉例來說:假設張三和李四參與同一個獎金的計算,張三的銷售總額是2萬元,而李四的銷售額是8萬元,它們的計算公式是不一樣的,假設獎金的計算規則是,銷售額在5萬以下,統一3%,而5萬以上,5萬內是4%,超過部分是6%。
參與同一個獎金的計算,這就意味着可以使用同一個裝飾器,但是在裝飾器的內部,不同條件下計算公式不一樣,那麼怎麼選擇具體的實現策略呢?自然使用策略模式就好了,也就是裝飾模式和策略模式組合來使用。 - 裝飾模式與模板方法模式
這是兩個功能上有相似點的模式。
模板方法模式主要應用在算法骨架固定的情況,那麼要是算法步驟不固定呢,也就是一個相對動態的算法步驟,就可以使用裝飾模式了,因爲在使用裝飾模式的時候,進行裝飾器的組裝,其實也相當於是一個調用算法步驟的組裝,相當於是一個動態的算法骨架。
既然裝飾模式可以實現動態的算法步驟的組裝和調用,那麼把這些算法步驟固定下來,那就是模板方法模式實現的功能了,因此裝飾模式可以模擬實現模板方法模式的功能。
但是請注意,僅僅只是可以模擬功能而已,兩個模式的設計目的、原本的功能、本質思想等都是不一樣的。
裝飾模式結束,謝謝觀賞