物件導向設計的設計模式(三):行為型模式(附 Demo & UML類圖)

J_Knight_發表於2019-03-18

本篇是物件導向設計系列文章的第四篇,講解的是設計模式中的7個比較常見的行為型模式(按照本文講解順序排列):

  • 模板方法模式
  • 策略模式
  • 責任鏈模式
  • 狀態模式
  • 命令模式
  • 觀察者模式
  • 中介者模式

一. 模板方法模式

定義

在模板模式(Template Method Pattern)中,定義一個操作中的演算法的框架,而將一些步驟的執行延遲到子類中,使得子類可以在不改變演算法的結構的前提下即可重新定義該演算法的某些特定步驟。

適用場景

通常一個演算法需要幾個執行步驟來實現,而有時我們需要定義幾種執行步驟一致,但是卻可能在某個步驟的實現略有差異的演算法。也就是說我們既需要複用實現相同的步驟,也可以通過在某個步驟的不同實現來靈活擴充套件出更多不同的演算法。

在這種場景下,我們可以使用模板方法模式:定義好一個演算法的框架,在父類實現可以複用的演算法步驟,而將需要擴充套件和修改其他步驟的任務推遲給子類進行。

現在我們清楚了模板方法模式的適用場景,下面看一下這個模式的成員和類圖。

成員與類圖

成員

模板方法模式的成員除了客戶端以外,只有兩個成員:

  • 演算法類(Algorithm):演算法類負責宣告演算法介面,演算法步驟介面。並實現可複用的演算法步驟介面,且將需要子類實現的介面暴露出來。
  • 具體演算法類(Concrete Algorithm):具體演算法類負責實現演算法類宣告的演算法步驟介面。

有些參考資料定義這兩個成員為Abstract ClassConcrete Class

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

模式類圖

模板方法模式類圖

由上圖可以看出,Algorithmexcute方法是演算法介面,它在內部呼叫了三個步驟方法:step1,step2,step3。而step2是未暴露在外部的,因為這個步驟是需要各個子類複用的。因此Algorithm只將step1step3暴露了出來以供子類來呼叫。

程式碼示例

場景概述

模擬一個製作三種熱飲的場景:熱美式咖啡,熱拿鐵,熱茶。

場景分析

這三種熱飲的製作步驟是一致的,都是三個步驟:

  • 步驟一:準備熱水
  • 步驟二:加入主成分
  • 步驟三:加入輔助成分(也可以不加,看具體熱飲的種類)

雖然製作步驟是一致的,但是不同種類的熱飲在每一步可能是不同的:咖啡和茶葉主成分是咖啡粉和茶葉;而輔助成分:美式咖啡和茶葉可以不新增,而拿鐵還需新增牛奶。

而第一步是相同的:準備熱水。

根據上面對模板方法模式的介紹,像這樣演算法步驟相同,演算法步驟裡的實現可能相同或不同的場景我們可以使用模板方法模式。下面我們看一下如何用程式碼來模擬該場景。

程式碼實現

首先我們建立演算法類HotDrink

//================== HotDrink.h ==================

@interface HotDrink : NSObject

- (void)makingProcess;

- (void)addMainMaterial;

- (void)addIngredients;

@end



//================== HotDrink.m ==================

@implementation HotDrink

- (void)makingProcess{
    
    NSLog(@" ===== Begin to making %@ ===== ", NSStringFromClass([self class]));
    
    [self boilWater];
    [self addMainMaterial];
    [self addIngredients];
}


- (void)prepareHotWater{
    
    NSLog(@"prepare hot water");
}


- (void)addMainMaterial{
    
    NSLog(@"implemetation by subClasses");
}


- (void)addIngredients{
    
    NSLog(@"implemetation by subClasses");
}


@end
複製程式碼

HotDrink向外部暴露了一個製作過程的介面makingProcess,這個介面內部呼叫了熱飲的所有制作步驟方法:

- (void)makingProcess{
         
     //準備熱水     
    [self prepareHotWater];
    
    //新增主成分
    [self addMainMaterial];
    
    //新增輔助成分
    [self addIngredients];
}
複製程式碼

HotDrink只向外暴露了這三個步驟中的兩個需要子類按照自己方式實現的介面:

//新增主成分
- (void)addMainMaterial;

//新增輔助成分
- (void)addIngredients;
複製程式碼

因為熱飲的第一步都是一致的(準備熱水),所以第一步驟的介面沒有暴露出來給子類實現,而是直接在當前類實現了,這也就是模板方法的一個可以複用程式碼的優點。

OK,我們現在建立好了演算法類,那麼根據上面的需求,我們接著建立三個具體演算法類:

  • HotDrinkTea : 熱茶
  • HotDrinkLatte : 熱拿鐵
  • HotDrinkAmericano: 熱美式
//================== HotDrinkTea.h ==================

@interface HotDrinkTea : HotDrink

@end



//================== HotDrinkTea.m ==================

@implementation HotDrinkTea


- (void)addMainMaterial{
    
    NSLog(@"add tea leaf");
}


- (void)addIngredients{
    
    NSLog(@"add nothing");
}


@end
複製程式碼

熱茶在addMainMaterial步驟裡面是新增了茶葉,而在addIngredients步驟沒有做任何事情(這裡先假定是純的茶葉)。

類似地,我們看一下兩種熱咖啡的實現。首先是熱拿鐵HotDrinkLatte:

//================== HotDrinkLatte.h ==================

@interface HotDrinkLatte : HotDrink

@end



//================== HotDrinkLatte.m ==================

@implementation HotDrinkLatte

- (void)addMainMaterial{
    
    NSLog(@"add ground coffee");
}


- (void)addIngredients{
    
    NSLog(@"add milk");
}


@end
複製程式碼

熱拿鐵在addMainMaterial步驟裡面是新增了咖啡粉,而在addIngredients步驟新增了牛奶。

下面再看一下熱美式HotDrinkAmericano

//================== HotDrinkAmericano.h ==================

@interface HotDrinkAmericano : HotDrink

@end



//================== HotDrinkAmericano.m ==================

@implementation HotDrinkAmericano

- (void)addMainMaterial{
    
    NSLog(@"add ground coffee");
}


- (void)addIngredients{
    
    NSLog(@"add nothing");
}

@end
複製程式碼

熱美式在addMainMaterial步驟裡面是新增了咖啡粉,而在addIngredients步驟沒有做任何事,因為美式就是純的咖啡,理論上除了水和咖啡不需要新增任何其他東西。

到現在三種熱飲類建立好了,我們現在分別製作這三種熱飲,並看一下日至輸出:

===== Begin to making HotDrinkTea =====
prepare hot water
add tea leaf
add nothing
===== Begin to making HotDrinkLatte =====
prepare hot water
add ground coffee
add milk
===== Begin to making HotDrinkAmericano =====
prepare hot water
add ground coffee
add nothing
複製程式碼

上面的日至輸出準確無誤地反映了我們所定義的這三種熱飲製作過程:

  • 熱茶:準備熱水 + 茶葉
  • 熱拿鐵:準備熱水 + 咖啡 + 牛奶
  • 熱美式:準備熱水 + 咖啡

下面看一下上面程式碼對應的類圖。

程式碼對應的類圖

模板方法模式程式碼示例類圖

優點

  • 複用性高:將相同的程式碼放在父類中,而不同的部分則由子類實現
  • 擴充套件性高:可以通過建立不同的子類來擴充套件不同的演算法
  • 符合開閉原則:可變與不可變的部分分離,而且不同的可變部分(子類)也是相互分離的,所以符合了開閉原則

缺點

  • 導致類的個數增加:對於每一個演算法實現都需要一個子類,如果實現過多的話會導致類的個數增加
  • 由繼承關係導致的缺點:如果父類需要增加或減少它的行為,則所有的子類都需要同步修改一次

iOS SDK 和 JDK中的應用

  • 在 iOS SDK 中,我們可以重寫 UIViewdrawRect:方法可以自定義繪圖,是模板方法模式的一種實踐。
  • 在JDK中,java.lang.Runnable是使用JDK的經典場景:Runnable介面可以作為抽象的命令,而實現了Runnable的執行緒即是具體的命令。

二. 策略模式

定義

策略模式(Strategy Pattern):定義一系列演算法,將每一個演算法封裝起來,並讓它們可以相互替換。

適用場景

有時候在實現某一個功能的時可能會有多個方案:我們需要讓系統可以動態靈活地更換方案;而且也能夠讓開發者方便地增加新的方案或刪除舊的方案。

如果我們將所有的方案硬編碼在同一個類中,那麼在今後修改,新增,刪除某個方案的時候就會改動原有類,這是違反開閉原則的。

其實我們可以定義一些獨立的類來封裝不同的解決方案,每一個類封裝一個具體的方案,這些不同的方案就是我們所說的策略。而且我們可以用一個抽象的策略類來保證這些策略的一致性,這就是策略模式的設計方案。

現在我們清楚了策略模式的適用場景,下面看一下策略模式的成員和類圖。

成員與類圖

成員

策略模式除了客戶端之外共有三個成員:

  • 環境類(Context):環境類內部持有一個具體策略類的例項,這個例項就是當前的策略,可以供客戶端使用
  • 抽象策略類(Strategy):抽象策略類宣告具體策略類需要實現的介面,這個介面同時也是提供給客戶端呼叫的介面
  • 具體策略類(Concrete Strategy):具體策略類實現抽象策略類宣告的介面,每個具體策略類都有自己獨有的實現方式,即代表不同策略

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

模式類圖

策略模式類圖

程式碼示例

場景概述

模擬一個兩個整數可以隨意替換加減乘除演算法的場景。

場景分析

在該場景中,傳入的兩個整數引數是不變的,但是對於這兩個整數的具體操作可以靈活切換,那麼我們可以使用策略模式:將每個操作(演算法)封裝起來,在需要替換的時候將Context類持有的具體策略例項更新即可。

程式碼實現

首先我們定義好抽象策略類和具體策略類:

因為是針對兩個整數的操作,所以在抽象策略類中,我們只需定義一個傳入兩個整數的介面即可。

抽象策略類TwoIntOperation:

//================== TwoIntOperation.h ==================

@interface TwoIntOperation : NSObject

- (int)operationOfInt1:(int)int1 int2:(int)int2;

@end



//================== TwoIntOperation.m ==================

@implementation  TwoIntOperation

- (int)operationOfInt1:(int)int1 int2:(int)int2{
    
    //implenting by sub classes;
    return 0;
}

@end
複製程式碼

接著我們根據加減乘除四種運算,來分別定義四個具體策略類:

加法TwoIntOperationAdd

//================== TwoIntOperationAdd.h ==================

@interface TwoIntOperationAdd : TwoIntOperation

@end



//================== TwoIntOperationAdd.m ==================

@implementation TwoIntOperationAdd

- (int)operationOfInt1:(int)int1 int2:(int)int2{
    
    NSLog(@"==== adding ====");
    
    return int1 + int2;
}

@end
複製程式碼

減法TwoIntOperationSubstract

//================== TwoIntOperationSubstract.h ==================

@interface TwoIntOperationSubstract : TwoIntOperation

@end



//================== TwoIntOperationSubstract.m ==================

@implementation TwoIntOperationSubstract

- (int)operationOfInt1:(int)int1 int2:(int)int2{
    
    NSLog(@"==== Substract ====");
    return int1 - int2;
}
@end
複製程式碼

乘法TwoIntOperationMultiply:

//================== TwoIntOperationMultiply.h ==================

@interface TwoIntOperationMultiply : TwoIntOperation

@end



//================== TwoIntOperationMultiply.m ==================

@implementation TwoIntOperationMultiply

- (int)operationOfInt1:(int)int1 int2:(int)int2{
    
    NSLog(@"==== multiply ====");
    
    return int1 * int2;
}

@end
複製程式碼

除法TwoIntOperationDivision:

//================== TwoIntOperationDivision.h ==================

@interface TwoIntOperationDivision : TwoIntOperation

@end



//================== TwoIntOperationDivision.m ==================

@implementation TwoIntOperationDivision

- (int)operationOfInt1:(int)int1 int2:(int)int2{
    
    NSLog(@"==== division ====");
    return int1/int2;
}

@end
複製程式碼

現在關於演算法的類都宣告好了,我們最後宣告一下 Context 類:

//================== Context.h ==================

@interface Context : NSObject

- (instancetype)initWithOperation: (TwoIntOperation *)operation;

- (void)setOperation:(TwoIntOperation *)operation;

- (int)excuteOperationOfInt1:(int)int1 int2:(int)int2;

@end



//================== Context.m ==================

@implementation Context
{
    TwoIntOperation *_operation;
}

- (instancetype)initWithOperation: (TwoIntOperation *)operation{

    self = [super init];
    if (self) {
        //injection from instane initialization
        _operation = operation;
    }
    return self;
}

- (void)setOperation:(TwoIntOperation *)operation{
    
    //injection from setting method
    _operation = operation;
}

- (int)excuteOperationOfInt1:(int)int1 int2:(int)int2{
    
    //return value by constract strategy instane
    return [_operation operationOfInt1:int1 int2:int2];
}

@end
複製程式碼

Context類在構造器(init方法)注入了一個具體策略例項並持有它,而且Context也提供了set方法,讓外部注入進來具體策略類的例項。

而策略的具體執行是通過Context的介面excuteOperationOfInt1:int2。這個介面是提供給客戶端呼叫的;而且在它的內部其實呼叫的是當前持有的策略例項的執行策略的方法。

所以如果想使用哪種策略,只要將具體策略的例項傳入到Context例項即可。

現在所有的類都定義好了,下面我們看一下具體如何使用:

int int1 = 6;
int int2 = 3;
    
NSLog(@"int1: %d    int2: %d",int1,int2);
    
//Firstly, using add operation
TwoIntOperationAdd *addOperation = [[TwoIntOperationAdd alloc] init];
Context *ct = [[Context alloc] initWithOperation:addOperation];
int res1 = [ct excuteOperationOfInt1:int1 int2:int2];
NSLog(@"result of adding : %d",res1);
    
//Changing to multiple operation
TwoIntOperationMultiply *multiplyOperation = [[TwoIntOperationMultiply alloc] init];
[ct setOperation:multiplyOperation];
int res2 = [ct excuteOperationOfInt1:int1 int2:int2];
NSLog(@"result of multiplying : %d",res2);
    
    
//Changing to substraction operation
TwoIntOperationSubstract *subOperation = [[TwoIntOperationSubstract alloc] init];
[ct setOperation:subOperation];
int res3 = [ct excuteOperationOfInt1:int1 int2:int2];
NSLog(@"result of substracting : %d",res3);
    
    
//Changing to division operation
TwoIntOperationDivision *divisionOperation = [[TwoIntOperationDivision alloc] init];
[ct setOperation:divisionOperation];
int res4 = [ct excuteOperationOfInt1:int1 int2:int2];
NSLog(@"result of dividing : %d",res4);
複製程式碼

看一下日至輸出:

[13431:1238320] int1: 6    int2: 3
[13431:1238320] ==== adding ====
[13431:1238320] result of adding : 9
[13431:1238320] ==== multiply ====
[13431:1238320] result of multiplying : 18
[13431:1238320] ==== Substract ====
[13431:1238320] result of substracting : 3
[13431:1238320] ==== division ====
[13431:1238320] result dividing : 2
複製程式碼

在上面的例子中,首先我們要使用加法,所以 例項化了加法策略類並傳入到了Context類的構造器中。

而後續的乘法,減法,除法的更換,則是分別將它們的策略例項傳入到了Context的set方法中,並執行即可。

下面看一下上面程式碼對應的類圖。

程式碼對應的類圖

策略模式程式碼示例類圖

優點

  • 策略模式遵循開閉原則,使用者可以在不修改原有系統的前提下選擇和更換演算法
  • 避免使用多重條件判斷
  • 可以靈活地增加新的演算法或行為
  • 提高演算法和策略的安全性:可以封裝策略的具體實現,呼叫者只需要知道不同策略之間的區別就可以

缺點

  • 客戶端必須知道當前所有的具體策略類,而且需要自行決定使用哪一個策略類
  • 如果可選的方案過多,會導致策略類數量激增。

iOS SDK 和 JDK中的應用

  • JDK中的Comparator是策略模式的實現,可以使用不同的子類,也就是具體策略來解決不同的需求。

三. 責任鏈模式

定義

責任鏈模式(Chain of Responsibility Pattern):為請求建立了一個接收者物件的鏈,每個接收者都包含對另一個接收者的引用。如果一個物件不能處理該請求,那麼它會把相同的請求傳給下一個接收者,依此類推。

適用場景

在處理某個請求的時候,解決策略因條件不同而不同。這時,相對於使用if-else來區分不同的條件和對應的解決策略,我們可以使用責任鏈模式,將不同條件和對應的解決策略封裝到一個類中,即不同的處理者。然後將這些處理者組成責任鏈,在當前處理者無法處理或不符合當前條件時,將請求傳遞給下一個處理者。

現在我們清楚了責任鏈模式的適用場景,下面看一下責任鏈模式的成員和類圖。

成員與類圖

成員

責任鏈模式的結構比較簡單,不包括客戶端只有兩個成員:

  • 處理者(Handler):處理者定義處理請求的介面
  • 具體處理者(Concrete Handler): 具體處理者實現處理者宣告的介面,負責處理請求

模式類圖

責任鏈模式類圖

程式碼示例

場景概述

模擬一個 ATM 取現金的場景:ATM機器有50,20,10面值的紙幣,根據使用者需要提取的現金金額來輸出紙幣張數最少的等價金額的紙幣。

比如使用者需要取130元,則ATM需要輸出2張50面額的紙幣,1張20面額的紙幣,1張10面額的紙幣;而不是6張20面額的紙幣加1張10面額的紙幣。

場景分析

顯然,為了輸出最少張數的紙幣,ATM在計算的時候是從面額最大的紙幣開始計算的。

如果不使用責任鏈模式,我們可能會寫一個do-while迴圈,在迴圈裡面再根據紙幣的面額在做if-else判斷,不斷去嘗試直到將面額除盡(沒有餘數)。但是如果未來面額的數值發生變化,或者新增新的面額的紙幣的話,我們還需要更改判斷條件或增加if-else語句,這顯然違反了開閉原則。

但是如果使用責任鏈模式,我們將每個面值的紙幣當做責任鏈中的一個處理者(節點,node),自成一類,單獨做處理。然後將這些處理者按照順序連線起來(50,20,10),按照順序對使用者輸入的數值進行處理即可。

這樣做的好處是,如果以後修改面值或新增一種新的面值,我們只需要修改其中某一個處理者或者新建一個處理者類,再重新插入到責任鏈的合適的位置即可。

下面我們看一下如何用程式碼來模擬該場景。

程式碼實現

首先建立抽象處理者DispenseChainNode:

//================== DispenseChainNode.h ==================

@interface DispenseChainNode : NSObject <DispenseProtocol>
{
    @protected DispenseChainNode *_nextChainUnit;
}

- (void)setNextChainUnit:(DispenseChainNode *)chainUnit;

@end



//================== DispenseChainNode.m ==================

@implementation DispenseChainNode

- (void)setNextChainNode:(DispenseChainNode *)chainNode{
    
    _nextChainNode = chainNode;
}

- (void)dispense:(int)amount{
    
    return;
}

@end
複製程式碼
  • DispenseChainNode是責任鏈節點,也就是具體處理者的父類,它持有DispenseChainNode的例項,用來儲存當前節點的下一個節點。這個下一個節點的例項是通過setNextChainNode:方法注入進來的 而且,DispenseChainNode遵循<DispenseProtocol>協議,這個協議只有一個方法,就是dispense:方法,每個節點都實現這個方法來對輸入的金額做處理。(dispense 單詞的意思是分配,分發)

現在我們根據需求,建立具體處理者,也就是針對50,20,10面額的具體處理者:

50面額的具體處理者:

//================== DispenseChainNodeFor50Yuan.h ==================

@interface DispenseChainNodeFor50Yuan : DispenseChainNode

@end



//================== DispenseChainNodeFor50Yuan.m ==================

@implementation DispenseChainNodeFor50Yuan

- (void)dispense:(int)amount{
    
    int unit = 50;
    
    if (amount >= unit) {
        
        int count = amount/unit;
        int remainder = amount % unit;
        
        NSLog(@"Dispensing %d of %d",count,unit);
        
        if (remainder != 0) {
            [_nextChainNode dispense:remainder];
        }
        
    }else{
        
        [_nextChainNode dispense:amount];
    }
}


@end
複製程式碼

20面額的具體處理者:

//================== DispenseChainNodeFor20Yuan.h ==================

@interface DispenseChainNodeFor20Yuan : DispenseChainNode

@end



//================== DispenseChainNodeFor20Yuan.m ==================

@implementation DispenseChainNodeFor20Yuan

- (void)dispense:(int)amount{
    
    int unit = 20;
    
    if (amount >= unit) {
        
        int count = amount/unit;
        int remainder = amount % unit;
        
        NSLog(@"Dispensing %d of %d",count,unit);
        
        if (remainder != 0) {
            [_nextChainNode dispense:remainder];
        }
        
    }else{
        
        [_nextChainNode dispense:amount];
    }
}


@end
複製程式碼

10面額的具體處理者:

//================== DispenseChainNodeFor10Yuan.h ==================

@interface DispenseChainNodeFor10Yuan : DispenseChainNode

@end



//================== DispenseChainNodeFor10Yuan.m ==================

@implementation DispenseChainNodeFor10Yuan

- (void)dispense:(int)amount{
    
    int unit = 10;
    
    if (amount >= unit) {
        
        int count = amount/unit;
        int remainder = amount % unit;
        
        NSLog(@"Dispensing %d of %d",count,unit);
        
        if (remainder != 0) {
            [_nextChainNode dispense:remainder];
        }
        
    }else{
        
        [_nextChainNode dispense:amount];
    }
}

@end
複製程式碼

上面三個具體處理者在dispense:方法的處理都是類似的:

首先檢視當前值是否大於面額

  • 如果大於面額
    • 將當前值除以當前面額
      • 如果沒有餘數,則停止,不作處理
      • 如果有餘數,則繼續將當前值傳遞給下一個具體處理者(責任鏈的下一個節點)
  • 如果小於面額:將當前值傳遞給下一個具體處理者(責任鏈的下一個節點)

現在我們建立好了三個具體處理者,我們再建立一個ATM類來把這些節點串起來:

//================== ATMDispenseChain.h ==================

@interface ATMDispenseChain : NSObject<DispenseProtocol>

@end



//================== ATMDispenseChain.m ==================

@implementation ATMDispenseChain
{
    DispenseChainNode *_chainNode;
}

- (instancetype)init{
    
    self = [super init];
    if(self){
        
        DispenseChainNodeFor50Yuan *chainNode50 = [[DispenseChainNodeFor50Yuan alloc] init];
        DispenseChainNodeFor20Yuan *chainNode20 = [[DispenseChainNodeFor20Yuan alloc] init]; 
        DispenseChainNodeFor10Yuan *chainNode10 = [[DispenseChainNodeFor10Yuan alloc] init];
        
         _chainNode = chainNode50;
        [_chainNode setNextChainNode:chainNode20];
        [chainNode20 setNextChainNode:chainNode10];
        
    }
    
    return self;
    
}



- (void)dispense:(int)amount{
    
    NSLog(@"==================================");
    NSLog(@"ATM start dispensing of amount:%d",amount);
    
    if (amount %10 != 0) {
        NSLog(@"Amount should be in multiple of 10");
        return;
    }

    [_chainNode dispense:amount];
    
}

@end
複製程式碼

ATMDispenseChain這個類在初始化的時候就將三個具體處理者並按照50,20,10的順序連線起來,並持有一個DispenseChainNode的指標指向當前的具體處理者(也就是責任鏈的第一個節點,面額50的具體處理者,因為面額的處理是從50開始的)。

OK,現在我們把三個具體處理者都封裝好了,可以看一下如何使用:

ATMDispenseChain *atm = [[ATMDispenseChain alloc] init];
    
[atm dispense:230];
    
[atm dispense:70];
    
[atm dispense:40];
    
[atm dispense:10];

[atm dispense:8];
複製程式碼

建立ATMDispenseChain的例項後,分別傳入一些數值來看一下處理的結果:

==================================
ATM start dispensing of amount:230
Dispensing 4 of 50
Dispensing 1 of 20
Dispensing 1 of 10
==================================
ATM start dispensing of amount:70
Dispensing 1 of 50
Dispensing 1 of 20
==================================
ATM start dispensing of amount:40
Dispensing 2 of 20
==================================
ATM start dispensing of amount:10
Dispensing 1 of 10
==================================
ATM start dispensing of amount:8
Amount should be in multiple of 10
複製程式碼

從日誌的輸出可以看出,我們的責任鏈處理是沒有問題的,針對每個不同的數值,ATMDispenseChain例項都作出了最正確的結果。

需要注意的是,該程式碼示例中的責任鏈類(ATMDispenseChain)並沒有在上述責任鏈模式的成員中。不過此處不必做過多糾結,我們在這裡只是在業務上稍微多做一點處理罷了。其實也完全可以不封裝這些節點,直接逐個呼叫setNextChainNode:方法組裝責任鏈,然後將任務交給第一個處理者即可。

需求完成了,是否可以做個重構?

我們回去看一下這三個具體處理者在dispense:方法的處理是非常相似的,他們的區別只有處理的面額數值的不同:而我們其實是建立了針對這三個面值的類,並將面值(50,20,10)硬編碼在了這三個類中。這樣做是有缺點的,因為如果後面的面額大小變了,或者增加或者減少面額的話我們會修改這些類或新增刪除這些類(即使這也比不使用責任鏈模式的if-else要好一些)。

因此我們可以不建立這些與面額值硬編碼的具體處理類,而是在初始化的時候直接將面額值注入到構造方法裡面即可!這樣一來,我們可以隨意調整和修改面額了。下面我們做一下這個重構:

首先刪除掉三個具體處理者DispenseChainNodeFor50Yuan,DispenseChainNodeFor20Yuan,DispenseChainNodeFor10Yuan

接著在DispenseChainNode新增傳入面額值的初始化方法以及面額值的成員變數:

//================== ADispenseChainNode.h ==================

@interface DispenseChainNode : NSObject <DispenseProtocol>
{
    @protected DispenseChainNode *_nextChainNode;
    @protected int _dispenseValue;
}

- (instancetype)initWithDispenseValue:(int)dispenseValue;

- (void)setNextChainNode:(DispenseChainNode *)chainNode;


@end



//================== ADispenseChainNode.m ==================

@implementation DispenseChainNode

- (instancetype)initWithDispenseValue:(int)dispenseValue
{
    self = [super init];
    if (self) {
        _dispenseValue = dispenseValue;
    }
    return self;
}

- (void)setNextChainNode:(DispenseChainNode *)chainNode{
    
    _nextChainNode = chainNode;
}

- (void)dispense:(int)amount{
    
    if (amount >= _dispenseValue) {
        
        int count = amount/_dispenseValue;
        int remainder = amount % _dispenseValue;
        
        NSLog(@"Dispensing %d of %d",count,_dispenseValue);
        
        if (remainder != 0) {
            [_nextChainNode dispense:remainder];
        }
        
    }else{
        
        [_nextChainNode dispense:amount];
    }
}

@end
複製程式碼

我們給DispenseChainNode新增了initWithDispenseValue:方法後,就可以根據需求隨意生成不同面額的具體處理者了。

接著我們思考一下之前的ATMDispenseChain可以做哪些改變?

既然DispenseChainNode可以根據不同的面額值生成處理不同面額的具體處理者例項,那麼對於串聯多個具體處理者的類ATMDispenseChain是不是也可以新增一個注入面額陣列的初始化方法呢?比如輸入[50,20,10]的陣列就可以生成50,20,10面額的具體處理者了;而且陣列是有序的,傳入陣列的元素順序就可以是責任鏈中節點的順序。

思路有了,我們看一下具體實現:

//================== ATMDispenseChain.m ==================

@implementation ATMDispenseChain
{
    DispenseChainNode *_firstChainNode;
    DispenseChainNode *_finalChainNode;
    int _minimumValue;
}


- (instancetype)initWithDispenseNodeValues:(NSArray *)nodeValues{
    
    self = [super init];
    
    if(self){
        
        NSUInteger length = [nodeValues count];
        
        [nodeValues enumerateObjectsUsingBlock:^(NSNumber * nodeValue, NSUInteger idx, BOOL * _Nonnull stop) {
            
            DispenseChainNode *iterNode = [[DispenseChainNode alloc] initWithDispenseValue:[nodeValue intValue]];
            
            if (idx == length - 1 ) {
                _minimumValue = [nodeValue intValue];
            }
            
            if (!self->_firstChainNode) {
                
                 //because this chain is empty, so the first node and the final node will refer the same node instance
                 self->_firstChainNode =  iterNode;
                 self->_finalChainNode =  self->_firstChainNode;
                
            }else{
                
                //appending the next node, and setting the new final node
                [self->_finalChainNode setNextChainNode:iterNode];
                 self->_finalChainNode = iterNode;
            }
        }];
    }
    
    return self;
}


- (void)dispense:(int)amount{
    
    NSLog(@"==================================");
    NSLog(@"ATM start dispensing of amount:%d",amount);
    
    if (amount % _minimumValue != 0) {
        NSLog(@"Amount should be in multiple of %d",_minimumValue);
        return;
    }

    [ _firstChainNode dispense:amount];
    
}

@end
複製程式碼

重構後的ATMDispenseChain類新增了initWithDispenseNodeValues:方法,需要從外部傳入面額值的陣列。在這個方法裡面根據傳入的陣列構造了整條責任鏈。

而在dispense:方法裡面則是從責任鏈的第一個節點來處理面額,並在方法最前面取最小面額的值來做邊界處理。

OK,到現在處理者類和責任鏈類都建立好了,我們看一下如何使用:

NSArray *dispenseNodeValues = @[@(100),@(50),@(20),@(10)];

ATMDispenseChain *atm = [[ATMDispenseChain alloc] initWithDispenseNodeValues:dispenseNodeValues];
    
[atm dispense:230];
    
[atm dispense:70];
    
[atm dispense:40];
    
[atm dispense:10];
    
[atm dispense:8];
複製程式碼

是不是感覺簡潔多了?我們只需要傳入一個面額值的陣列即可構造出整條責任鏈並直接使用。來看一下日至輸出:

==================================
ATM start dispensing of amount:230
Dispensing 2 of 100
Dispensing 1 of 20
Dispensing 1 of 10
==================================
ATM start dispensing of amount:70
Dispensing 1 of 50
Dispensing 1 of 20
==================================
ATM start dispensing of amount:40
Dispensing 2 of 20
==================================
ATM start dispensing of amount:10
Dispensing 1 of 10
==================================
ATM start dispensing of amount:8
Amount should be in multiple of 10
複製程式碼

從日誌的輸出結果上看,我們重構後的責任鏈方案沒有問題。

下面看一下上面程式碼對應的類圖。

程式碼對應的類圖

重構前:

責任鏈模式程式碼示例類圖一

重構後:

責任鏈模式程式碼示例類圖二

優點

  • 處理者之間的責任分離,處理者只要處理好自己的邏輯即可
  • 方便修改每個處理者的處理邏輯,也方便刪除或者新增處理者,或者改變責任鏈中處理者的順序。

缺點

  • 因為需要在責任鏈上傳遞責任,直到找到合適的物件來處理,所以可能會導致處理的延遲。因此在延遲不允許過高的場景下不適合使用責任鏈模式。

iOS SDK 和 JDK中的應用

  • iOS SDK中的響應者鏈就是責任鏈模式的實踐:如果當前檢視無法響應則傳遞給下一層級檢視。
  • servlet中的Filter可以組成FilterChain,是責任鏈模式的一種實踐。

四. 狀態模式

定義

在狀態模式(State Pattern):允許一個物件在其內部狀態改變時,改變它的行為。

適用場景

一個物件存在多個狀態,不同狀態下的行為會有不同,而且狀態之間可以相互轉換。

如果我們通過if else來判斷物件的狀態,那麼程式碼中會包含大量與物件狀態有關的條件語句,而且在新增,刪除和更改這些狀態的時候回比較麻煩;而如果使用狀態模式。將狀態物件分散到不同的類中,則可以消除 if...else等條件選擇語句。

現在我們清楚了狀態模式的適用場景,下面看一下狀態模式的成員和類圖。

成員與類圖

成員

狀態模式一共只有四個成員:

  • 環境類(Context):環境類引用了具體狀態的例項。環境類持有的具體狀態就是當前的狀態,可以通過 set 方法將狀態例項注入到環境類中。
  • 抽象狀態類(State):抽象狀態類宣告具體狀態類需要實現的介面。
  • 具體狀態類(Concrete State):具體狀態類實現抽象狀態類宣告的介面。

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

模式類圖

狀態模式類圖

程式碼示例

場景概述

模擬一個程式設計師一天的生活,他有四個狀態:

  1. 醒著
  2. 睡覺中
  3. 寫程式碼中
  4. 吃飯中

看這幾個狀態應該是個非常愛寫程式碼的程式設計師 ^ ^

場景分析

這個程式設計師有四個狀態,但是有些狀態之間是無法切換的:比如從睡覺是無法切換到寫程式碼的(因為需要切換到醒著,然後才能到寫程式碼);從吃飯中是無法切換到醒著的,因為已經醒著了。

如果我們不使用狀態模式,在切換狀態的時候可能會寫不少if-else判斷,而且隨著狀態的增多,這些分支會變得更多,難以維護。

而如果我們使用狀態模式,則可以將每個狀態封裝到一個類中,便於管理;而且在增加或減少狀態時也會很方便。

下面我們看一下如何用程式碼來模擬該場景。

程式碼實現

首先我們定義狀態類:

//================== State.h ==================

@interface State : NSObject<ActionProtocol>
{
    @protected Coder *_coder;
}

- (instancetype)initWithCoder:(Coder *)coder;

@end



//================== State.m ==================

@implementation State

- (instancetype)initWithCoder:(Coder *)coder{
    
    self = [super init];
    if (self) {
        _coder = coder;
    }
    return self;
}

@end
複製程式碼

狀態類持有一個coder,也就是程式設計師的例項,並遵循了ActionProtocol

//================== ActionProtocol.h ==================

@protocol ActionProtocol <NSObject>

@optional;

- (void)wakeUp;

- (void)fallAsleep;

- (void)startCoding;

- (void)startEating;

@end
複製程式碼

ActionProtocol定義了程式設計師的一些動作,這些動作是程式設計師的日常活動,也是觸發狀態切換的動作,因此State也需要遵循這個協議,因為它的子類需要實現這些操作。

接下來我們看一下State的子類,根據上面說的四種狀態,我們定義下面四個狀態子類:

StateAwake:

//================== StateAwake.h ==================

@interface StateAwake : State

@end

@implementation StateAwake

- (void)wakeUp{
    
    NSLog(@"Already awake, can not change state to awake again");
}

- (void)startCoding{
    
    NSLog(@"Change state from awake to coding");
    [_coder setState:(State *)[_coder stateCoding]];
}

- (void)startEating{
    
    NSLog(@"Change state from awake to eating");
    [_coder setState:(State *)[_coder stateEating]];
}


- (void)fallAsleep{
    
    NSLog(@"Change state from awake to sleeping");
    [_coder setState:(State *)[_coder stateSleeping]];
}

@end
複製程式碼

StateSleeping:

//================== StateSleeping.h ==================

@interface StateSleeping : State

@end



//================== StateSleeping.m ==================

@implementation StateSleeping

- (void)wakeUp{
    
    NSLog(@"Change state from sleeping to awake");
    [_coder setState:(State *)[_coder stateAwake]];
}


- (void)startCoding{
    
    NSLog(@"Already sleeping, can not change state to coding");
}

- (void)startEating{
    
    NSLog(@"Already sleeping, can change state to eating");
}


- (void)fallAsleep{
    
    NSLog(@"Already sleeping, can not change state to sleeping again");
}

@end
複製程式碼

StateEating:

//================== StateEating.h ==================

@interface StateEating : State

@end



//================== StateEating.m ==================

@implementation StateEating

- (void)wakeUp{
    
    NSLog(@"Already awake, can not change state to awake again");
}


- (void)startCoding{
    
    NSLog(@"New idea came out! change state from eating to coding");
    [_coder setState:(State *)[_coder stateCoding]];
}

- (void)startEating{
    
    NSLog(@"Already eating, can not change state to eating again");
}


- (void)fallAsleep{
    
    NSLog(@"Too tired, change state from eating to sleeping");
    [_coder setState:(State *)[_coder stateSleeping]];
}



@end
複製程式碼

"StateCoding":

//================== StateCoding.h ==================

@interface StateCoding : State

@end



//================== StateCoding.m ==================

@implementation StateCoding

- (void)wakeUp{
    
    NSLog(@"Already awake, can not change state to awake again");
}


- (void)startCoding{
    
    NSLog(@"Already coding, can not change state to coding again");
}

- (void)startEating{
    
    NSLog(@"Too hungry, change state from coding to eating");
    [_coder setState:(State *)[_coder stateEating]];
}


- (void)fallAsleep{
    
    NSLog(@"Too tired, change state from coding to sleeping");
    [_coder setState:(State *)[_coder stateSleeping]];
}

@end
複製程式碼

從上面的類可以看出,在有些狀態之間的轉換是失效的,有些是可以的。 比如相同狀態的切換是無效的;從 sleeping無法切換到coding,但是反過來可以,因為可能寫程式碼累了就直接睡了。

下面我們看一下程式設計師類的實現:

//================== Coder.h ==================

@interface Coder : NSObject<ActionProtocol>

@property (nonatomic, strong) StateAwake *stateAwake;
@property (nonatomic, strong) StateCoding *stateCoding;
@property (nonatomic, strong) StateEating *stateEating;
@property (nonatomic, strong) StateSleeping *stateSleeping;

- (void)setState:(State *)state;

@end



//================== Coder.m ==================

@implementation Coder
{
    State *_currentState;
}

- (instancetype)init{
    
    self = [super init];
    if (self) {
        
        _stateAwake = [[StateAwake alloc] initWithCoder:self];
        _stateCoding = [[StateCoding alloc] initWithCoder:self];
        _stateEating = [[StateEating alloc] initWithCoder:self];
        _stateSleeping = [[StateSleeping alloc] initWithCoder:self];
        
        _currentState = _stateAwake;
    }
    return self;
}

- (void)setState:(State *)state{
    
    _currentState = state;
}

- (void)wakeUp{
    
    [_currentState wakeUp];
}

- (void)startCoding{
    
    [_currentState startCoding];
}

- (void)startEating{
    
    [_currentState startEating];
}


- (void)fallAsleep{
    
    [_currentState fallAsleep];
}

@end
複製程式碼

從上面的程式碼我們可以看到,程式設計師類持有一個當前的狀態的例項,在初始化後預設的狀態為awake,並對外提供一個setState:的方法來切換狀態。而且在初始化方法裡,我們例項化了所有的狀態,目的是在切換狀態中時使用,詳見具體狀態類的方法:

- (void)startEating{
    
    NSLog(@"Too hungry, change state from coding to eating");
    [_coder setState:(State *)[_coder stateEating]];
}
複製程式碼

上面這段程式碼有點繞,可能需要多看幾遍原始碼才能理解(這裡面[_coder stateEating]是呼叫了coder的一個get方法,返回了stateEating這個例項)。

最後,在程式設計師的動作方法裡面,實際上呼叫的是當前狀態對應的方法(這也就是為何程式設計師類和狀態類都要遵循ActionProtocol的原因)。

這樣,我們的狀態類,狀態子類,程式設計師類都宣告好了。我們看一下如何使用:

Coder *coder = [[Coder alloc] init];
    
//change to awake.. failed
[coder wakeUp];//Already awake, can not change state to awake again
    
//change to coding
[coder startCoding];//Change state from awake to coding
    
//change to sleep
[coder fallAsleep];//Too tired, change state from coding to sleeping
    
//change to eat...failed
[coder startEating];//Already sleeping, can change state to eating
    
//change to wake up
[coder wakeUp];//Change state from sleeping to awake

//change wake up...failed
[coder wakeUp];//Already awake, can not change state to awake again
    
//change to eating
[coder startEating];//Change state from awake to eating
    
//change to coding
[coder startCoding];//New idea came out! change state from eating to coding
    
//change to sleep
[coder fallAsleep];//Too tired, change state from coding to sleeping
複製程式碼

在上面的程式碼裡,我們例項化了一個程式設計師類,接著不斷呼叫一些觸發狀態改變的方法。我們把每次狀態切換的日至輸出註釋到了程式碼右側,可以看到在一些狀態的切換是不允許的:

  • 比如從上到下的第一個[coder wakeUp]:因為程式設計師物件初始化後預設是awake狀態,所以無法切換到相同的狀態
  • 比如從上到下的第一個[coder startEating]:在睡覺時是無法直接切換到eating狀態;而在後面wake以後,再執行[coder startEating]就成功了。

從上面的例子可以看出,使用狀態模式不需要去寫if-else,而且如果今後想新增一個狀態,只需要再建立一個狀態子類,並在新的狀態子類新增好對所有狀態的處理,並在之前的狀態子類中新增上對新狀態的處理即可。即便我們修改了之前定義好的狀態子類,但是這樣也總比使用龐大的if-else要方便多。

下面看一下上面程式碼對應的類圖。

程式碼對應的類圖

狀態模式程式碼示例類圖

優點

  1. 把各種狀態的轉換邏輯,分佈到不同的類中,減少相互間的依賴。

缺點

  1. 增加新的狀態類需要修改狀態轉換的原始碼,而且增加新的行為也要修改原來的狀態類(前提是新的行為和原來的狀態有關係)。
  2. 過多的狀態會增加系統中的類的個數,增加系統的複雜性。

iOS SDK 和 JDK中的應用

  • javax包下的LifyCycle是狀態模式的一種實現

五. 命令模式

定義

命令模式(Command Pattern):命令(或請求)被封裝成物件。客戶端將命令(或請求)物件先傳遞給呼叫物件。呼叫物件再把該命令(或請求)物件傳給合適的,可處理該命令(或請求)的物件來做處理。

由定義可以看出,在命令模式中,命令被封裝成了物件,而傳送命令的客戶端與處理命令的接收者中間被呼叫物件隔開了,這種設計的原因或者適用的場景是什麼樣的呢?

適用場景

在有些場景下,任務的處理可能不是需要立即執行的:可能需要記錄(日至),撤銷或重試(網路請求)。那麼在這些場景下,如果任務的請求者和執行者是緊耦合狀態下的話就可能會將很多其他執行策略的程式碼和立即執行的程式碼混合到一起。

這些其他執行策略,我們暫時稱之為控制和管理策略,而如果我們如果想控制和管理請求,就需要:

  1. 把請求抽象出來
  2. 讓另外一個角色來負責控制和管理請求的任務

因此命令模式就是為此場景量身打造的,它通過:

  1. 把請求封裝成物件
  2. 使用呼叫者在客戶端和請求處理者之間來做一個“攔截”,方便對請求物件做控制和管理。

現在我們清楚了命令模式的適用場景,下面看一下命令模式的成員和類圖。

成員與類圖

成員

不包括請求的發起者(客戶端),命令模式共有四個成員:

  • 抽象命令類(Command):命令類負責宣告命令的介面。
  • 具體命令類(Concrete Command):具體命令類負責實現抽象命令類宣告的介面
  • 呼叫者(Invoker):呼叫者負責將具體命令類的例項傳遞給接收者
  • 接收者(Receiver):接收者負責處理命令

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

模式類圖

命令模式類圖

程式碼示例

場景概述

模擬一個使用遙控器開燈和關燈的例子。

場景分析

在這個例子中,使用遙控器的人就是客戶端,TA發起開啟或關閉燈的命令給遙控器(呼叫者)。然後呼叫者將命令傳遞給接收者(燈)。

在這裡,人是不直接接觸燈的,開啟和關閉的命令是通過遙控器來做的轉發,最後傳達給燈來執行。

下面我們看一下如何用程式碼來模擬該場景。

程式碼實現

首先我們建立接收者,燈類:

//================== Light.h ==================

@interface Light : NSObject

- (void)lightOn;

- (void)lightOff;

@end



//================== Light.m ==================

@implementation Light


- (void)lightOn{
    
    NSLog(@"Light on");
}


- (void)lightOff{
    
    NSLog(@"Light off");
}

@end
複製程式碼

燈類宣告並實現了兩個介面:開燈介面和關燈介面,來讓外部執行開燈和關燈的操作。

接著我們建立抽象命令類和具體命令類:

抽象命令類:

//================== Command.h ==================

@interface Command : NSObject

- (void)excute;

@end



//================== Command.m ==================

@implementation Command

@end
複製程式碼

抽象命令類宣告瞭一個執行命令的介面excute,這個介面由它的子類,也就是具體命令類來實現。

因為這裡面只有開燈和關燈兩種命令,所以我們建立兩個具體命令類來繼承上面的抽象命令類:

開燈命令CommandLightOn

//================== CommandLightOn.h ==================

@interface CommandLightOn : Command

- (instancetype)initWithLight:(Light *)light;

@end


//================== CommandLightOn.m ==================

@implementation CommandLightOn
{
    Light *_light;
}

- (instancetype)initWithLight:(Light *)light{
    
    self = [super init];
    if (self) {
        _light = light;
    }
    return self;
}

- (void)excute{
    
    [_light lightOn];
}
複製程式碼

關燈命令CommandLightOff

//================== CommandLightOff.h ==================

@interface CommandLightOff : Command

- (instancetype)initWithLight:(Light *)light;

@end



//================== CommandLightOff.m ==================
@implementation CommandLightOff
{
    Light *_light;
}

- (instancetype)initWithLight:(Light *)light{
    
    self = [super init];
    if (self) {
        _light = light;
    }
    return self;
}

- (void)excute{
    
    [_light lightOff];
}
複製程式碼

我們可以看到這兩個具體命令類分別以自己的方式實現了它們的父類宣告的excute介面。

最後我們建立連結客戶端和接收者的呼叫者類,也就是遙控器類RemoteControl

//================== RemoteControl.h ==================

@interface RemoteControl : NSObject

- (void)setCommand:(Command *)command;

- (void)pressButton;

@end



//================== RemoteControl.m ==================

@implementation RemoteControl
{
    Command *_command;
}


- (void)setCommand:(Command *)command{
    
    _command = command;
}

- (void)pressButton{
    
    [_command excute];
}

@end
複製程式碼

遙控器類使用set方法注入了具體命令類,並向外提供了pressButton這個方法來內部呼叫已傳入的具體命令類的excute方法。

最後我們看一下客戶端是如何操作這些類的:

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

//init Light and Command instance
//inject light instance into command instance
Light *light = [[Light alloc] init];
CommandLightOn *co = [[CommandLightOn alloc] initWithLight:light];
    
//set command on instance into remote control instance
RemoteControl *rm = [[RemoteControl alloc] init];
[rm setCommand:co];
    
//excute command(light  on command)
[rm pressButton];
    

//inject light instance into command off instance
CommandLightOff *cf = [[CommandLightOff alloc] initWithLight:light];

//change to off command
[rm setCommand:cf];

//excute command(light  close command)
[rm pressButton];
複製程式碼

看一下日至輸出:

[11851:1190777] Light on
[11851:1190777] Light off
複製程式碼

從上面的程式碼可以看到,我們首先準備好具體命令類的例項,然後將其傳遞給遙控器類,最後觸發遙控器的pressButton方法來間接觸發light物件的相應操作。

下面看一下上面程式碼對應的類圖。

程式碼對應的類圖

命令模式程式碼示例類圖

優點

  • 將命令的發起者和命令的執行者分離,降低系統的耦合度
  • 便於批量處理命令,比如日至佇列的實現;便於命令的撤銷或重試,比如網路請求等

缺點

  • 需要針對每一個命令建立一個命令物件。如果系統中的命令過多,會造成系統中存在大量的命令類,提高系統的複雜度。

iOS SDK 和 JDK中的應用

  • 在JDK中,java.lang.Runnable是使用命令模式的經典場景,Runnable介面可以作為抽象的命令,而實現了Runnable的執行緒即是具體的命令。

六. 觀察者模式

定義

觀察者模式(Observer Pattern):定義物件間的一種一對多的依賴關係,使得每當一個物件狀態發生改變時,其相關依賴物件都可以到通知並做相應針對性的處理。

適用場景

凡是涉及到一對一或者一對多的物件互動場景都可以使用觀察者模式。通常我們使用觀察者模式實現一個物件的改變會令其他一個或多個物件發生改變的需求,比如換膚功能,監聽列表滾動的偏移量等等。

現在我們清楚了觀察者模式的適用場景,下面看一下觀察者模式的成員和類圖。

成員與類圖

成員

觀察者模式有四個成員:

  • 目標(Subject):目標是被觀察的角色,宣告新增和刪除觀察者以及通知觀察者的介面。
  • 具體目標(Concrete Subject):具體目標實現目標類宣告的介面,儲存所有觀察者的例項(通過集合的形式)。在被觀察的狀態發生變化時,給所有登記過的觀察者傳送通知。
  • 觀察者(Observer):觀察者定義具體觀察者的更新介面,以便在收到通知時實現一些操作。
  • 具體觀察者(Concrete Observer):具體觀察者實現抽象觀察者定義的更新介面。

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

模式類圖

觀察者模式類圖

程式碼示例

場景概述

模擬這樣的一個場景:客戶(投資者)訂閱理財顧問的建議購買不同價格的股票。當價格資訊變化時,所有客戶會收到通知(可以使簡訊,郵件等等),隨後客戶檢視最新資料並進行操作。

場景分析

一個理財顧問可能服務於多個客戶,而且訊息需要及時傳達到各個客戶那邊;而客戶接收到這些訊息後,需要對這些訊息做出相應的措施。這種一對多的通知場景我們可以使用觀察者模式:理財顧問是被觀察的目標(Subject),而他的客戶則是觀察者(Observer)。

下面我們看一下如何用程式碼來模擬該場景。

程式碼實現

首先我們定義觀察者Observer:

//================== Observer.h ==================

@interface Observer : NSObject
{
    @protected Subject *_subject;
}

- (instancetype)initWithSubject:(Subject *)subject;

- (void)update;

@end



//================== Observer.m ==================

@implementation Observer

- (instancetype)initWithSubject:(Subject *)subject{
    
    self = [super init];
    if (self) {
        _subject = subject;
       [_subject addObserver:self];
    }
    return self;
}

- (void)update{
    
    NSLog(@"implementation by subclasses");
}
複製程式碼

Observer類是具體觀察者的父類,它宣告瞭一個傳入目標類(Subject)的構造方法並在構造方法裡持有這個傳入的例項。而且在這個構造方法裡,呼叫了Subject的‘新增觀察者’的方法,即addObserver:,目的是將當前的觀察者例項放入Subject的用來儲存觀察者例項的集合中(具體操作可以在下面講解Subject類的部分看到)

另外它也定義了update方法供子類使用。

下面我們看一下具體觀察者類Investor:

//================== Investor.h ==================

@interface Investor : Observer

@end



//================== Investor.m ==================

@implementation Investor

- (void)update{

    float buyingPrice = [_subject getBuyingPrice];
    NSLog(@"investor %p buy stock of price:%.2lf",self,buyingPrice);    
}

@end
複製程式碼

具體觀察者實現了該協議中定義的方法update方法,在這個方法裡面,首先通過getBuyingPrice方法獲得到最新的在監聽的資料buyingPrice,然後再做其他操作。這裡為了方便展示,直接使用日至列印出當前的具體觀察者例項的記憶體地址和當前監聽的最新值。

下面我們宣告一下目標類和具體目標類:

目標類Subject

//================== Subject.h ==================

@interface Subject : NSObject
{
    @protected float _buyingPrice;
    @protected NSMutableArray <Observer *>*_observers;
}

- (void)addObserver:(Observer *) observer;


- (void)removeObserver:(Observer *) observer;


- (void)notifyObservers;


- (void)setBuyingPrice:(float)price;


- (double)getBuyingPrice;


@end




//================== Subject.m ==================

@implementation Subject

- (instancetype)init{
    
    self = [super init];
    if (self) {
        _observers = [NSMutableArray array];
    }
    return self;
}


- (void)addObserver:( Observer * ) observer{
    
    [_observers addObject:observer];
}


- (void)removeObserver:( Observer *) observer{
    
    [_observers removeObject:observer];
}


- (void)notifyObservers{
    
    [_observers enumerateObjectsUsingBlock:^(Observer *  _Nonnull observer, NSUInteger idx, BOOL * _Nonnull stop) {
        
        [observer update];
    }];
}


- (void)setBuyingPrice:(float)price{
    
    _buyingPrice = price;
    
    [self notifyObservers];
}


- (double)getBuyingPrice{
    
    return _buyingPrice;
}


@end
複製程式碼

目標類持有一個可變陣列,用來儲存觀察自己的觀察者們;並且還提供了增加,刪除觀察者的介面,也提供了通知所有觀察者的介面。

而且它持有一個資料buyingPrice,這個資料就是讓外部觀察者觀察的資料。尤其注意它向外界提供的setBuyingPrice:方法:當外部呼叫這個方法,也就是要更新buyingPrice這個資料時,目標類呼叫了notifyObservers方法來告知當前所有觀察自己的觀察者們:我更新了。

getBuyingPrice就是用來返回當前的buyingPrice的值的,一般是在觀察者們收到更新通知後,主動調動這個方法獲取的(具體看上面Investor類的實現)。

OK,現在抽象目標類定義好了,下面我們看一下具體目標類FinancialAdviser

//================== FinancialAdviser.h ==================

@interface FinancialAdviser : Subject

@end



//================== FinancialAdviser.m ==================

@implementation FinancialAdviser

@end
複製程式碼

因為所有的介面的事先已經在Subject類定義好了,所以我們只需新建一個我們需要的子類即可(如果有不同於父類的操作的話還是可以按照自己的方式定義)。

下面我們看一下觀察者的機制是如何實現的:

FinancialAdviser *fa = [[FinancialAdviser alloc] init];
    
Investor *iv1 = [[Investor alloc] initWithSubject:fa];
    
NSLog(@"====== first advice ========");
[fa setBuyingPrice:1.3];
    
    
Investor *iv2 = [[Investor alloc] initWithSubject:fa];
Investor *iv3 = [[Investor alloc] initWithSubject:fa];

NSLog(@"====== second advice ========");
[fa setBuyingPrice:2.6];
複製程式碼

從程式碼中可以看到,我們最開始向FinancialAdviser(具體目標類)新增了一個具體觀察者類的例項iv1,然後FinancialAdviser的例項fa便通知了所有觀察者(此時的觀察者只有iv1)。

後面我們繼續向fa新增了iv2iv3後傳送通知。此時三個觀察者都收到了訊息。

在下面的日至輸出中也可以看到,記憶體地址0x600003094c00就是iv10x6000030836800x600003083690就是iv2iv3

====== first advice ========
investor 0x600003094c00 buy stock of price:1.30
====== second advice ========
investor 0x600003094c00 buy stock of price:2.60
investor 0x600003083680 buy stock of price:2.60
investor 0x600003083690 buy stock of price:2.60
複製程式碼

下面看一下上面程式碼對應的類圖。

程式碼對應的類圖

觀察者模式程式碼示例類圖

優點

  • 觀察者模式在觀察目標和觀察者之間建立了一個抽象的耦合。
  • 可實現廣播的,一對多的通訊

缺點

  • 如果一個觀察目標物件有很多直接和間接的觀察者的話,會需要比較多的通訊時間。
  • 需要注意觀察者和觀察目標之間是否有迴圈引用。

iOS SDK 和 JDK中的應用

  • 在 iOS SDK 中的 KVO 與 NSNotification 是觀察者模式的應用。
  • 在JDK的java.util包中,提供了Observable類以及Observer介面,它們構成了Java語言對觀察者模式的支援。

七. 中介者模式

定義

中介者模式(Mediator Pattern):用一箇中介物件來封裝一系列的物件互動,中介者使各物件之間不需要顯式地相互引用,從而使其耦合鬆散,而且可以獨立地改變它們之間的互動。

適用場景

系統結構可能會日益變得複雜,物件之間存在大量的相互關聯和呼叫,系統的整體結構容易變為網狀結構。在這種情況下,如果需要修改某一個物件,則可能會要跟蹤和該物件關聯的其他所有物件,並進行處理。耦合越多,修改的地方就會越多。

如果我們使用中介者物件,則可以將系統的網狀結構變成以中介者為中心的星型結構。中介者承擔了中轉作用和協調作用,簡化了物件之間的互動,而且還可以給物件間的互動進行進一步的控制。

現在我們清楚了中介者模式的適用場景,下面看一下中介者模式的成員和類圖。

成員與類圖

成員

中介者模式一共有四個成員:

  1. 抽象中介者(Mediator):抽象中介者定義具體中介者需要實現的介面。
  2. 具體中介者(Concrete Mediator):具體中介者實現抽象中介者定義的介面,承擔多個具體同事類之間的中介者的角色。
  3. 抽象同事類(Colleague):抽象同事類定義具體同事類需要實現的介面。
  4. 具體同事類(Concrete Colleague):具體同事類實現抽象同事類定義的介面。

模式類圖

狀態模式類圖

程式碼示例

場景概述

模擬一個多人對話的場景:當一個人發出訊息後,另外的那些人可以收到該訊息。

場景分析

假設一共有A,B,C三個人,那麼當A發出訊息後,需要分別傳遞給B,C二人。如果三個人直接相互通訊,可能虛擬碼會是這樣的:

A sent message to B
A sent message to C
複製程式碼

而且隨著人數的增多,程式碼行數也會變多,這顯然是不合理的。

因此在這種場景下,我們需要使用中介者模式,在所有人中間來做一個訊息的多路轉發:當A發出訊息後,由中介者來傳送給B和C:

A sent message to Mediator ;
Mediator sent message to B & C
複製程式碼

下面我們看一下如何用程式碼來模擬該場景。

程式碼實現

首先我們建立通話的使用者類User:

//================== User.h ==================

@interface User : NSObject

- (instancetype)initWithName:(NSString *)name mediator:(ChatMediator *)mediator;

- (void)sendMessage:(NSString *)message;

- (void)receivedMessage:(NSString *)message;

@end



//================== User.m ==================

@implementation User
{
    NSString *_name;
    ChatMediator *_chatMediator;
}

- (instancetype)initWithName:(NSString *)name mediator:(ChatMediator *)mediator{
    
    self = [super init];
    if (self) {
        _name = name;
        _chatMediator = mediator;
    }
    return self;
}

- (void)sendMessage:(NSString *)message{
    
    NSLog(@"================");
    NSLog(@"%@ sent message:%@",_name,message);
    [_chatMediator sendMessage:message fromUser:self];
    
}

- (void)receivedMessage:(NSString *)message{
    
    NSLog(@"%@ has received message:%@",_name,message);
}

@end
複製程式碼

使用者類在初始化的時候需要傳入中介者的例項,並持有。目的是為了在後面傳送訊息的時候把訊息轉發給中介者。

另外,使用者類還對外提供了傳送訊息和接收訊息的介面。而在傳送訊息的方法內部其實呼叫的是中介者的傳送訊息的方法(因為中介者持有了所有使用者的例項,因此可以做多路轉發),具體是如何做的我們可以看下中介者類ChatMediator的實現:

//================== ChatMediator.h ==================

@interface ChatMediator : NSObject

- (void)addUser:(User *)user;

- (void)sendMessage:(NSString *)message fromUser:(User *)user;

@end



//================== ChatMediator.m ==================

@implementation ChatMediator
{
    NSMutableArray <User *>*_userList;
}

- (instancetype)init{
    
    self = [super init];
    
    if (self) {
        _userList = [NSMutableArray array];
    }
    return self;
}

- (void)addUser:(User *)user{

    [_userList addObject:user];
}

- (void)sendMessage:(NSString *)message fromUser:(User *)user{
    
    [_userList enumerateObjectsUsingBlock:^(User * _Nonnull iterUser, NSUInteger idx, BOOL * _Nonnull stop) {
        
        if (iterUser != user) {
            [iterUser receivedMessage:message];
        }
    }];
}

@end
複製程式碼

中介者類提供了addUser:的方法,因此我們可以不斷將使用者新增到這個中介者裡面(可以看做是註冊行為或是“加入群聊”)。在每次加入一個User例項後,都將這個例項新增到中介者持有的這個可變陣列裡。於是在將來中介者就可以通過遍歷陣列的方式來做訊息的多路轉發,具體實現可以看sendMessage:fromUser:這個方法。

到現在為止,使用者類和中介者類都建立好了,我們看一下訊息是如何轉發的:

ChatMediator *cm = [[ChatMediator alloc] init];
    
User *user1 = [[User alloc] initWithName:@"Jack" mediator:cm];
User *user2 = [[User alloc] initWithName:@"Bruce" mediator:cm];
User *user3 = [[User alloc] initWithName:@"Lucy" mediator:cm];
    
[cm addUser:user1];
[cm addUser:user2];
[cm addUser:user3];
    
[user1 sendMessage:@"happy"];
[user2 sendMessage:@"new"];
[user3 sendMessage:@"year"];
複製程式碼

從程式碼中可以看到,我們這裡建立了三個使用者,分別加入到了聊天中介者物件裡。再後面我們分別讓每個使用者傳送了一條訊息。我們下面通過日至輸出來看一下每個使用者的訊息接收情況:

[13806:1284059] ================
[13806:1284059] Jack sent message:happy
[13806:1284059] Bruce has received message:happy
[13806:1284059] Lucy has received message:happy
[13806:1284059] ================
[13806:1284059] Bruce sent message:new
[13806:1284059] Jack has received message:new
[13806:1284059] Lucy has received message:new
[13806:1284059] ================
[13806:1284059] Lucy sent message:year
[13806:1284059] Jack has received message:year
[13806:1284059] Bruce has received message:year
複製程式碼

下面看一下上面程式碼對應的類圖。

程式碼對應的類圖

中介者模式程式碼示例類圖

優點

  • 中介者使各物件不需要顯式地相互引用,從而使其耦合鬆散。

缺點

  • 在具體中介者類中包含了同事類之間的互動細節,可能會導致具體中介者類非常複雜,使得其難以維護。

iOS SDK 和 JDK中的應用

  • JDK中的Timer就是中介者類的實現,而配合使用的TimerTask則是同事類的實現。

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

本篇部落格的程式碼和類圖都儲存在我的GitHub庫中:knightsj:object-oriented-design中的 Chapter 2.3

到本篇為止,物件導向設計系列暫時告一段落,短期內不會有新的文章出來。讀者朋友們可以隨時給我提意見或溝通。

本篇已同步到個人部落格:傳送門

該系列前面的三篇文章:

參考書籍和教程

相關文章