1. 程式人生 > >面向物件設計的設計模式(二):結構型模式(附 Demo & UML類圖)

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

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

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

該系列前面的兩篇文章:

一. 外觀模式

定義

外觀模式(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%的中介費,剩下的交給了房東。

程式碼對應的類圖

代理模式程式碼示例類圖