物件導向設計的設計模式(二):結構型模式(附 Demo & UML類圖)

J_Knight_發表於2018-11-28

本篇是物件導向設計系列文章的第三篇,講解的是設計模式中的結構型模式:

  • 外觀模式
  • 介面卡模式
  • 橋接模式
  • 代理模式
  • 裝飾者模式
  • 享元模式

該系列前面的兩篇文章:

一. 外觀模式

定義

外觀模式(Facade Pattern):外觀模式定義了一個高層介面,為子系統中的一組介面提供一個統一的介面。外觀模式又稱為門面模式,它是一種結構型設計模式模式。

定義解讀:通過這個高層介面,可以將客戶端與子系統解耦:客戶端可以不直接訪問子系統,而是通過外觀類間接地訪問;同時也可以提高子系統的獨立性和可移植性。

適用場景

  • 子系統隨著業務複雜度的提升而變得越來越複雜,客戶端需要某些子系統共同協作來完成某個任務。
  • 在多層結構的系統中,使用外觀物件可以作為每層的入口來簡化層間的呼叫。

成員與類圖

成員

外觀模式包括客戶端共有三個成員:

  • 客戶端類(Client):客戶端是意圖操作子系統的類,它與外觀類直接接觸;與外觀類間接接觸

  • 外觀類(Facade):外觀類知曉各個子系統的職責和介面,封裝子系統的介面並提供給客戶端

  • 子系統類(SubSystem):子系統類實現子系統的功能,對外觀類一無所知

下面通過類圖來看一下各個成員之間的關係:

模式類圖

外觀模式類圖

上圖中的method1&2()方法就是呼叫SubSystem1SubSystem2method1()method2()方法。同樣適用於method2&3()

程式碼示例

場景概述

模擬一個智慧家居系統。這個智慧家居系統可以用一箇中央遙控器操作其所接入的一些傢俱:檯燈,音響,空調等等。

在這裡我們簡單操縱幾個裝置:

  • 空調
  • CD Player
  • DVD Player
  • 音響
  • 投影儀

場景分析

有的時候,我們需要某個裝置可以一次執行兩個不同的操作;也可能會需要多個裝置共同協作來執行一些任務。比如:

假設我們可以用遙控器直接開啟熱風,那麼實際上就是兩個步驟:

  1. 開啟空調
  2. 空調切換為熱風模式

我們把這兩個步驟用一個操作包含起來,一步到位。像這樣簡化操作步驟的場景比較適合用外觀模式。

同樣的,我們想聽歌的話,需要四個步驟:

  1. 開啟CD Player
  2. 開啟音響
  3. 連線CD Player和音響
  4. 播放CD Player

這些步驟我們也可以裝在單獨的一個介面裡面。

類似的,如果我們想看DVD的話,步驟會更多,因為DVD需要同時輸出聲音和影像:

  1. 開啟DVD player
  2. 開啟音響
  3. 音響與DVD Player連線
  4. 開啟投影儀
  5. 投影儀與DVD Player連線
  6. 播放DVD Player

這些介面也可以裝在一個單獨的介面裡。

最後,如果我們要出門,需要關掉所有家用電器,也不需要一個一個將他們關掉,也只需要一個關掉的總介面就好了,因為這個關掉的總介面裡面可以包含所有家用電器的關閉介面。

因此,這些裝置可以看做是該智慧家居系統的子系統;而這個遙控器則扮演的是外觀類的角色。

下面我們用程式碼來看一下如何實現這些設計。

程式碼實現

因為所有家用電器都有開啟和關閉的操作,所以我們先建立一個家用電器的基類HomeDevice

//================== HomeDevice.h ==================
//裝置基類

@interface HomeDevice : NSObject

//連線電源
- (void)on;

//關閉電源
- (void)off;

@end
複製程式碼

然後是繼承它的所有家用電器類:

空調類AirConditioner:

//================== AirConditioner.h ==================

@interface AirConditioner : HomeDevice

//高溫模式
- (void)startHighTemperatureMode;

//常溫模式
- (void)startMiddleTemperatureMode;

//低溫模式
- (void)startLowTemperatureMode;

@end
複製程式碼

CD Player類:CDPlayer:

//================== CDPlayer.h ==================

@interface CDPlayer : HomeDevice

- (void)play;

@end
複製程式碼

DVD Player類:DVDPlayer:

//================== DVDPlayer.h ==================

@interface DVDPlayer : HomeDevice

- (void)play;

@end
複製程式碼

音響類VoiceBox:

//================== VoiceBox.h ==================

@class CDPlayer;
@class DVDPlayer;

@interface VoiceBox : HomeDevice

//與CDPlayer連線
- (void)connetCDPlayer:(CDPlayer *)cdPlayer;

//與CDPlayer斷開連線
- (void)disconnetCDPlayer:(CDPlayer *)cdPlayer;

//與DVD Player連線
- (void)connetDVDPlayer:(DVDPlayer *)dvdPlayer;

//與DVD Player斷開連線
- (void)disconnetDVDPlayer:(DVDPlayer *)dvdPlayer;

@end
複製程式碼

投影儀類Projecter

//================== Projecter.h ==================

@interface Projecter : HomeDevice

//與DVD Player連線
- (void)connetDVDPlayer:(DVDPlayer *)dvdPlayer;

//與DVD Player斷開連線
- (void)disconnetDVDPlayer:(DVDPlayer *)dvdPlayer;

@end
複製程式碼

注意,音響是可以連線CD Player和DVD Player的;而投影儀只能連線DVD Player

現在我們把所有的家用電器類和他們的介面都定義好了,下面我們看一下該例項的外觀類HomeDeviceManager如何設計。

首先我們看一下客戶端期望外觀類實現的介面:

//================== HomeDeviceManager.h ==================

@interface HomeDeviceManager : NSObject

//===== 關於空調的介面 =====

//空調吹冷風
- (void)coolWind;

//空調吹熱風
- (void)warmWind;


//===== 關於CD Player的介面 =====

//播放CD
- (void)playMusic;

//關掉音樂
- (void)offMusic;


//===== 關於DVD Player的介面 =====

//播放DVD
- (void)playMovie;

//關閉DVD
- (void)offMoive;


//===== 關於總開關的介面 =====

//開啟全部家用電器
- (void)allDeviceOn;

//關閉所有家用電器
- (void)allDeviceOff;

@end
複製程式碼

上面的介面分為了四大類,分別是:

  • 關於空調的介面
  • 關於CD Player的介面
  • 關於DVD Player的介面
  • 關於總開關的介面

為了便於讀者理解,這四類的介面所封裝的子系統介面的數量是逐漸增多的。

在看這些介面時如何實現的之前,我們先看一下外觀類是如何保留這些子系統類的例項的。在該程式碼示例中,這些子系統類的例項在外觀類的構造方法裡被建立,而且作為外觀類的成員變數被儲存了下來。

//================== HomeDeviceManager.m ==================

@implementation HomeDeviceManager
{
    NSMutableArray *_registeredDevices;//所有註冊(被管理的)的家用電器
    AirConditioner *_airconditioner;
    CDPlayer *_cdPlayer;
    DVDPlayer *_dvdPlayer;
    VoiceBox *_voiceBox;
    Projecter *_projecter;
    
}

- (instancetype)init{
    
    self = [super init];
    
    if (self) {
        
        _airconditioner = [[AirConditioner alloc] init];
        _cdPlayer = [[CDPlayer alloc] init];
        _dvdPlayer = [[DVDPlayer alloc] init];
        _voiceBox = [[VoiceBox alloc] init];
        _projecter = [[Projecter alloc] init];
        
        _registeredDevices = [NSMutableArray arrayWithArray:@[_airconditioner,
                                                              _cdPlayer,
                                                              _dvdPlayer,
                                                              _voiceBox,
                                                              _projecter]];
    }
    return self;
}
複製程式碼

其中 _registeredDevices這個成員變數是一個陣列,它包含了所有和這個外觀類例項關聯的子系統例項。

子系統與外觀類的關聯實現方式不止一種,不作為本文研究重點,現在只需知道外觀類保留了這些子系統的例項即可。按照順序,我們首先看一下關於空調的介面的實現:

//================== HomeDeviceManager.m ==================

//空調吹冷風
- (void)coolWind{
    
    [_airconditioner on];
    [_airconditioner startLowTemperatureMode];
    
}

//空調吹熱風
- (void)warmWind{
    
    [_airconditioner on];
    [_airconditioner startHighTemperatureMode];
}
複製程式碼

吹冷風和吹熱風的介面都包含了空調例項的兩個介面,第一個都是開啟空調,第二個則是對應的冷風和熱風的介面。

我們接著看關於CD Player的介面的實現:

//================== HomeDeviceManager.m ==================

- (void)playMusic{
    
    //1. 開啟CDPlayer開關
    [_cdPlayer on];
    
    //2. 開啟音響
    [_voiceBox on];
    
    //3. 音響與CDPlayer連線
    [_voiceBox connetCDPlayer:_cdPlayer];
    
    //4. 播放CDPlayer
    [_cdPlayer play];
}

//關掉音樂
- (void)offMusic{
    
   //1. 切掉與音響的連線
    [_voiceBox disconnetCDPlayer:_cdPlayer];
    
    //2. 關掉音響
    [_voiceBox off];
    
    //3. 關掉CDPlayer
    [_cdPlayer off];
}
複製程式碼

在上面的場景分析中提到過,聽音樂這個指令要分四個步驟:CD Player和音響的開啟,二者的連線,以及播放CD Player,這也比較符合實際生活中的場景。關掉音樂也是先斷開連線再切斷電源(雖然直接切斷電源也可以)。

接下來我們看一下關於DVD Player的介面的實現:

//================== HomeDeviceManager.m ==================

- (void)playMovie{
    
    //1. 開啟DVD player
    [_dvdPlayer on];
    
    //2. 開啟音響
    [_voiceBox on];
    
    //3. 音響與DVDPlayer連線
    [_voiceBox connetDVDPlayer:_dvdPlayer];
    
    //4. 開啟投影儀
    [_projecter on];
    
    //5.投影儀與DVDPlayer連線
    [_projecter connetDVDPlayer:_dvdPlayer];
    
    //6. 播放DVDPlayer
    [_dvdPlayer play];
}


- (void)offMoive{

    //1. 切掉音響與DVDPlayer連線
    [_voiceBox disconnetDVDPlayer:_dvdPlayer];
    
    //2. 關掉音響
    [_voiceBox off];
    
    //3. 切掉投影儀與DVDPlayer連線
    [_projecter disconnetDVDPlayer:_dvdPlayer];
    
    //4. 關掉投影儀
    [_projecter off];
    
    //5. 關掉DVDPlayer
    [_dvdPlayer off];
}
複製程式碼

因為DVD Player要同時連線音響和投影儀,所以這兩個介面封裝的子系統介面相對於CD Player的更多一些。

最後我們看一下關於總開關的介面的實現:

//================== HomeDeviceManager.m ==================

//開啟全部家用電器
- (void)allDeviceOn{
    
    [_registeredDevices enumerateObjectsUsingBlock:^(HomeDevice *device, NSUInteger idx, BOOL * _Nonnull stop) {
        [device on];
    }];
}


//關閉所有家用電器
- (void)allDeviceOff{
    
    [_registeredDevices enumerateObjectsUsingBlock:^(HomeDevice *device, NSUInteger idx, BOOL * _Nonnull stop) {
        [device off];
    }];
}
複製程式碼

這兩個介面是為了方便客戶端開啟和關閉所有裝置的,有這兩個介面的話,使用者就不用一一開啟或關閉多個裝置了。

關於這兩個介面的實現:

上文說過,該外觀類通過一個陣列成員變數_registeredDevices來儲存所有可操作的裝置。所以如果我們需要開啟或關閉所有的裝置就可以遍歷這個陣列並向每個元素呼叫onoff方法。因為這些元素都繼承於HomeDevice,也就是都有onoff方法。

這樣做的好處是,我們不需要單獨列出所有裝置來分別呼叫它們的介面;而且後面如果新增或者刪除某些裝置的話也不需要修改這兩個介面的實現了。

下面我們看一下該demo多對應的類圖。

程式碼對應的類圖

外觀模式程式碼示例類圖

從上面的UML類圖中可以看出,該示例的子系統之間的耦合還是比較多的;而外觀類HomeDeviceManager的介面大大簡化了User對這些子系統的使用成本。

優點

  • 實現了客戶端與子系統間的解耦:客戶端無需知道子系統的介面,簡化了客戶端呼叫子系統的呼叫過程,使得子系統使用起來更加容易。同時便於子系統的擴充套件和維護。
  • 符合迪米特法則(最少知道原則):子系統只需要將需要外部呼叫的介面暴露給外觀類即可,而且他的介面則可以隱藏起來。

缺點

  • 違背了開閉原則:在不引入抽象外觀類的情況下,增加新的子系統可能需要修改外觀類或客戶端的程式碼。

Objective-C & Java的實踐

  • Objective-C:SDWebImage封裝了負責圖片下載的類和負責圖片快取的類,而外部僅向客戶端暴露了簡約的下載圖片的介面。
  • Java:Spring-JDBC中的JdbcUtils封裝了ConnectionResultSetStatement的方法提供給客戶端

二. 介面卡模式

定義

介面卡模式(Adapter Pattern) :將一個介面轉換成客戶希望的另一個介面,使得原本由於介面不相容而不能一起工作的那些類可以一起工作。介面卡模式的別名是包裝器模式(Wrapper),是一種結構型設計模式。

定義解讀:介面卡模式又分為物件介面卡和類介面卡兩種。

  • 物件介面卡:利用組合的方式將請求轉發給被適配者。
  • 類介面卡:通過介面卡類多重繼承目標介面和被適配者,將目標方法的呼叫轉接到呼叫被適配者的方法。

適用場景

  • 想使用一個已經存在的類,但是這個類的介面不符合我們的要求,原因可能是和系統內的其他需要合作的類不相容。
  • 想建立一個功能上可以複用的類,這個類可能需要和未來某些未知介面的類一起工作。

成員與類圖

成員

介面卡模式有三個成員:

  • 目標(Target):客戶端希望直接接觸的類,給客戶端提供了呼叫的介面
  • 被適配者(Adaptee):被適配者是已經存在的類,即需要被適配的類
  • 介面卡(Adapter):介面卡對Adaptee的介面和Target的介面進行適配

模式類圖

如上文所說,介面卡模式分為類介面卡模式和物件介面卡模式,因此這裡同時提供這兩種細分模式的 UML類圖。

物件介面卡模式:

介面卡模式類圖

物件介面卡中,被適配者的物件被介面卡所持有。當介面卡的request方法被呼叫時,在這個方法內部再呼叫被適配者對應的方法。

類介面卡模式:

類介面卡模式類圖

類介面卡中採用了多繼承的方式:介面卡同時繼承了目標類和被適配者類,也就都持有了者二者的方法。

多繼承在Objective-C中可以通過遵循多個協議來實現,在本模式的程式碼示例中只使用物件介面卡來實現。

程式碼示例

場景概述

模擬一個替換快取元件的場景:目前客戶端已經依賴於舊的快取元件的介面,而後來發現有一個新的緩元件的效能更好一些,需要將舊的快取元件替換成新的快取元件,但是新的快取元件的介面與舊的快取介面不一致,所以目前來看客戶端是無法直接與新快取元件一起工作的。

場景分析

由於客戶端在很多地方依賴了舊快取元件的介面,將這些地方的介面都換成新快取元件的介面會比較麻煩,而且萬一後面還要換回舊快取元件或者再換成另外一個新的快取元件的話就還要做重複的事情,這顯然是不夠優雅的。

因此該場景比較適合使用介面卡模式:建立一個介面卡,讓原本與舊快取介面的客戶端可以與新快取元件一起工作。

在這裡,新的快取元件就是Adaptee,舊的快取元件(介面)就是Target,因為它是直接和客戶端接觸的。而我們需要建立一個介面卡類Adaptor來讓客戶端與新快取元件一起工作。下面用程式碼看一下上面的問題如何解決:

程式碼實現

首先我們建立舊快取元件,並讓客戶端正常使用它。 先建立舊快取元件的介面OldCacheProtocol

對應Java的介面,Objective-C中叫做協議,也就是protocol。

//================== OldCacheProtocol.h ==================

@protocol OldCacheProtocol <NSObject>

- (void)old_saveCacheObject:(id)obj forKey:(NSString *)key;

- (id)old_getCacheObjectForKey:(NSString *)key;

@end
複製程式碼

可以看到該介面包含了兩個操作快取的方法,方法字首為old

再簡單建立一個快取元件類OldCache,它實現了OldCacheProtocol介面:

//================== OldCache.h ==================

@interface OldCache : NSObject <OldCacheProtocol>

@end


    
//================== OldCache.m ==================
    
@implementation OldCache

- (void)old_saveCacheObject:(id)obj forKey:(NSString *)key{
    
    NSLog(@"saved cache by old cache object");
    
}

- (id)old_getCacheObjectForKey:(NSString *)key{
    
    NSString *obj = @"get cache by old cache object";
    NSLog(@"%@",obj);
    return obj;
}

@end
複製程式碼

為了讀者區分方便,將新舊兩個快取元件取名為NewCacheOldCache。實現程式碼也比較簡單,因為不是本文介紹的重點,只需區分介面名稱即可。

現在我們讓客戶端來使用這個舊快取元件:

//================== client.m ==================

@interface ViewController ()

@property (nonatomic, strong) id<OldCacheProtocol>cache;

@end

@implementation ViewController


- (void)viewDidLoad {
    
    [super viewDidLoad];
 
    //使用舊快取
    [self useOldCache];

    //使用快取元件操作
    [self saveObject:@"cache" forKey:@"key"];
    
}

//例項化舊快取並儲存在``cache``屬性裡
- (void)useOldCache{

    self.cache = [[OldCache alloc] init];
}

//使用cache物件
- (void)saveObject:(id)object forKey:(NSString *)key{

    [self.cache old_saveCacheObject:object forKey:key];
}
複製程式碼
  • 在這裡的客戶端就是ViewController,它持有一個遵從OldCacheProtocol協議的例項,也就是說它目前依賴於OldCacheProtocol的介面。
  • useOldCache方法用來例項化舊快取並儲存在cache屬性裡。
  • saveObject:forKey:方法是真正使用cache物件來儲存快取。

執行並列印一下結果輸出是:saved cache by old cache object。現在看來客戶端使用舊快取是沒有問題的。

而現在我們要加入新的快取元件了: 首先定義新快取元件的介面NewCacheProtocol

//================== NewCacheProtocol.h ==================

@protocol NewCacheProtocol <NSObject>

- (void)new_saveCacheObject:(id)obj forKey:(NSString *)key;

- (id)new_getCacheObjectForKey:(NSString *)key;

@end
複製程式碼

可以看到,NewCacheProtocolOldCacheProtocol介面大致是相似的,但是名稱還是不同,這裡使用了不同的方法字首做了區分。

接著看一下新快取元件是如何實現這個介面的:

//================== NewCache.h ==================

@interface NewCache : NSObject <NewCacheProtocol>

@end


    
//================== NewCache.m ==================
@implementation NewCache

- (void)new_saveCacheObject:(id)obj forKey:(NSString *)key{
    
    NSLog(@"saved cache by new cache object");
}

- (id)new_getCacheObjectForKey:(NSString *)key{
    
    NSString *obj = @"saved cache by new cache object";
    NSLog(@"%@",obj);
    return obj;
}
@end
複製程式碼

現在我們拿到了新的快取元件,但是客戶端類目前依賴的是舊的介面,因此介面卡類應該上場了:

//================== Adaptor.h ==================

@interface Adaptor : NSObject <OldCacheProtocol>

- (instancetype)initWithNewCache:(NewCache *)newCache;

@end


    
//================== Adaptor.m ==================
    
@implementation Adaptor
{
    NewCache *_newCache;
}

- (instancetype)initWithNewCache:(NewCache *)newCache{
    
    self = [super init];
    if (self) {
        _newCache = newCache;
    }
    return self;
}

- (void)old_saveCacheObject:(id)obj forKey:(NSString *)key{
    
    //transfer responsibility to new cache object
    [_newCache new_saveCacheObject:obj forKey:key];
}

- (id)old_getCacheObjectForKey:(NSString *)key{
    
    //transfer responsibility to new cache object
    return [_newCache new_getCacheObjectForKey:key];
    
}
@end
複製程式碼
  • 首先,介面卡類也實現了舊快取元件的介面;目的是讓它也可以接收到客戶端操作舊快取元件的方法。
  • 然後,介面卡的構造方法裡面需要傳入新元件類的例項;目的是在收到客戶端操作舊快取元件的命令後,將該命令轉發給新快取元件類,並呼叫其對應的方法。
  • 最後我們看一下介面卡類是如何實現兩個舊快取元件的介面的:在old_saveCacheObject:forKey:方法中,讓新快取元件物件呼叫對應的new_saveCacheObject:forKey:方法;同樣地,在old_getCacheObjectForKey方法中,讓新快取元件物件呼叫對應的new_getCacheObjectForKey:方法。

這樣一來,介面卡類就定義好了。 那麼最後我們看一下在客戶端裡面是如何使用介面卡的:

//================== client ==================

- (void)viewDidLoad {

    [super viewDidLoad];
 
    //使用新快取元件
    [self useNewCache];
    
    [self saveObject:@"cache" forKey:@"key"];
}

- (void)useOldCache{
    
    self.cache = [[OldCache alloc] init];
}

//使用新快取元件
- (void)useNewCache{
    
    self.cache = [[Adaptor alloc] initWithNewCache:[[NewCache alloc] init]];
}

//使用cache物件
- (void)saveObject:(id)object forKey:(NSString *)key{
    
    [self.cache old_saveCacheObject:object forKey:key];
}
複製程式碼

我們可以看到,在客戶端裡面,只需要改一處就可以了:將我們定義好的介面卡類儲存在原來的cache屬性中就可以了(useNewCache方法的實現)。而真正操作快取的方法saveObject:forKey不需要有任何改動。

我們可以看到,使用介面卡模式,客戶端呼叫舊快取元件介面的方法都不需要改變;只需稍作處理,就可以在新舊快取元件中來回切換,也不需要原來客戶端對快取的操作。

而之所以可以做到這麼靈活,其實也是因為在一開始客戶端只是依賴了舊快取元件類所實現的介面,而不是舊快取元件類的型別。有心的讀者可能注意到了,上面viewController的屬性是@property (nonatomic, strong) id<OldCacheProtocol>cache;。正因為如此,我們新建的介面卡例項才能直接用在這裡,因為介面卡類也是實現了<OldCacheProtocol>介面。相反,如果我們的cache屬性是這麼寫的:@property (nonatomic, strong) OldCache *cache;,即客戶端依賴了舊快取元件的型別,那麼我們的介面卡類就無法這麼容易地放在這裡了。因此為了我們的程式在將來可以更好地修改和擴充套件,依賴介面是一個前提。

下面我們看一下該程式碼示例對應的類圖:

程式碼對應的類圖

介面卡模式程式碼示例類圖

優點

  • 符合開閉原則:使用介面卡而不需要改變現有類,提高類的複用性。
  • 目標類和介面卡類解耦,提高程式擴充套件性。

缺點

  • 增加了系統的複雜性

Objective-C & Java的實踐

  • Objective-C:暫時未發現介面卡模式的實踐,有知道的同學可以留言
  • Java:JDK中的XMLAdapter使用了介面卡模式。

三. 橋接模式

定義

橋接模式(Simple Factory Pattern):將抽象部分與它的實現部分分離,使它們都可以獨立地變化。

定義解讀:橋接模式的核心是兩個抽象以組合的形式關聯到一起,從而他們的實現就互不依賴了。

適用場景

如果一個系統存在兩個獨立變化的維度,而且這兩個維度都需要進行擴充套件的時候比較適合使用橋接模式。

下面來看一下簡單工廠模式的成員和類圖。

成員與類圖

成員

橋接模式一共只有三個成員:

  • 抽象類(Abstraction):抽象類維護一個實現部分的物件的引用,並宣告呼叫實現部分的物件的介面。
  • 擴充套件抽象類(RefinedAbstraction):擴充套件抽象類定義跟實際業務相關的方法。
  • 實現類介面(Implementor):實現類介面定義實現部分的介面。
  • 具體實現類(ConcreteImplementor):具體實現類具體實現類是實現實現類介面的物件。

下面通過類圖來看一下各個成員之間的關係:

模式類圖

橋接模式類圖

從類圖中可以看出Abstraction持有Implementor,但是二者的實現類互不依賴。這就是橋接模式的核心。

程式碼示例

場景概述

建立一些不同的形狀,這些形狀帶有不同的顏色。

三種形狀:

  • 正方形
  • 長方形
  • 原型

三種顏色:

  • 紅色
  • 綠色
  • 藍色

場景分析

根據上述需求,可能有的朋友會這麼設計:

  • 正方形
    • 紅色正方形
    • 綠色正方形
    • 藍色正方形
  • 長方形
    • 紅色長方形
    • 綠色長方形
    • 藍色長方形
  • 圓形
    • 紅色圓形
    • 綠色圓形
    • 藍色圓形

這樣的設計確實可以實現上面的需求。但是設想一下,如果後來增加了一種顏色或者形狀的話,是不是要多出來很多類?如果形狀的種類數是m,顏色的種類數是n,以這種方式建立的總類數就是 m*n,當m或n非常大的時候,它們相乘的結果就會變得很大。

我們觀察一下這個場景:形狀和顏色這二者的是沒有關聯性的,二者可以獨立擴充套件和變化,這樣的組合比較適合用橋接模式來做。

根據上面提到的橋接模式的成員:

  • 抽象類就是圖形的抽象類
  • 擴充套件抽象類就是繼承圖形抽象類的子類:各種形狀
  • 實現類介面就是顏色介面
  • 具體實現類就是繼承顏色介面的類:各種顏色

下面我們用程式碼看一下該如何設計。

程式碼實現

首先我們建立形狀的基類Shape

//================== Shape.h ==================

@interface Shape : NSObject
{
    @protected Color *_color;
}

- (void)renderColor:(Color *)color;

- (void)show;

@end


    

//================== Shape.m ==================
    
@implementation Shape

- (void)renderColor:(Color *)color{
    
    _color = color;
}

- (void)show{
    NSLog(@"Show %@ with %@",[self class],[_color class]);
}

@end
複製程式碼

由上面的程式碼可以看出:

  • 形狀類Shape持有Color類的例項,二者是以組合的形式結合到一起的。而且Shape類定義了供外部傳入Color例項的方法renderColor::在這個方法裡面接收從外部傳入的Color例項並儲存起來。
  • 另外一個公共介面show實際上就是列印這個圖形的名稱及其所搭配的顏色,便於我們後續驗證。

接著我們建立三種不同的圖形類,它們都繼承於Shape類:

正方形類:

//================== Square.h ==================

@interface Square : Shape

@end


    
    
//================== Square.m ==================
    
@implementation Square

- (void)show{
    
    [super show];
}

@end
複製程式碼

長方形類:

//================== Rectangle.h ==================

@interface Rectangle : Shape

@end

    
    
    
//================== Rectangle.m ==================
    
@implementation Rectangle

- (void)show{
    
    [super show];
}

@end
複製程式碼

圓形類:

//================== Circle.h ==================

@interface Circle : Shape

@end
    

    
    
//================== Circle.m ==================  
    
@implementation Circle

- (void)show{
    
    [super show];
}

@end
複製程式碼

還記得上面的Shape類持有的Color類麼?它就是所有顏色類的父類:

//================== Color.h ==================   

@interface Color : NSObject

@end
    
    


//================== Color.m ================== 
    
@implementation Color

@end
複製程式碼

接著我們建立繼承這個Color類的三個顏色類:

紅色類:

//================== RedColor.h ==================

@interface RedColor : Color

@end


    
    
//================== RedColor.m ==================  
    
@implementation RedColor

@end
複製程式碼

綠色類:

//================== GreenColor.h ==================

@interface GreenColor : Color

@end


    
    
//================== GreenColor.m ==================
@implementation GreenColor

@end
複製程式碼

藍色類:

//================== BlueColor.h ==================

@interface BlueColor : Color

@end


    
 
//================== BlueColor.m ==================
    
@implementation BlueColor

@end
複製程式碼

OK,到現在所有的形狀類和顏色類的相關類已經建立好了,我們看一下客戶端是如何使用它們來組合成不同的帶有顏色的形狀的:

//================== client ==================


//create 3 shape instances
Rectangle *rect = [[Rectangle alloc] init];
Circle *circle = [[Circle alloc] init];
Square *square = [[Square alloc] init];
    
//create 3 color instances
RedColor *red = [[RedColor alloc] init];
GreenColor *green = [[GreenColor alloc] init];
BlueColor *blue = [[BlueColor alloc] init];
    
//rect & red color
[rect renderColor:red];
[rect show];
    
//rect & green color
[rect renderColor:green];
[rect show];
    
    
//circle & blue color
[circle renderColor:blue];
[circle show];
    
//circle & green color
[circle renderColor:green];
[circle show];
    
    
    
//square & blue color
[square renderColor:blue];
[square show];
    
//square & red color
[square renderColor:red];
[square show];
複製程式碼

上面的程式碼裡,我們先宣告瞭所有的形狀類和顏色類的例項,然後自由搭配,形成不同的形狀+顏色的組合。

下面我們通過列印的結果來看一下組合的效果:

Show Rectangle with RedColor
Show Rectangle with GreenColor
Show Circle with BlueColor
Show Circle with GreenColor
Show Square with BlueColor
Show Square with RedColor
複製程式碼

從列印的介面可以看出組合的結果是沒問題的。

跟上面沒有使用橋接模式的設計相比,使用橋接模式需要的類的總和是 m + n:當m或n的值很大的時候是遠小於 m * n(沒有使用橋接,而是使用繼承的方式)的。

而且如果後面還要增加形狀和顏色的話,使用橋接模式就可以很方便地將原有的形狀和顏色和新的形狀和顏色進行搭配了,新的類和舊的類互不干擾。

下面我們看一下上面程式碼所對應的類圖:

程式碼對應的類圖

橋接模式程式碼示例類圖

從UML類圖可以看出,該設計是由兩個抽象層的類ShapeColor構建的,正因為依賴的雙方都是抽象類(而不是具體的實現),而且二者是以組合的方式聯絡到一起的,所以擴充套件起來非常方便,互不干擾。這對於今後我們對程式碼的設計有比較好的借鑑意義。

優點

  • 擴充套件性好,符合開閉原則:將抽象與實現分離,讓二者可以獨立變化

缺點

  • 在設計之前,需要識別出兩個獨立變化的維度。

Objective-C & Java的實踐

  • Objective-C:暫時未發現橋接模式的實踐,有知道的同學可以留言
  • Java:Spring-JDBC中的DriveManager通過registerDriver方法註冊不同型別的驅動

四. 代理模式

定義

代理模式(Proxy Pattern) :為某個物件提供一個代理,並由這個代理物件控制對原物件的訪問。

定義解讀:使用代理模式以後,客戶端直接訪問代理,代理在客戶端和目標物件之間起到中介的作用。

適用場景

在某些情況下,一個客戶不想或者不能直接引用一個物件,此時可以通過一個稱之為“代理”的第三者來實現間接引用。

因為代理物件可以在客戶端和目標物件之間起到中介的作用,因此可以通過代理物件去掉客戶不能看到 的內容和服務或者新增客戶需要的額外服務。

根據業務的不同,代理也可以有不同的型別:

  • 遠端代理:為位於不同地址或網路化中的物件提供本地代表。
  • 虛擬代理:根據要求建立重型的物件。
  • 保護代理:根據不同訪問許可權控制對原物件的訪問。

下面來看一下代理模式的成員和類圖。

成員與類圖

成員

代理模式算上客戶端一共有四個成員:

  • 客戶端(Client):客戶端意圖訪問真是主體介面
  • 抽象主題(Subejct):抽象主題定義客戶端需要訪問的介面
  • 代理(Proxy):代理繼承於抽象主題,目的是為了它持有真實目標的例項的引用,客戶端直接訪問代理
  • 真實主題(RealSubject):真實主題即是被代理的物件,它也繼承於抽象主題,它的例項被代理所持有,它的介面被包裝在了代理的介面中,而且客戶端無法直接訪問真實主題物件。

其實我也不太清楚代理模式裡面為什麼會是Subject和RealSubject這個叫法。

下面通過類圖來看一下各個成員之間的關係:

模式類圖

代理模式類圖

從類圖中可以看出,工廠類提供一個靜態方法:通過傳入的字串來製造其所對應的產品。

程式碼示例

場景概述

在這裡舉一個買房者通過買房中介買房的例子。

現在一般我們買房子不直接接觸房東,而是先接觸中介,買房的相關合同和一些事宜可以先和中介進行溝通。

在本例中,我們在這裡讓買房者直接支付費用給中介,然後中介收取一部分的中介費, 再將剩餘的錢交給房東。

場景分析

中介作為房東的代理,與買房者直接接觸。而且中介還需要在真正交易前做其他的事情(收取中介費,幫買房者check房源的真實性等等),因此該場景比較適合使用代理模式。

根據上面的代理模式的成員:

  • 客戶端就是買房者

  • 代理就是中介

  • 真實主題就是房東

  • 中介和房東都會實現收錢的方法,我們可以定義一個抽象主題類,它有一個公共介面是收錢的方法。

程式碼實現

首先我們定義一下房東和代理需要實現的介面PaymentInterface(在類圖裡面是繼承某個共同物件,我個人比較習慣用介面來做)。

//================== PaymentInterface.h ==================

@protocol PaymentInterface <NSObject>

- (void)getPayment:(double)money;

@end
複製程式碼

這個介面宣告瞭中介和房東都需要實現的方法getPayment:

接著我們宣告代理類HouseProxy:

//================== HouseProxy.h ==================

@interface HouseProxy : NSObject<PaymentInterface>

@end

    


//================== HouseProxy.m ==================
const float agentFeeRatio = 0.35;

@interface HouseProxy()

@property (nonatomic, copy) HouseOwner *houseOwner;

@end

@implementation HouseProxy

- (void)getPayment:(double)money{
    
    double agentFee = agentFeeRatio * money;
    NSLog(@"Proxy get payment : %.2lf",agentFee);
    
    [self.houseOwner getPayment:(money - agentFee)];
}

- (HouseOwner *)houseOwner{
    
    if (!_houseOwner) {
         _houseOwner = [[HouseOwner alloc] init];
    }
    return _houseOwner;
}

@end
複製程式碼

HouseProxy裡面,持有了房東,也就是被代理者的例項。然後在的getPayment:方法裡,呼叫了房東例項的getPayment:方法。而且我們可以看到,在呼叫房東例項的getPayment:方法,代理先拿到了中介費(中介費比率agentFeeRatio定義為0.35,即中介費的比例佔35%)。

這裡面除了房東例項的getPayment:方法之外的一些操作就是代理存在的意義:它可以在真正被代理物件做事情之前,之後做一些其他額外的事情。比如類似AOP程式設計一樣,定義類似的before***Method或是after**Method方法等等。

最後我們看一下房東是如何實現getPayment:方法的:

//================== HouseOwner.h ==================

@interface HouseOwner : NSObject<PaymentInterface>

@end

    

    
//================== HouseOwner.m ==================
    
@implementation HouseOwner

- (void)getPayment:(double)money{
    
    NSLog(@"House owner get payment : %.2lf",money);
}

@end
複製程式碼

房東類HouseOwner按照自己的方式實現了getPayment:方法。

很多時候被代理者(委託者)可以完全按照自己的方式去做事情,而把一些額外的事情交給代理來做,這樣可以保持原有類的功能的純粹性,符合開閉原則。

下面我們看一下客戶端的使用以及列印出來的結果:

客戶端程式碼:

//================== client.m ==================

HouseProxy *proxy = [[HouseProxy alloc] init];
[proxy getPayment:100];
複製程式碼

上面的客戶端支付給了中介100元。

下面我們看一下列印結果:

Proxy get payment : 35.00
House owner get payment : 65.00

複製程式碼

和預想的一樣,中介費收取了35%的中介費,剩下的交給了房東。

程式碼對應的類圖

代理模式程式碼示例類圖

從UML類圖中我們可以看出,在這裡沒有使用抽象主題物件,而是用一個介面來分別讓中介和房東實現。

優點

  • 降低系統的耦合度:代理模式能夠協調呼叫者和被呼叫者,在一定程度上降低了系 統的耦合度。
  • 不同型別的代理可以對客戶端對目標物件的訪問進行不同的控制:
    • 遠端代理,使得客戶端可以訪問在遠端機器上的物件,遠端機器 可能具有更好的計算效能與處理速度,可以快速響應並處理客戶端請求。
    • 虛擬代理通過使用一個小物件來代表一個大物件,可以減少系統資源的消耗,對系統進行優化並提高執行速度。
    • 保護代理可以控制客戶端對真實物件的使用許可權。

缺點

  • 由於在客戶端和被代理物件之間增加了代理物件,因此可能會讓客戶端請求的速度變慢。

Objective-C & Java的實踐

  • iOS SDK:NSProxy可以為持有的物件進行訊息轉發
  • JDK:AOP下的JDKDynamicAopProxy是對JDK的動態代理進行了封裝

五. 裝飾者模式

定義

裝飾模式(Decorator Pattern) :不改變原有物件的前提下,動態地給一個物件增加一些額外的功能。

適用場景

  • 動態地給一個物件增加職責(功能),這些職責(功能)也可以動態地被撤銷。
  • 當不能採用繼承的方式對系統進行擴充套件或者採用繼承不利於系統擴充套件和維護時。

成員與類圖

成員

裝飾者模式一共有四個成員:

  1. 抽象構件(Component):抽象構件定義一個物件(介面),可以動態地給這些物件新增職責。
  2. 具體構件(Concrete Component):具體構件是抽象構件的例項。
  3. 裝飾(Decorator):裝飾類也繼承於抽象構件,它持有一個具體構件物件的例項,並實現一個與抽象構件介面一致的介面。
  4. 具體裝飾(Concrete Decorator):具體裝飾負責給具體構建物件例項新增上附加的責任。

模式類圖

裝飾者模式類圖

程式碼示例

場景概述

模擬沙拉的製作:沙拉由沙拉底和醬汁兩個部分組成,不同的沙拉底和醬汁搭配可以組成不同的沙拉。

沙拉底 價格
蔬菜 5
雞肉 10
牛肉 16
醬汁 價格
醋汁 2
花生醬 4
藍莓醬 6

注意:同一份沙拉底可以搭配多鍾醬汁,而且醬汁的份數也可以不止一份。

場景分析

因為選擇一個沙拉底之後,可以隨意新增不同份數和種類的醬汁,也就是在原有的沙拉物件增加新的物件,所以比較適合用裝飾者模式來設計:醬汁相當於裝飾者,而沙拉底則是被裝飾的構件。

下面我們用程式碼看一下如何實現該場景。

程式碼實現

首先我們定義 抽象構件,也就是沙拉類的基類Salad

//================== Salad.h ==================

@interface Salad : NSObject

- (NSString *)getDescription;

- (double)price;

@end
複製程式碼

getDescriptionprice方法用來描述當前沙拉的配置以及價格(因為隨著裝飾者的裝飾,這兩個資料會一直變化)。

下面我們再宣告裝飾者的基類SauceDecorator。按照裝飾者設計模式類圖,該類是繼承於沙拉類的:

//================== SauceDecorator.h ==================

@interface SauceDecorator : Salad

@property (nonatomic, strong) Salad *salad;

- (instancetype)initWithSalad:(Salad *)salad;

@end

    

//================== SauceDecorator.m ==================
    
@implementation SauceDecorator

- (instancetype)initWithSalad:(Salad *)salad{
    
    self = [super init];
    
    if (self) {
        self.salad = salad;
    }
    return self;
}

@end
複製程式碼

在裝飾者的構造方法裡面傳入Salad類的例項,並將它儲存下來,目的是為了在裝飾它的時候用到。

現在抽象構件和裝飾者的基類都建立好了,下面我們建立具體構件和具體裝飾者。首先我們建立具體構件:

  • 蔬菜沙拉
  • 雞肉沙拉
  • 牛肉沙拉

蔬菜沙拉VegetableSalad

//================== VegetableSalad.h ==================

@interface VegetableSalad : Salad

@end


 
//================== VegetableSalad.m ==================
    
@implementation VegetableSalad

- (NSString *)getDescription{
    return @"[Vegetable Salad]";
}

- (double)price{
    return 5.0;
}

@end
複製程式碼

首先getDescription方法返回的是蔬菜沙拉底的描述;然後price方法返回的是它所對應的價格。

類似的,我們繼續按照價格表來建立雞肉沙拉底和牛肉沙拉底:

雞肉沙拉底:

//================== ChickenSalad.h ==================

@interface ChickenSalad : Salad

@end


    
//================== ChickenSalad.m ==================
@implementation ChickenSalad

- (NSString *)getDescription{
    return @"[Chicken Salad]";
}

- (double)price{
    return 10.0;
}

@end
複製程式碼

牛肉沙拉底:

//================== BeefSalad.h ==================

@interface BeefSalad : Salad

@end


    
//================== BeefSalad.m ==================
    
@implementation BeefSalad


- (NSString *)getDescription{
    return @"[Beef Salad]";
}

- (double)price{
    return 16.0;
}

@end
複製程式碼

現在所有的被裝飾者建立好了,下面我們按照醬汁的價格表來建立醬汁類(也就是具體裝飾者):

  • 醋汁
  • 花生醬
  • 藍莓醬

首先看一下醋汁VinegarSauceDecorator:

//================== VinegarSauceDecorator.h ==================

@interface VinegarSauceDecorator : SauceDecorator

@end

    

//================== VinegarSauceDecorator.m ==================    
    
@implementation VinegarSauceDecorator

- (NSString *)getDescription{
    return [NSString stringWithFormat:@"%@ + vinegar sauce",[self.salad getDescription]];
}

- (double)price{
    return [self.salad price] + 2.0;
}

@end
複製程式碼

重寫了getDescription方法,並新增了自己的裝飾,即在原來的描述上增加了+ vinegar sauce字串。之所以可以獲取到原有的描述,是因為在構造方法裡已經獲取了被裝飾者的物件(在裝飾者基類中定義的方法)。同樣地,價格也在原來的基礎上增加了自己的價格。

現在我們知道了具體裝飾者的設計,以此類推,我們看一下花生醬和藍莓醬類如何定義:

花生醬PeanutButterSauceDecorator類:

//================== PeanutButterSauceDecorator.h ==================     

@interface PeanutButterSauceDecorator : SauceDecorator

@end


    
//================== PeanutButterSauceDecorator.m ==================     
@implementation PeanutButterSauceDecorator

- (NSString *)getDescription{
    return [NSString stringWithFormat:@"%@ + peanut butter sauce",[self.salad getDescription]];
}

- (double)price{
    return [self.salad price] + 4.0;
}

@end
複製程式碼

藍莓醬類BlueBerrySauceDecorator:

//================== BlueBerrySauceDecorator.h ==================     

@interface BlueBerrySauceDecorator : SauceDecorator

@end


 
//================== BlueBerrySauceDecorator.m ==================     
    
@implementation BlueBerrySauceDecorator
    
- (NSString *)getDescription{
    
    return [NSString stringWithFormat:@"%@ + blueberry sauce",[self.salad getDescription]];
}

- (double)price{
    
    return [self.salad price] + 6.0;
}

@end
複製程式碼

OK,到現在所有的類已經定義好了,為了驗證是否實現正確,下面用客戶端嘗試著搭配幾種不同的沙拉吧:

  1. 蔬菜加單份醋汁沙拉(7元)
  2. 牛肉加雙份花生醬沙拉(24元)
  3. 雞肉加單份花生醬再加單份藍莓醬沙拉(20元)

首先我們看第一個搭配:

//================== client ==================     

//vegetable salad add vinegar sauce
Salad *vegetableSalad = [[VegetableSalad alloc] init];
NSLog(@"%@",vegetableSalad);

vegetableSalad = [[VinegarSauceDecorator alloc] initWithSalad:vegetableSalad];
NSLog(@"%@",vegetableSalad);
複製程式碼

第一次列印輸出:This salad is: [Vegetable Salad] and the price is: 5.00 第二次列印輸出:This salad is: [Vegetable Salad] + vinegar sauce and the price is: 7.00

上面程式碼中,我們首先建立了蔬菜底,然後再讓醋汁裝飾它(將蔬菜底的例項傳入醋汁裝飾者的構造方法中)。最後我們列印這個蔬菜底物件,描述和價格和裝飾之前的確實發生了變化,說明我們的程式碼沒有問題。

接著我們看第二個搭配:

//================== client ================== 

//beef salad add two peanut butter sauce:
Salad *beefSalad = [[BeefSalad alloc] init];
NSLog(@"%@",beefSalad);

beefSalad = [[PeanutButterSauceDecorator alloc] initWithSalad:beefSalad];
NSLog(@"%@",beefSalad);

beefSalad = [[PeanutButterSauceDecorator alloc] initWithSalad:beefSalad];
NSLog(@"%@",beefSalad);
複製程式碼

第一次列印輸出:[Beef Salad] and the price is: 16.00 第二次列印輸出:[Beef Salad] + peanut butter sauce and the price is: 20.00 第三次列印輸出:[Beef Salad] + peanut butter sauce + peanut butter sauce and the price is: 24.00

和上面的程式碼實現類似,都是先建立沙拉底(這次是牛肉底),然後再新增調料。由於是分兩次裝飾,所以要再寫一次花生醬的裝飾程式碼。對比每次列印的結果和上面的價格表可以看出輸出是正確的。

這個例子是加了兩次相同的醬汁,最後我們看第三個搭配,加入的是不同的兩個醬汁:

//================== client ================== 

//chiken salad add peanut butter sauce and blueberry sauce
Salad *chikenSalad = [[ChickenSalad alloc] init];
NSLog(@"%@",chikenSalad);

chikenSalad = [[PeanutButterSauceDecorator alloc] initWithSalad:chikenSalad];
NSLog(@"%@",chikenSalad);

chikenSalad = [[BlueBerrySauceDecorator alloc] initWithSalad:chikenSalad];
NSLog(@"%@",chikenSalad);
複製程式碼

第一次列印輸出:[Chicken Salad] and the price is: 10.00 第二次列印輸出:[Chicken Salad] + peanut butter sauce and the price is: 14.00 第三次列印輸出:[Chicken Salad] + peanut butter sauce + blueberry sauce and the price is: 20.00

對比每次列印的結果和上面的價格表可以看出輸出是正確的。

到這裡,該場景就模擬結束了。可以試想一下,如果今後加了其他的沙拉底和醬汁的話,只需要分別繼承Salad類和SauceDecorator類就可以了,現有的程式碼並不需要更改;而且經過不同組合可以搭配出更多種類的沙拉。

下面我們看一下該程式碼實現對應的類圖。

程式碼對應的類圖

裝飾者模式程式碼示例類圖

優點

  • 比繼承更加靈活:不同於在編譯期起作用的繼承;裝飾者模式可以在執行時擴充套件一個物件的功能。另外也可以通過配置檔案在執行時選擇不同的裝飾器,從而實現不同的行為。也可以通過不同的組合,可以實現不同效果。
  • 符合“開閉原則”:裝飾者和被裝飾者可以獨立變化。使用者可以根據需要增加新的裝飾類,在使用時再對其進行組合,原有程式碼無須改變。

缺點

  • 裝飾者模式需要建立一些具體裝飾類,會增加系統的複雜度。

Objective-C & Java的實踐

  • Objective-C中暫時未發現裝飾者模式的實踐,有知道的小夥伴可以留言
  • JDK中:BufferReader繼承了Reader,在BufferReader的構造器中傳入了Reader,實現了裝飾

六. 享元模式

定義

享元模式(Flyweight Pattern):運用共享技術複用大量細粒度的物件,降低程式記憶體的佔用,提高程式的效能。

定義解讀:

  • 享元模式的目的就是使用共享技術來實現大量細粒度物件的複用,提高效能。
  • 享元物件能做到共享的關鍵是區分內部狀態(Internal State)和外部狀態(External State)。
    • 內部狀態是儲存在享元物件內部並且不會隨環境改變而改變的狀態,因此內部狀態可以共享。
    • 外部狀態是隨環境改變而改變的、不可以共享的狀態。享元物件的外部狀態必須由客戶端儲存,並在享元物件被建立之後,在需要使用的時候再傳入到享元物件內部。一個外部狀態與另一個外部狀態之間是相互獨立的。

適用場景

  • 系統有大量的相似物件,這些物件有一些外在狀態。
  • 應當在多次重複使用享元物件時才值得使用享元模式。使用享元模式需要維護一個儲存享元物件的享元池,而這需要耗費資源,因此,

成員與類圖

成員

享元模式一共有三個成員:

  • 享元工廠(FlyweightFactory): 享元工廠提供一個用於儲存享元物件的享元池,使用者需要物件時,首先從享元池中獲取,如果享元池中不存在,則建立一個新的享元物件返回給使用者,並在享元池中儲存該新增物件
  • 抽象享元(Flyweight):抽象享元定義了具體享元物件需要實現的介面。
  • 具體享元(ConcreteFlyweight): 具體享元實現了抽象享元類定義的介面。

模式類圖

享元模式類圖

程式碼示例

場景概述

這裡我們使用《Objective-C 程式設計之道:iOS設計模式解析》裡的第21章使用的例子:在一個頁面展示數百個大小,位置不同的花的圖片,然而這些花的樣式只有6種。

看一下截圖:

百花圖

場景分析

由於這裡我們需要建立很多物件,而這些物件有可以共享的內部狀態(6種圖片內容)以及不同的外部狀態(隨機的,數百個位置座標和圖片大小),因此比較適合使用享元模式來做。

根據上面提到的享元模式的成員:

  • 我們需要建立一個工廠類來根據花的型別來返回花物件(這個物件包括內部可以共享的圖片以及外部狀態位置和大小):每次當新生成一種花的型別的物件的時候就把它儲存起來,因為下次如果還需要這個型別的花內部圖片物件的時候就可以直接用了。
  • 抽象享元類就是Objective-C的原生UIImageView,它可以顯示圖片
  • 具體享元類可以自己定義一個類繼承於UIImageView,因為後續我們可以直接新增更多其他的屬性。

下面我們看一下用程式碼如何實現:

程式碼實現

首先我們建立一個工廠,這個工廠可以根據所傳入花的型別來返回花內部圖片物件,在這裡可以直接使用原生的UIImage物件,也就是圖片物件。而且這個工廠持有一個儲存圖片物件的池子:

  • 當該型別的花第一次被建立時,工廠會新建一個所對應的花內部圖片物件,並將這個物件放入池子中儲存起來。
  • 當該型別的花內部圖片物件在池子裡已經有了,那麼工廠則直接從池子裡返回這個花內部圖片物件。

下面我們看一下程式碼是如何實現的:

//================== FlowerFactory.h ================== 

typedef enum 
{
  kAnemone,
  kCosmos,
  kGerberas,
  kHollyhock,
  kJasmine,
  kZinnia,
  kTotalNumberOfFlowerTypes
    
} FlowerType;

@interface FlowerFactory : NSObject 

- (FlowerImageView *) flowerImageWithType:(FlowerType)type

@end




//================== FlowerFactory.m ================== 
    
@implementation FlowerFactory
{
    NSMutableDictionary *_flowersPool;
}

- (FlowerImageView *) flowerImageWithType:(FlowerType)type
{
    
  if (_flowersPool == nil){
      
     _flowersPool = [[NSMutableDictionary alloc] initWithCapacity:kTotalNumberOfFlowerTypes];
  }
  
  //嘗試獲取傳入型別對應的花內部圖片物件
  UIImage *flowerImage = [_flowersPool objectForKey:[NSNumber numberWithInt:type]];
  
  //如果沒有對應型別的圖片,則生成一個
  if (flowerImage == nil){
    
    NSLog(@"create new flower image with type:%u",type);
      
    switch (type){
            
      case kAnemone:
        flowerImage = [UIImage imageNamed:@"anemone.png"];
        break;
      case kCosmos:
        flowerImage = [UIImage imageNamed:@"cosmos.png"];
        break;
      case kGerberas:
        flowerImage = [UIImage imageNamed:@"gerberas.png"];
        break;
      case kHollyhock:
        flowerImage = [UIImage imageNamed:@"hollyhock.png"];
        break;
      case kJasmine:
        flowerImage = [UIImage imageNamed:@"jasmine.png"];
        break;
      case kZinnia:
        flowerImage = [UIImage imageNamed:@"zinnia.png"];
        break;
      default:
        flowerImage = nil;
        break;
    
    }
      
    [_flowersPool setObject:flowerImage forKey:[NSNumber numberWithInt:type]];
      
  }else{
      //如果有對應型別的圖片,則直接使用
      NSLog(@"reuse flower image with type:%u",type);
  }
    
  //建立花物件,將上面拿到的花內部圖片物件賦值並返回
  FlowerImageView *flowerImageView = [[FlowerImageView alloc] initWithImage:flowerImage];
    
  return flowerImageView;
}
複製程式碼
  • 在這個工廠類裡面定義了六中圖片的型別
  • 該工廠類持有_flowersPool私有成員變數,儲存新建立過的圖片。
  • flowerImageWithType:的實現:結合了_flowersPool:當_flowersPool沒有對應的圖片時,新建立圖片並返回;否則直接從_flowersPool獲取對應的圖片並返回。

接著我們定義這些花物件FlowerImageView

//================== FlowerImageView.h ================== 

@interface FlowerImageView : UIImageView 

@end


//================== FlowerImageView.m ================== 
    
@implementation FlowerImageView

@end
複製程式碼

在這裡面其實也可以直接使用UIImageView,之所以建立一個子類是為了後面可以更好地擴充套件這些花獨有的一些屬性。

注意一下花物件和花內部圖片物件的區別:花物件FlowerImageView是包含花內部圖片物件的UIImage。因為在Objective-C裡面,UIImageFlowerImageView所繼承的UIImageView的一個屬性,所以在這裡FlowerImageView就直接包含了UIImage

下面我們來看一下客戶端如何使用FlowerFactoryFlowerImageView這兩個類:

//================== client ================== 

//首先建造一個生產花內部圖片物件的工廠
FlowerFactory *factory = [[FlowerFactory alloc] init];

for (int i = 0; i < 500; ++i)
{
    //隨機傳入一個花的型別,讓工廠返回該型別對應花型別的花物件
    FlowerType flowerType = arc4random() % kTotalNumberOfFlowerTypes;
    FlowerImageView *flowerImageView = [factory flowerImageWithType:flowerType];

    // 建立花物件的外部屬性值(隨機的位置和大小)
    CGRect screenBounds = [[UIScreen mainScreen] bounds];
    CGFloat x = (arc4random() % (NSInteger)screenBounds.size.width);
    CGFloat y = (arc4random() % (NSInteger)screenBounds.size.height);
    NSInteger minSize = 10;
    NSInteger maxSize = 50;
    CGFloat size = (arc4random() % (maxSize - minSize + 1)) + minSize;

    //將位置和大小賦予花物件
    flowerImageView.frame = CGRectMake(x, y, size, size);

    //展示這個花物件
    [self.view addSubview:flowerImageView];
}
複製程式碼

上面程式碼裡面是生成了500朵位置和大小都是隨機的花內部圖片物件。這500朵花最主要的區別還是它們的位置和大小;而它們使用的花的圖片物件只有6個,因此可以用專門的Factory來生成和管理這些少數的花內部圖片物件,從工廠的列印我們可以看出來:

create new flower image with type:1
create new flower image with type:3
create new flower image with type:4
reuse flower image with type:3
create new flower image with type:5
create new flower image with type:2
create new flower image with type:0
reuse flower image with type:5
reuse flower image with type:5
reuse flower image with type:4
reuse flower image with type:1
reuse flower image with type:3
reuse flower image with type:4
reuse flower image with type:0

複製程式碼

從上面的列印結果可以看出,在六種圖片都建立好以後,再獲取時就直接拿生成過的圖片了,在一定程度上減少了記憶體的開銷。

下面我們來看一下該程式碼示例對應的UML類圖。

程式碼對應的類圖

享元模式程式碼示例類圖

這裡需要注意的是

  • 工廠和花物件是組合關係:FlowerFactroy生成了多個FlowerImageView物件,也就是花的內部圖片物件,二者的關係屬於強關係,因為在該例子中二者如果分離而獨立存在都將會失去意義,所以在UML類圖中用了組合的關係(實心菱形)。
  • 抽象享元類是UIImageView,它的一個內部物件是UIImage(這兩個都是Objective-C原生的關於圖片的類)。
  • 客戶端依賴的物件是工廠物件和花物件,而對花的內部圖片物件UIImage可以一無所知,因為它是被FlowerFactroy建立並被FlowerImageView所持有的。(但是因為UIImageFlowerImageView的一個外部可以引用的屬性,所以在這裡客戶端還是可以訪問到UIImage,這是Objective-C原生的實現。後面我們在用享元模式的時候可以不將內部屬性暴露出來)

優點

  • 使用享元模可以減少記憶體中物件的數量,使得相同物件或相似物件在記憶體中只儲存一份,降低系統的使用記憶體,也可以提效能。
  • 享元模式的外部狀態相對獨立,而且不會影響其內部狀態,從而使得享元物件可以在不同的環境中被共享。

缺點

  • 使用享元模式需要分離出內部狀態和外部狀態,這使得程式的邏輯複雜化。

  • 物件在緩衝池中的複用需要考慮執行緒問題。

Objective-C & Java的實踐

  • iOS SDK中的UITableViewCell的複用池就是使用享元模式的一個例子。
  • Java:JDK中的Integer類的valueOf方法,如果傳入的值的區間在[IntegerCache.low,IntegerCache.high]中的話,則直接從快取裡獲取;否則就建立一個新的Integer

到這裡設計模式中的結構型模式就介紹完了,讀者可以結合UML類圖和demo的程式碼來理解每個設計模式的特點和相互之間的區別,希望讀者可以有所收穫。

另外,本篇部落格的程式碼和類圖都儲存在我的GitHub庫中:object-oriented-design chapter 2.2

該系列前面的兩篇文章:

本篇的下一篇是物件導向系列的第四篇,講解的是物件導向設計模式中的行為型模式。

本篇已同步到個人部落格:物件導向設計的設計模式(二):結構型模式(附 Demo 及 UML 類圖)

參考書籍和教程


筆者在近期開通了個人公眾號,主要分享程式設計,讀書筆記,思考類的文章。

  • 程式設計類文章:包括筆者以前釋出的精選技術文章,以及後續釋出的技術文章(以原創為主),並且逐漸脫離 iOS 的內容,將側重點會轉移到提高程式設計能力的方向上。
  • 讀書筆記類文章:分享程式設計類思考類心理類職場類書籍的讀書筆記。
  • 思考類文章:分享筆者平時在技術上生活上的思考。

因為公眾號每天釋出的訊息數有限制,所以到目前為止還沒有將所有過去的精選文章都發布在公眾號上,後續會逐步釋出的。

而且因為各大部落格平臺的各種限制,後面還會在公眾號上釋出一些短小精幹,以小見大的乾貨文章哦~

掃下方的公眾號二維碼並點選關注,期待與您的共同成長~

物件導向設計的設計模式(二):結構型模式(附 Demo & UML類圖)

相關文章