設計模式系列10--裝飾者模式

西木柚子發表於2016-12-14

設計模式系列10--裝飾者模式
image

大部分公司都有銷售團隊,假設老闆給你佈置了一個任務,讓你按照下面的要求開發一套程式來計算銷售團隊每個月的工資。

  • 每個人當月業務獎金 = 當月銷售額 * 3%
  • 每個人的累積獎金 = 總的回款額 * 0.1%
  • 銷售經理的團隊獎金 = 團隊總銷售額 * 1%

每個人的工資就是基本工資加上獎金,那麼按照常規模式我們來看下如何讓實現。

#import "calculateBonus.h"

@implementation calculateBonus

-(NSInteger)calculateSalary:(NSInteger)monthSales  sumSales:(NSInteger)sumSales isManager:(BOOL)manager{
    //基本工資都是8000
    NSInteger salary = 8000;
    salary += [self monthBonus:monthSales];
    salary += [self sumBonus:sumSales];
    if (manager) {
        salary += [self groupBonus];
    }

    return salary;

}


//當月獎金
-(NSInteger)monthBonus:(NSInteger)monthSales{
    return monthSales * 0.003;
}


//累積獎金
-(NSInteger)sumBonus:(NSInteger)sumSales{
    return  sumSales * 0.001;
}

//團隊獎金
-(NSInteger)groupBonus{
    //簡單起見,團隊的銷售總額設定為100000
    return 100000 * 0.01;
}
@end複製程式碼

測試下:

calculateBonus *calculate = [calculateBonus new];
NSInteger salary1 = [calculate calculateSalary:12222 sumSales:12000 isManager:YES];
NSLog(@"經理工資:%zd", salary1);

NSInteger salary2 = [calculate calculateSalary:21333 sumSales:23111 isManager:NO];
NSLog(@"員工甲:%zd", salary2);

NSInteger salary3 = [calculate calculateSalary:22113 sumSales:11222 isManager:NO];
NSLog(@"員工乙:%zd", salary3);複製程式碼

輸出:

2016-12-14 08:57:58.600 裝飾者模式[64313:1880733] 經理工資:9048
2016-12-14 08:57:58.601 裝飾者模式[64313:1880733] 員工甲:8086
2016-12-14 08:57:58.601 裝飾者模式[64313:1880733] 員工乙:8077
Program ended with exit code: 0複製程式碼

看起來執行良好,好,該來的還是來了,該需求。

現在要增加一個環比獎金:就是本月銷售額比上月又增加,然後達到一定比例,就會有獎金,增加越多,獎金比率越高。你說這還不簡單,再加一個環比獎金計算方法不就是了。那麼如果工資計算的方式也換了呢?不同級別的人員或者員工的計算獎金的方式也換了呢?

假設

  • 甲的總工資 = 基本工資 + 當月銷售獎金 + 環比獎金

  • 乙的工資 = 基本工資 + 環比獎金

  • 丙的工資 = 基本工資 + 當月銷售獎金

  • 丁的工資 = 基本工資 + 環比獎金 + 團隊獎金

後面再給你制定十幾種不同的計算方式,崩潰了沒有?按照上面的寫法,那麼每一種總工資計算方式你都要寫一種方法,再假設這些人的總工資計算方式每個季度還會有調整,你怎麼辦,接著改?

仔細分析下上面的需求,總工資的計算有兩部分:基本工資加上各種獎金,基本工資是固定的,麻煩的地方就在於獎金的計算方式是動態變化的,有各種各樣的組合方式。按照設計模式的思想:封裝變化,這裡變化的部分是獎金的組合方式,那麼我們就把這部分封裝起來。

我們可以採取這樣的方式,把每張獎金的計算方式都單獨成類,然後需要用到哪種獎金計算,就把這個獎金計算和基本工資組合起來,需要多少種獎金計算方式,就組合多少種。這樣實現起來,是不是非常靈活?以後你想修改或者增加減少獎金計算方式,只需要修改或者增加減少一個獎金計算方式就可以了,至於每個人的總工資計算方式各不相同,就更簡單了,交給客戶端自由組合。

總結起來就是如下三個要求:

  • 獎金計算方式要靈活,可以動態增加或者減少

  • 可以動態組合獎金計算方式

要實現上面的功能,就要清楚我們今天的豬腳:裝飾器模式。

下面來認識下這位仁兄


定義

動態地給一個物件新增一些額外的職責。就增加功能來說, 裝飾模式比生成子類更為靈活。

一般我們在給一個原本的類新增功能的時候,都會想到使用繼承,在原有的類上擴充套件新的功能,但是繼承有一個非常大的缺點:和父類耦合性太高。如果後續需要新增或者減少功能,就不得不每次都要修改子類,而且如果修改了父類,對子類的影響也非常大。

所以我們一般優先考慮使用組合來實現功能的擴充套件,這也是設計模式的一個原則:多用組合,少用繼承。裝飾器模式就是實現組合功能的一種方式,它可以透明的給原本的類增加或者減少功能,而且可以把多個功能組合在一起,他不會改變原有類的功能,只是在原來功能的基礎上加上一些新功能,而這些操作被裝飾物件是毫不知情的。

比如上面的計算總工資,原有的物件是基本工資,但是需要在基本工資的基礎上加上各種獎金,也就是給基本工資擴充套件了功能,但是基本工資這個原有功能是不會改變的,只是給他加上了各種各樣的獎金,豐富了它的功能,最後算出來的還是工資,也就是保持原有的型別(整數型)不改變,這點要切記。


UML結構如及說明

設計模式系列10--裝飾者模式
image

實現裝飾器模式要注意如下幾點:

1.介面的一致性

裝飾物件的介面必須與它所裝飾的Component的介面是一致的,因此,所有的concreteDecorator類必須有一個公共的父類

2.省略抽象的Decorator類

當你僅需要新增一個職責時,沒有必要定義抽象Decorator類。你常常需要處理現存的類層次結構而不是設計一個新系統,這時你可以把Decorator向Component轉發請求的職責合併到ConcreteDecorator中。

3.保持Component類的簡單性

為了保證介面的一致性,元件和裝飾必須有一個公共的Component父類。因此保持這個類的簡單性是很重要的;即,它應集中於定義介面而不是儲存資料。對資料表示的定義應延遲到子類中,否則Component類會變得過於複雜和龐大,因而難以大量使用。賦予Component太多的功能也使得,具體的子類有一些它們並不需要的功能的可能性大大增加。

4.改變物件外殼與改變物件核心

我們可以將Decorator看作一個物件的外殼,它可以改變這個物件的行為。另外一種方法是改變物件的核心。例如,Strategy模式就是一個用於改變核心的很好的模式。
當Component類原本就很龐大時,使用Decorator模式代價太高,Strategy模式相對更好一些。在Strategy模式中,元件將它的一些行為轉發給一個獨立的策略物件,我們可以替換strategy物件,從而改變或擴充元件的功能。

設計模式系列10--裝飾者模式
image


程式碼實現

1、定義抽象基類

先定義一個抽象基類,工資類和獎金計算方式類都繼承自這個類,該類定義了一個公開介面,用於計算獎金

#import <Foundation/Foundation.h>

@interface component : NSObject
-(NSInteger)calculateSalary:(NSInteger)monthSales  sumSales:(NSInteger)sumSales;
@end

=================複製程式碼

2、定義工資類(被裝飾物件)

#import "component.h"

@interface concreteComponent : component

@end

======================

//被裝飾物件,基本工資

#import "concreteComponent.h"

@implementation concreteComponent

-(NSInteger)calculateSalary:(NSInteger)monthSales  sumSales:(NSInteger)sumSales{
    //基本工資8000
    return 8000;
}

@end複製程式碼

3、定義抽象裝飾器

定義一個抽象裝飾器,繼承自抽象基類component,每個具體的裝飾器繼承自該類,該類主要做一些初始化工作


#import "component.h"

@interface Decorator : component
@property(strong,nonatomic)component *components;
- (instancetype)initWithComponet:(component *)component;
@end

=================

#import "Decorator.h"

@implementation Decorator
- (instancetype)initWithComponet:(component *)component
{
    self = [super init];
    if (self) {
        self.components = component;
    }
    return self;
}

-(NSInteger)calculateSalary:(NSInteger)monthSales sumSales:(NSInteger)sumSales{
    return [self.components calculateSalary:monthSales sumSales:sumSales];
}
@end複製程式碼

4、具體裝飾器

每月銷售獎金裝飾器

#import "Decorator.h"

@interface monthBonusDecorator : Decorator

@end

==================


#import "monthBonusDecorator.h"

@implementation monthBonusDecorator

-(NSInteger)calculateSalary:(NSInteger)monthSales sumSales:(NSInteger)sumSales{
    NSInteger salary = [self.components calculateSalary:monthSales sumSales:sumSales];
    NSInteger bonus = monthSales * 0.03;
    NSLog(@"當月銷售獎金:%zd", bonus);
    return salary += bonus;
}
@end複製程式碼

累積獎金裝飾器

#import "Decorator.h"

@interface sumBonusDecatorator : Decorator

@end

================

#import "sumBonusDecatorator.h"

@implementation sumBonusDecatorator

-(NSInteger)calculateSalary:(NSInteger)monthSales sumSales:(NSInteger)sumSales{
    NSInteger salary = [self.components calculateSalary:monthSales sumSales:sumSales];
    NSInteger bonus = sumSales * 0.01;
    NSLog(@"累積銷售獎金:%zd", bonus);
    return salary += bonus;
}

@end複製程式碼

團隊獎金裝飾器

#import "Decorator.h"

@interface groupBonusDecorator : Decorator

@end

=================

#import "groupBonusDecorator.h"

@implementation groupBonusDecorator
-(NSInteger)calculateSalary:(NSInteger)monthSales sumSales:(NSInteger)sumSales{
    NSInteger salary = [self.components calculateSalary:monthSales sumSales:sumSales];
    NSInteger bonus = 100000 * 0.01;
    NSLog(@"團隊獎金:%zd", bonus);
    return salary += bonus;
}
@end複製程式碼

5、測試

 //基本工資,被裝飾物件
        component *c1 = [concreteComponent new];

        //裝飾器
        Decorator *d1 = [[monthBonusDecorator alloc]initWithComponet:c1];
        Decorator *d2 = [[sumBonusDecatorator alloc]initWithComponet:d1];
        NSInteger salary1 = [d2 calculateSalary:10000 sumSales:12212];
        NSLog(@"\n獎金組合方式:當月銷售獎金 + 累積銷售獎金 \n 總工資 = %zd", salary1);

        NSLog(@"\n=============================================================================");

        Decorator *d3 = [[monthBonusDecorator alloc]initWithComponet:c1];
        Decorator *d4 = [[sumBonusDecatorator alloc]initWithComponet:d3];
        Decorator *d5 = [[groupBonusDecorator alloc]initWithComponet:d4];
        NSInteger salary2 = [d5 calculateSalary:12100 sumSales:12232];
        NSLog(@"\n獎金組合方式:當月銷售獎金 + 累積銷售獎金 + 團隊獎金 \n 總工資 = %zd", salary2);


        NSLog(@"\n=============================================================================");

        Decorator *d6 = [[monthBonusDecorator alloc]initWithComponet:c1];
        Decorator *d7 = [[groupBonusDecorator alloc]initWithComponet:d6];
        NSInteger salary3 = [d7 calculateSalary:23111 sumSales:231111];
        NSLog(@"\n獎金組合方式:當月銷售獎金 + 團隊獎金 \n 總工資 = %zd", salary3);複製程式碼

輸出如下

2016-12-14 10:34:31.280 裝飾者模式[64586:1944336] 當月銷售獎金:300
2016-12-14 10:34:31.280 裝飾者模式[64586:1944336] 累積銷售獎金:122
2016-12-14 10:34:31.280 裝飾者模式[64586:1944336] 
獎金組合方式:當月銷售獎金 + 累積銷售獎金 
 總工資 = 8422

=============================================================================
2016-12-14 10:34:31.280 裝飾者模式[64586:1944336] 當月銷售獎金:363
2016-12-14 10:34:31.280 裝飾者模式[64586:1944336] 累積銷售獎金:122
2016-12-14 10:34:31.280 裝飾者模式[64586:1944336] 團隊獎金:1000
2016-12-14 10:34:31.280 裝飾者模式[64586:1944336] 
獎金組合方式:當月銷售獎金 + 累積銷售獎金 + 團隊獎金 
 總工資 = 9485

=============================================================================
2016-12-14 10:34:31.281 裝飾者模式[64586:1944336] 當月銷售獎金:693
2016-12-14 10:34:31.281 裝飾者模式[64586:1944336] 團隊獎金:1000
2016-12-14 10:34:31.281 裝飾者模式[64586:1944336] 
獎金組合方式:當月銷售獎金 + 團隊獎金 
 總工資 = 9693複製程式碼

6、小結

從上面的測試可以看出,不管是使用何種獎金組合方式,只需要呼叫對應的裝飾器即可,非常靈活。通過上面的程式碼我們看到,裝飾器是一層層包裹的,基本工資被月工資裝飾器包裹,月工資裝飾器被累積獎金裝飾器包裹,累積裝飾器被團隊獎金裝飾器包裹,當呼叫計算獎金的公式的時候,就會按照順序層層遞迴呼叫每個裝飾器的功能,到最後算出總工資,我們來用示意圖看看呼叫過程。

由於每個裝飾器之間是完全獨立的,所以我們可以使用任何我們想要的方式去組合這些裝飾器,比如多次重複呼叫同一個裝飾器,調換裝飾器的順序等等。

設計模式系列10--裝飾者模式
image


適用性

在如下情況可以考慮使用物件組合

  • 在不影響其他物件的情況下,以動態、透明的方式給單個物件新增職責。

  • 處理那些可以撤消的職責。

  • 當不能採用生成子類的方法進行擴充時
    一種情況是,可能有大量獨立的擴充套件,為支援每一種組合將產生大量的子類,使得子類數目呈爆炸性增長。另一種情況可能是因為類定義被隱藏,或類定義不能用於生成子類。


優缺點

  1. 比繼 承 更 靈 活

    與 對 象 的 靜 態 繼 承 ( 多 重 繼 承 ) 相 比 , D e c o r a t o r 模 式 提 供 了 更 加
    靈活的向物件新增職責的方式。可以用新增和分離的方法,用裝飾在執行時刻增加和刪除職 責。相比之下,繼承機制要求為每個新增的職責建立一個新的子類。這會產生許多新的類,並且會增加系統的複雜度。此外,為一 個特定的 C o m p o n e n t 類 提 供 多 個 不 同 的 D e c o r a t o r 類 , 這 就 使 得 你 可 以 對 一 些 職 責 進 行 混 合 和 匹配。
    使用 D e c o r a t o r 模式可以很容易地重複新增一個特性。

  2. 避 免 了高層次類 有 太 多 的 特 徵

    D e c o r a t o r模 式 提 供 了 一 種 “ 即 用 即 付 ” 的 方 法來新增職責。它並不試圖在一個複雜的可定製的類中支援所有可預見的特徵,相反,你可 以定義一個簡單的類,並且用 D e c o r a t o r 類 給 它 逐 漸 地 添 加 功 能 。 可 以 從 簡 單 的 部 件 組 合 出 復 雜的功能。這樣,應用程式不必為不需要的特徵付出代價。同時也更易於不依賴於 D e c o r a t o r 所擴充套件(甚至是不可預知的擴充套件)的類而獨立地定義新型別的 D e c o r a t o r 。 擴 展 一 個 復 雜 類 的 時候,很可能會暴露與新增的職責無關的細節。

  3. 產生 許 多 小 對 象

    採用 D e c o r a t o r 模 式 進 行 系 統 設 計 往 往 會 產 生 許 多 看 上 去 類 似 的 小 對 象 , 這些物件僅僅在他們相互連線的方式上有所不同,而不是它們的類或是它們的屬性值有所不 同。儘管對於那些瞭解這些系統的人來說,很容易對它們進行定製,但是很難學習這些系統, 排錯也很困難。


裝飾器和AOP

關於面向切換程式設計的具體解釋看這裡

百度百科AOP解釋

AOP一般用來實現如下功能:日誌記錄,效能統計,安全控制,事務處理,異常處理等等。將日誌記錄,效能統計,安全控制,事務處理,異常處理等程式碼從業務邏輯程式碼中劃分出來,通過對這些行為的分離,我們希望可以將它們獨立到非業務邏輯的方法中,進而改變這些行為的時候不影響業務邏輯的程式碼。

看上面的描述就知道,裝飾器就是一個很好實現AOP的方式,因為裝飾器就是在不改變原有功能的前提下對其進行透明擴充套件的。

我們目前有一個鴨子類,實現呱呱叫的一個方法,現在我希望在不改變原有功能的情況下,統計鴨子叫了多少次,這就是AOP中的日誌記錄功能,我們可以使用裝飾器模式來實現,具體程式碼我就不貼了,直接看最後的demo。


裝飾器在iOS中的運用

我們應該用過一些圖片處理app,可以給圖片加上各種各樣的濾鏡或者裁剪旋轉圖片等等功能,其實這些也可以使用裝飾器來實現,可以把每個功能都實現為一個裝飾器,然後使用者選擇使用什麼功能,就給圖片加上對應的裝飾器去做處理,這樣做是不是非常靈活?

其實在iOS裡面已經為我們提供了類似裝飾器模式的功能的方法:category。category也可以透明的為一個類新增方法,下面我就使用一個小demo來演示如何使用category和裝飾者模式分別來實現圖片的選擇和陰影效果。具體見demo。


demo下載

裝飾者模式Demo

鴨子叫聲計數器Demo

裝飾者模式和category Demo

相關文章