面向對象設計的設計模式(二):結構型模式(附 Demo & UML類圖)

本篇是面向對象設計系列文章的第三篇,講解的是設計模式中的結構型模式:html

  • 外觀模式
  • 適配器模式
  • 橋接模式
  • 代理模式
  • 裝飾者模式
  • 享元模式

該系列前面的兩篇文章:java

一. 外觀模式

定義

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

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

適用場景

  • 子系統隨着業務複雜度的提高而變得愈來愈複雜,客戶端須要某些子系統共同協做來完成某個任務。
  • 在多層結構的系統中,使用外觀對象能夠做爲每層的入口來簡化層間的調用。

成員與類圖

成員

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

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

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

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

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

模式類圖

外觀模式類圖

上圖中的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 的內容,將側重點會轉移到提升編程能力的方向上。
  • 讀書筆記類文章:分享編程類思考類心理類職場類書籍的讀書筆記。
  • 思考類文章:分享筆者平時在技術上生活上的思考。

由於公衆號天天發佈的消息數有限制,因此到目前爲止尚未將全部過去的精選文章都發布在公衆號上,後續會逐步發佈的。

並且由於各大博客平臺的各類限制,後面還會在公衆號上發佈一些短小精幹,以小見大的乾貨文章哦~

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

相關文章
相關標籤/搜索