設計模式系列9--狀態模式

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

設計模式系列9--狀態模式
image

今天我們來做一個糖果機吧,使用者只需要投入25美分,就可以購買糖果了,具體的構造如下圖所示:

設計模式系列9--狀態模式
image

每個圓圈都表示一種狀態,而每個箭頭都表示一種動作,這些狀態隨著不同動作的進行就可以不斷切換。從圖中可以看到我們有四種狀態和四種動作,那麼廢話不多說,下面我們就來看看具體的程式碼實現。

#import "gumabllMachines.h"

typedef enum : NSUInteger {
    sold_out,          //糖果賣完了
    no_quarter,        //沒有硬幣
    has_quarter,       //有硬幣
    sold               //售出糖果
}gumabllMachineState;

@interface gumabllMachines ()
@property(assign,nonatomic)gumabllMachineState state;
@property(assign,nonatomic)NSInteger gumabllCount;
@end


@implementation gumabllMachines

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.gumabllCount = 10;
    }
    return self;
}


//投入硬幣
-(void)insertQuarter{
    if(self.state == has_quarter){
        NSLog(@"不要重複投幣");

    }else if (self.state == no_quarter){
        self.state = has_quarter;
        NSLog(@"你投入了一枚硬幣");

    }else if (self.state == sold_out){
        NSLog(@"你不能投幣了,糖果已經賣完");

    }else if (self.state == sold){
        NSLog(@"請等待,我們正在售出糖果");
    }
}


//退出硬幣
-(void)ejectQuarter{
    if(self.state == has_quarter){
        NSLog(@"正在為你退出硬幣");
        self.state = no_quarter;

    }else if (self.state == no_quarter){
        NSLog(@"你沒有投入硬幣,不能退幣");

    }else if (self.state == sold_out){
        NSLog(@"不能退幣,你還沒有投入硬幣");

    }else if (self.state == sold){
        NSLog(@"不能退幣,你已經轉動曲柄,購買了糖果");
    }
}

//退出硬幣
-(void)turnCrank{
    if(self.state == has_quarter){
        NSLog(@"不要重複轉動曲柄");

    }else if (self.state == no_quarter){
        NSLog(@"請投入硬幣");

    }else if (self.state == sold_out){
        NSLog(@"沒有糖果啦");

    }else if (self.state == sold){
        NSLog(@"正在售出糖果,請稍後...");
        self.state = sold;
        [self turnCrank];
    }
}


//售出糖果
-(void)dispense{
    if(self.state == has_quarter){
        self.gumabllCount --;
        if (self.gumabllCount > 0) {
            NSLog(@"糖果正在售出");
            self.state = no_quarter;
        }else{
            self.state = sold_out;
            NSLog(@"不好意思,糖果賣完了");
        }

    }else if (self.state == no_quarter){
        NSLog(@"沒有糖果售出");

    }else if (self.state == sold_out){
        NSLog(@"沒有糖果售出");

    }else if (self.state == sold){
        NSLog(@"沒有糖果售出");
    }

}


@end複製程式碼

上面的程式碼可以解決我們目前的問題,但是該來的還是來了:需求改了。我們要增加一種狀態:當轉到曲柄的時候,有10%的機率會掉下來兩顆糖果。此時的糖果機如下圖所示:

設計模式系列9--狀態模式
image

現在多了一種狀態--贏家,那麼就必須在上面的四個方法裡面都加上這個狀態判斷,如果哪天要修改某種狀態,那麼又必須在四個方法裡面一個個的改,簡直不要太麻煩了。

那怎麼解決呢?

仔細分析上面的程式碼和圖,我們發現每次修改了狀態,我們都必須修改原有程式碼,是因為原有的程式碼和狀態混在一起了,那把這些狀態獨立出來成為一個個的類不就行了嗎?這樣不管以後增加還是修改狀態只需要修改單獨的狀態類就行了,原來的邏輯程式碼不需要做任何更改。

上面的程式碼是使用動作來進行分類,一個動作方法裡面分為四種狀態,但是狀態是需要經常修改的,所以導致每次狀態的修改都需要修改每個動作方法,所以為了避免這樣的情況發生,我們需要被狀態單獨出去成為一個類。如果狀態是固定的,而動作是經常變化的,那麼就可以考慮把動作單獨出去成為一個類。其實終極目標就是:把變化和不變化的分離開來,把變化的部分單獨封裝起來,使之單獨變化,不會影響不變化的部分。

下面我們就來看看具體的程式碼實現


程式碼實現

1、定義狀態類的介面

我們需要定義一個介面,介面裡麵包括上面的四種動作,每個狀態類都需要實現這四個方法

#import <Foundation/Foundation.h>

@protocol stateInterface <NSObject>
@required
-(void)insertQuarter;
-(void)ejectQuarter;
-(void)trunCrank;
-(void)dispense;

@end複製程式碼

2、定義四種狀態

現在我們要實現糖果機的四種狀態,我們把這些狀態都提取出來,單獨成類,每個狀態都會實現上面介面的四個動作

沒有25分錢的狀態

#import <Foundation/Foundation.h>
#import "stateInterface.h"
#import "gumabllMachine.h"

@interface noQuarterState : NSObject<stateInterface>
@property(strong,nonatomic)gumabllMachine *machine;
- (instancetype)initWithMachine:(gumabllMachine *)machine;
@end


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

#import "noQuarterState.h"

@implementation noQuarterState
- (instancetype)initWithMachine:(gumabllMachine *)machine
{
    self = [super init];
    if (self) {
        self.machine = machine;
    }
    return self;
}

-(void)insertQuarter{
    NSLog(@"你塞入了一枚硬幣");
    self.machine.state = self.machine.hasQuarterState;
}

-(void)ejectQuarter{
    NSLog(@"你沒有塞入一枚硬幣,不能退錢");
}


-(void)trunCrank{
    NSLog(@"你按了購買按鈕,但是你沒有塞入硬幣,請塞入硬幣");

}
-(void)dispense{
    NSLog(@"你要買一個糖果,但是你沒有塞入硬幣,請先付款");

}


@end複製程式碼

有25分錢的狀態

#import <Foundation/Foundation.h>
#import "stateInterface.h"
#import "gumabllMachine.h"

@interface hasQUarterState : NSObject<stateInterface>
@property(strong,nonatomic)gumabllMachine *machine;
- (instancetype)initWithMachine:(gumabllMachine *)machine;

@end

===========

#import "hasQUarterState.h"

@implementation hasQUarterState
- (instancetype)initWithMachine:(gumabllMachine *)machine
{
    self = [super init];
    if (self) {
        self.machine = machine;
    }
    return self;
}

-(void)insertQuarter{
    NSLog(@"你已經塞入了一枚硬幣,不要重複投幣");
}

-(void)ejectQuarter{
    NSLog(@"硬幣即將推出");
    self.machine.state = self.machine.noQuarterState;

}


-(void)trunCrank{
    NSLog(@"你選擇購買糖果,處理中....");
    self.machine.state = self.machine.soldingState;


}
-(void)dispense{
    NSLog(@"請先選擇購買糖果");
}

@end複製程式碼

售賣中的狀態

#import <Foundation/Foundation.h>
#import "stateInterface.h"
#import "gumabllMachine.h"

@interface soldingState : NSObject<stateInterface>
@property(strong,nonatomic)gumabllMachine *machine;
- (instancetype)initWithMachine:(gumabllMachine *)machine;

@end

=====================
#import "soldingState.h"

@implementation soldingState
- (instancetype)initWithMachine:(gumabllMachine *)machine
{
    self = [super init];
    if (self) {
        self.machine = machine;
    }
    return self;
}

-(void)insertQuarter{
    NSLog(@"請等待我們正在出貨,不要重複投幣...");
}

-(void)ejectQuarter{
    NSLog(@"對不起,你已經購買了糖果,不能退款");

}


-(void)trunCrank{
    NSLog(@"重複點選按鈕,不會得到更多糖果哦");

}
-(void)dispense{
    if (self.machine.count > 0) {
        self.machine.count --;
        self.machine.state = self.machine.noQuarterState;
        NSLog(@"糖果已經售出");
        NSLog(@"糖果還剩下:%zd",self.machine.count);
    }else{
        NSLog(@"抱歉,沒有糖果了,如果需要退款,請點選退幣按鈕");
        self.machine.state = self.machine.soldOutState;
    }

}

@end複製程式碼

糖果售罄的狀態

#import <Foundation/Foundation.h>
#import "stateInterface.h"
#import "gumabllMachine.h"

@interface soldOutState : NSObject<stateInterface>
@property(strong,nonatomic)gumabllMachine *machine;
- (instancetype)initWithMachine:(gumabllMachine *)machine;

@end

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

#import "soldOutState.h"

@implementation soldOutState
- (instancetype)initWithMachine:(gumabllMachine *)machine
{
    self = [super init];
    if (self) {
        self.machine = machine;
    }
    return self;
}

-(void)insertQuarter{
    NSLog(@"沒有糖果啦,不要投幣,請下次再來");
}

-(void)ejectQuarter{
    NSLog(@"即將為你退款...");

}


-(void)trunCrank{
    NSLog(@"沒有糖果哦");

}
-(void)dispense{
    NSLog(@"沒有糖果啦");
}

@end複製程式碼

3、實現糖果機

糖果機類主要幹兩件事:

  • 公開方法,給客戶端操作,公開的四種方法分別對應四種動作
  • 公開並初始化四種狀態,供狀態類切換狀態
#import <Foundation/Foundation.h>
#import "stateInterface.h"

@interface gumabllMachine : NSObject
-(void)setState:(id<stateInterface>)state;
@property(strong,nonatomic)id<stateInterface> state;
@property(strong,nonatomic)id<stateInterface> soldOutState;
@property(strong,nonatomic)id<stateInterface> noQuarterState;
@property(strong,nonatomic)id<stateInterface> hasQuarterState;
@property(strong,nonatomic)id<stateInterface> soldingState;
@property(assign,nonatomic)NSInteger count;

- (instancetype)initWithGumabllCount:(NSInteger)count;
-(void)machineInsertQuarter;
-(void)machineEjectQuarter;
-(void)machinetrunCrank;
-(void)machineDispense;


@end


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

#import "gumabllMachine.h"
#import "noQuarterState.h"
#import "hasQUarterState.h"
#import "soldingState.h"
#import "soldOutState.h"


@implementation gumabllMachine
- (instancetype)initWithGumabllCount:(NSInteger)count
{
    self = [super init];
    if (self) {
        self.count =count;
        self.noQuarterState = [[noQuarterState alloc]initWithMachine:self];
        self.hasQuarterState = [[hasQUarterState alloc]initWithMachine:self];
        self.soldingState = [[soldingState alloc]initWithMachine:self];
        self.soldOutState = [[soldOutState alloc]initWithMachine:self];
        //初始化狀態為沒有硬幣狀態
        if (self.count > 0) self.state = self.noQuarterState;
    }
    return self;
}

//可以發現此時的四種動作方法都委託給狀態類去實現了
-(void)machineInsertQuarter{
    [self.state insertQuarter];
}

-(void)machineEjectQuarter{
    [self.state ejectQuarter];
}


-(void)machinetrunCrank{
    [self.state trunCrank];
}

-(void)machineDispense{
    [self.state dispense];
}


@end複製程式碼

4、客戶端測試

        1、gumabllMachine *machine = [[gumabllMachine alloc]initWithGumabllCount:2];
        2、[machine machineInsertQuarter];
        3、[machine machinetrunCrank];
        4、[machine machineDispense];
        [machine machineEjectQuarter];複製程式碼

輸出如下

2016-12-13 10:42:24.218 狀態模式[62936:1497982] 你塞入了一枚硬幣
2016-12-13 10:42:24.218 狀態模式[62936:1497982] 你選擇購買糖果,處理中....
2016-12-13 10:42:24.218 狀態模式[62936:1497982] 糖果已經售出
2016-12-13 10:42:24.219 狀態模式[62936:1497982] 糖果還剩下:1
2016-12-13 10:42:24.219 狀態模式[62936:1497982] 你沒有塞入一枚硬幣,不能退錢
Program ended with exit code: 0複製程式碼

我們使用示意圖來展示下上面的流程

設計模式系列9--狀態模式
image

上面的流程圖的四個步驟正好對應客戶端測試程式碼的四句程式碼,下面來一一分析下

  1. 初始化糖果機,此時糖果機的狀態時noquarter(沒有25分錢的狀態)
  2. 執行糖果機gumabllMachine的動作方法machineInsertQuarter,投入硬幣,此方法把動作委託到當前狀態類(noQuarterState)去執行,跳到noQuarterState類,執行insertQuarter方法,輸出顯示,並通過引用gumabllMachine類的例項改變當前狀態為hasQuarterState
  3. 執行糖果機gumabllMachine的動作方法machinetrunCrank,此方法把動作委託到當前狀態類(hasQuarterState)去執行,跳到hasQuarterState類,執行trunCrank方法,輸出顯示,並通過引用gumabllMachine類的例項改變當前狀態為soldingState
  4. 執行糖果機gumabllMachine的動作方法machineDispense,此方法把動作委託到當前狀態類(soldingState)去執行,跳到soldingState類,執行dispense方法,輸出顯示,並通過引用gumabllMachine類的例項改變當前狀態為noQuarterState回到第一步的初始狀態

通過上面的分析我們可以看出兩點:

  1. 糖果機的動作方法全部委託給具體的狀態類去實現
  2. 狀態類自身可以切換狀態

這就是我們今天要講的狀態模式的兩個作用,下面來具體看看


定義

允許一個物件在其內部狀態改變時改變它的行為。物件看起來似乎修改了它的類。

我們先來解讀下第一句話,首先狀態模式把狀態單獨分裝成一個個的類,然後把動作委託到當前的狀態類去執行,那麼當狀態改變的時候,即使同一個動作的執行結果也會不同。對比到上面的例子,對於不同的糖果機狀態,我們投入25分錢,可能會被接受或者拒絕。這就是說狀態改變的時候,行為也會跟著改變,也就是第一句話的意思。

那麼第二句話呢?由於狀態在執行的時候是在不斷變化的,而相同的動作也會隨著狀態的變化而變化,那麼同一個物件gumabllMachine在不同的時刻,相同的動作會因為狀態的不同而出現不同的執行結果,看起來就像是gumabllMachine被改變了(因為一般情況下,如果外在條件不改變,一個類的相同方法每次執行結果應該相同)。而實際上只是通過切換到不同的狀態物件來造成gumabllMachine類被改變的假象。

物件的狀態一般指的是物件例項的屬性的值,對應到上面的例子就是gumabllMachine例項物件的state屬性,行為指的是物件的功能,對應上面的例子就是gumabllMachine的例項的四個公開方法。狀態模式的作用是分離狀態所對應的行為,每個狀態都對應相同的行為,但是每個狀態的相同行為卻有不同的表現形式。對應上面的例子就是每個糖果機狀態都有四種行為,但是每種行為的表現卻是不同的。這樣通過切換到不同的狀態,就可以實現該狀態下對應的具體行為,所以說狀態決定行為。

通過上面的分析可以得知:狀態模式實現的前提是,行為不變,而狀態不斷變化。


適用性

在如下情況可以考慮採用狀態模式:

  • 一個物件的行為取決於它的狀態,並且它必須在執行時刻根據狀態改變它的行為。

  • 一個操作中含有龐大的多分支的條件語句,且這些分支依賴於該物件的狀態。
    這個狀態通常用一個或多個列舉常量表示。通常 , 有多個操作包含這一相同的條件結構。 state模式將每一個條件分支放入一個獨立的類中。這使得你可以根據物件自身的情況將對 象的狀態作為一個物件,這一物件可以不依賴於其他物件而獨立變化。


UML結構圖及說明

設計模式系列9--狀態模式
image

狀態模式將 與 特 定 狀 態 相 關 的 行 為 局 部 化 , 並 且 將 不 同 狀 態 的 行 為 分 割 開 來 state 模式將所有與一個特定的狀態相關的行為都放入一個物件中。因為所有與狀態相關的程式碼都存在於某 一個 S t a t e 子類中 , 所 以 通 過 定 義 新 的 子 類 可 以 很 容 易 的 增 加 新 的 狀 態 和 轉 換 。

另一個方法是使用資料值定義內部狀態並且讓 C o n t e x t 操 作 來 顯 式 地 檢 查 這 些 數 據 。 但 這 樣將會使整個 C o n t e x t 的實現中遍佈看起來很相似的條件語句或 c a s e 語 句 。 增 加 一 個 新 的 狀 態 可能需要改變若干個操作 , 這就使得維護變得複雜了。

S t a t e 模式避免了這個問題 , 但可能會引入另一個問題 , 因 為 該 模 式 將 不 同 狀 態 的 行 為 分 布在多個 S t a t e 子 類 中 。 這 就 增 加 了 子 類 的 數 目 , 相 對 於 單 個 類 的 實 現 來 說 不 夠 緊 湊 。 但 是 如 果 有許多狀態時這樣的分佈實際上更好一些 , 否則需要使用巨大的條件語句。
正如很長的過程一樣,巨大的條件語句是不受歡迎的。它們形成一大整塊並且使得程式碼 不夠清晰,這又使得它們難以修改和擴充套件。

S t a t e模 式 提 供 了 一 個 更 好 的 方 法 來 組 織 與 特 定 狀 態 相 關 的 代 碼 。 決 定 狀 態 轉 移 的 邏 輯 不 在 單 塊 的 i f 或 s w i t c h 語句中 , 而 是 分 布 在 S t a t e 子類之間。 將每一個狀態轉換和動作封裝到一個類中,就把著眼點從執行狀態提高到整個物件的狀態。 這將使程式碼結構化並使其意圖更加清晰。


優缺點

  • 簡化應用的邏輯控制

    狀態模式使用單獨的類來封裝一個狀態的處理。這樣可以把一個很大的程式控制分割到很多小的單獨的狀態類中去實現,這樣把本來著眼於通過行為分類轉換到著眼於通過狀態分類,對於那種狀態經常變化的程式來說,這樣的改變後,程式碼邏輯更加清晰,也可以消除巨大的if-else判斷語句

  • 更好的分離狀態和行為

    通過設定所有狀態類的公共介面,定義他們共有的行為,每個狀態類都實現這些行為但是表現不同,這樣程式只需要設定合適的狀態類就可以執行行為,從而讓程式只需要關心狀態的切換,而不需要關心狀態對應的行為,處理起來更加簡單清晰。

  • 更好的擴充套件性

    以後如果新增一個狀態類,只需要實現公共介面定義的行為即可,然後在需要的地方新增介面,做到了開閉原則。

  • 更加明瞭的狀態切換

    狀態切換隻在一個地方進行(context或者狀態類中),狀態的切換都是通過一個變數來記錄,這樣就不會造成狀態切換混亂。


狀態的維護和轉換

有兩種方式來實現狀態的維護和轉換

  1. 在context中
  2. 在每個具體的狀態類中

上面的例子就是使用了第二種方法,如果放在context中怎麼實現呢?也很簡單,只需要把本來在每個具體的狀態類中的狀態轉換程式碼提取到context中即可,修改gumabllMachine如下所示,記得刪除每個狀態類的狀態轉換程式碼


#import "gumabllMachine.h"
#import "noQuarterState.h"
#import "hasQUarterState.h"
#import "soldingState.h"
#import "soldOutState.h"


@implementation gumabllMachine
- (instancetype)initWithGumabllCount:(NSInteger)count
{
    self = [super init];
    if (self) {
        self.count =count;
        self.noQuarterState = [[noQuarterState alloc]initWithMachine:self];
        self.hasQuarterState = [[hasQUarterState alloc]initWithMachine:self];
        self.soldingState = [[soldingState alloc]initWithMachine:self];
        self.soldOutState = [[soldOutState alloc]initWithMachine:self];
        //初始化狀態為沒有硬幣狀態
        if (self.count > 0) self.state = self.noQuarterState;
    }
    return self;
}


-(void)machineInsertQuarter{
    [self.state insertQuarter];
    self.state = self.hasQuarterState;

}

-(void)machineEjectQuarter{
    [self.state ejectQuarter];
    self.state = self.noQuarterState;

}


-(void)machinetrunCrank{
    [self.state trunCrank];
    self.state = self.soldingState;

}

-(void)machineDispense{
    [self.state dispense];
    if (self.count > 0) {
        self.state = self.noQuarterState;
    }else{
        self.state = self.soldOutState;
    }

}


@end複製程式碼

Demo下載

狀態模式Demo

相關文章