今天我們來做一個糖果機吧,用戶只需要投入25美分孵户,就可以購買糖果了环揽,具體的構(gòu)造如下圖所示:
每個圓圈都表示一種狀態(tài),而每個箭頭都表示一種動作吨拗,這些狀態(tài)隨著不同動作的進行就可以不斷切換满哪。從圖中可以看到我們有四種狀態(tài)和四種動作婿斥,那么廢話不多說,下面我們就來看看具體的代碼實現(xiàn)哨鸭。
#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(@"你不能投幣了民宿,糖果已經(jīng)賣完");
}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(@"不能退幣,你已經(jīng)轉(zhuǎn)動曲柄只估,購買了糖果");
}
}
//退出硬幣
-(void)turnCrank{
if(self.state == has_quarter){
NSLog(@"不要重復轉(zhuǎn)動曲柄");
}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
上面的代碼可以解決我們目前的問題仅乓,但是該來的還是來了:需求改了赖舟。我們要增加一種狀態(tài):當轉(zhuǎn)到曲柄的時候蓬戚,有10%的幾率會掉下來兩顆糖果夸楣。此時的糖果機如下圖所示:
現(xiàn)在多了一種狀態(tài)--贏家,那么就必須在上面的四個方法里面都加上這個狀態(tài)判斷子漩,如果哪天要修改某種狀態(tài)豫喧,那么又必須在四個方法里面一個個的改,簡直不要太麻煩了幢泼。
那怎么解決呢紧显?
仔細分析上面的代碼和圖,我們發(fā)現(xiàn)每次修改了狀態(tài)缕棵,我們都必須修改原有代碼孵班,是因為原有的代碼和狀態(tài)混在一起了,那把這些狀態(tài)獨立出來成為一個個的類不就行了嗎招驴?這樣不管以后增加還是修改狀態(tài)只需要修改單獨的狀態(tài)類就行了篙程,原來的邏輯代碼不需要做任何更改。
上面的代碼是使用動作來進行分類别厘,一個動作方法里面分為四種狀態(tài)虱饿,但是狀態(tài)是需要經(jīng)常修改的,所以導致每次狀態(tài)的修改都需要修改每個動作方法触趴,所以為了避免這樣的情況發(fā)生氮发,我們需要被狀態(tài)單獨出去成為一個類。如果狀態(tài)是固定的冗懦,而動作是經(jīng)常變化的爽冕,那么就可以考慮把動作單獨出去成為一個類。其實終極目標就是:把變化和不變化的分離開來披蕉,把變化的部分單獨封裝起來颈畸,使之單獨變化前塔,不會影響不變化的部分。
下面我們就來看看具體的代碼實現(xiàn)
代碼實現(xiàn)
1承冰、定義狀態(tài)類的接口
我們需要定義一個接口华弓,接口里面包括上面的四種動作,每個狀態(tài)類都需要實現(xiàn)這四個方法
#import <Foundation/Foundation.h>
@protocol stateInterface <NSObject>
@required
-(void)insertQuarter;
-(void)ejectQuarter;
-(void)trunCrank;
-(void)dispense;
@end
2困乒、定義四種狀態(tài)
現(xiàn)在我們要實現(xiàn)糖果機的四種狀態(tài)寂屏,我們把這些狀態(tài)都提取出來,單獨成類娜搂,每個狀態(tài)都會實現(xiàn)上面接口的四個動作
沒有25分錢的狀態(tài)
#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分錢的狀態(tài)
#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(@"你已經(jīng)塞入了一枚硬幣携御,不要重復投幣");
}
-(void)ejectQuarter{
NSLog(@"硬幣即將推出");
self.machine.state = self.machine.noQuarterState;
}
-(void)trunCrank{
NSLog(@"你選擇購買糖果昌粤,處理中....");
self.machine.state = self.machine.soldingState;
}
-(void)dispense{
NSLog(@"請先選擇購買糖果");
}
@end
售賣中的狀態(tài)
#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(@"對不起啄刹,你已經(jīng)購買了糖果涮坐,不能退款");
}
-(void)trunCrank{
NSLog(@"重復點擊按鈕,不會得到更多糖果哦");
}
-(void)dispense{
if (self.machine.count > 0) {
self.machine.count --;
self.machine.state = self.machine.noQuarterState;
NSLog(@"糖果已經(jīng)售出");
NSLog(@"糖果還剩下:%zd",self.machine.count);
}else{
NSLog(@"抱歉誓军,沒有糖果了袱讹,如果需要退款,請點擊退幣按鈕");
self.machine.state = self.machine.soldOutState;
}
}
@end
糖果售罄的狀態(tài)
#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、實現(xiàn)糖果機
糖果機類主要干兩件事:
- 公開方法壹甥,給客戶端操作救巷,公開的四種方法分別對應四種動作
- 公開并初始化四種狀態(tài),供狀態(tài)類切換狀態(tài)
#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];
//初始化狀態(tài)為沒有硬幣狀態(tài)
if (self.count > 0) self.state = self.noQuarterState;
}
return self;
}
//可以發(fā)現(xiàn)此時的四種動作方法都委托給狀態(tài)類去實現(xiàn)了
-(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 狀態(tài)模式[62936:1497982] 你塞入了一枚硬幣
2016-12-13 10:42:24.218 狀態(tài)模式[62936:1497982] 你選擇購買糖果,處理中....
2016-12-13 10:42:24.218 狀態(tài)模式[62936:1497982] 糖果已經(jīng)售出
2016-12-13 10:42:24.219 狀態(tài)模式[62936:1497982] 糖果還剩下:1
2016-12-13 10:42:24.219 狀態(tài)模式[62936:1497982] 你沒有塞入一枚硬幣缸榄,不能退錢
Program ended with exit code: 0
我們使用示意圖來展示下上面的流程
上面的流程圖的四個步驟正好對應客戶端測試代碼的四句代碼渤弛,下面來一一分析下
- 初始化糖果機,此時糖果機的狀態(tài)時noquarter(沒有25分錢的狀態(tài))
- 執(zhí)行糖果機
gumabllMachine
的動作方法machineInsertQuarter
甚带,投入硬幣她肯,此方法把動作委托到當前狀態(tài)類(noQuarterState)去執(zhí)行佳头,跳到noQuarterState
類,執(zhí)行insertQuarter
方法晴氨,輸出顯示康嘉,并通過引用gumabllMachine
類的實例改變當前狀態(tài)為hasQuarterState
- 執(zhí)行糖果機
gumabllMachine
的動作方法machinetrunCrank
,此方法把動作委托到當前狀態(tài)類(hasQuarterState)去執(zhí)行籽前,跳到hasQuarterState
類亭珍,執(zhí)行trunCrank
方法,輸出顯示枝哄,并通過引用gumabllMachine
類的實例改變當前狀態(tài)為soldingState
- 執(zhí)行糖果機
gumabllMachine
的動作方法machineDispense
肄梨,此方法把動作委托到當前狀態(tài)類(soldingState)去執(zhí)行,跳到soldingState
類挠锥,執(zhí)行dispense
方法众羡,輸出顯示,并通過引用gumabllMachine
類的實例改變當前狀態(tài)為noQuarterState
回到第一步的初始狀態(tài)
通過上面的分析我們可以看出兩點:
- 糖果機的動作方法全部委托給具體的狀態(tài)類去實現(xiàn)
- 狀態(tài)類自身可以切換狀態(tài)
這就是我們今天要講的狀態(tài)模式的兩個作用蓖租,下面來具體看看
定義
允許一個對象在其內(nèi)部狀態(tài)改變時改變它的行為粱侣。對象看起來似乎修改了它的類。
我們先來解讀下第一句話菜秦,首先狀態(tài)模式把狀態(tài)單獨分裝成一個個的類甜害,然后把動作委托到當前的狀態(tài)類去執(zhí)行,那么當狀態(tài)改變的時候球昨,即使同一個動作的執(zhí)行結(jié)果也會不同。對比到上面的例子眨攘,對于不同的糖果機狀態(tài)主慰,我們投入25分錢,可能會被接受或者拒絕鲫售。這就是說狀態(tài)改變的時候共螺,行為也會跟著改變,也就是第一句話的意思情竹。
那么第二句話呢藐不?由于狀態(tài)在運行的時候是在不斷變化的,而相同的動作也會隨著狀態(tài)的變化而變化秦效,那么同一個對象gumabllMachine
在不同的時刻雏蛮,相同的動作會因為狀態(tài)的不同而出現(xiàn)不同的執(zhí)行結(jié)果,看起來就像是gumabllMachine
被改變了(因為一般情況下阱州,如果外在條件不改變挑秉,一個類的相同方法每次執(zhí)行結(jié)果應該相同)。而實際上只是通過切換到不同的狀態(tài)對象來造成gumabllMachine
類被改變的假象苔货。
對象的狀態(tài)一般指的是對象實例的屬性的值犀概,對應到上面的例子就是gumabllMachine
實例對象的state屬性立哑,行為指的是對象的功能,對應上面的例子就是gumabllMachine
的實例的四個公開方法姻灶。狀態(tài)模式的作用是分離狀態(tài)所對應的行為铛绰,每個狀態(tài)都對應相同的行為,但是每個狀態(tài)的相同行為卻有不同的表現(xiàn)形式产喉。對應上面的例子就是每個糖果機狀態(tài)都有四種行為至耻,但是每種行為的表現(xiàn)卻是不同的。這樣通過切換到不同的狀態(tài)镊叁,就可以實現(xiàn)該狀態(tài)下對應的具體行為尘颓,所以說狀態(tài)決定行為。
通過上面的分析可以得知:狀態(tài)模式實現(xiàn)的前提是晦譬,行為不變疤苹,而狀態(tài)不斷變化。
適用性
在如下情況可以考慮采用狀態(tài)模式:
一個對象的行為取決于它的狀態(tài),并且它必須在運行時刻根據(jù)狀態(tài)改變它的行為敛腌。
一個操作中含有龐大的多分支的條件語句,且這些分支依賴于該對象的狀態(tài)卧土。
這個狀態(tài)通常用一個或多個枚舉常量表示。通常 , 有多個操作包含這一相同的條件結(jié)構(gòu)像樊。 state模式將每一個條件分支放入一個獨立的類中尤莺。這使得你可以根據(jù)對象自身的情況將對 象的狀態(tài)作為一個對象,這一對象可以不依賴于其他對象而獨立變化。
UML結(jié)構(gòu)圖及說明
狀態(tài)模式將 與 特 定 狀 態(tài) 相 關(guān) 的 行 為 局 部 化 , 并 且 將 不 同 狀 態(tài) 的 行 為 分 割 開 來 state 模式將所有與一個特定的狀態(tài)相關(guān)的行為都放入一個對象中生棍。因為所有與狀態(tài)相關(guān)的代碼都存在于某 一個 S t a t e 子類中 , 所 以 通 過 定 義 新 的 子 類 可 以 很 容 易 的 增 加 新 的 狀 態(tài) 和 轉(zhuǎn) 換 颤霎。
另一個方法是使用數(shù)據(jù)值定義內(nèi)部狀態(tài)并且讓 C o n t e x t 操 作 來 顯 式 地 檢 查 這 些 數(shù) 據(jù) 。 但 這 樣將會使整個 C o n t e x t 的實現(xiàn)中遍布看起來很相似的條件語句或 c a s e 語 句 涂滴。 增 加 一 個 新 的 狀 態(tài) 可能需要改變?nèi)舾蓚€操作 , 這就使得維護變得復雜了友酱。
S t a t e 模式避免了這個問題 , 但可能會引入另一個問題 , 因 為 該 模 式 將 不 同 狀 態(tài) 的 行 為 分 布在多個 S t a t e 子 類 中 。 這 就 增 加 了 子 類 的 數(shù) 目 , 相 對 于 單 個 類 的 實 現(xiàn) 來 說 不 夠 緊 湊 柔纵。 但 是 如 果 有許多狀態(tài)時這樣的分布實際上更好一些 , 否則需要使用巨大的條件語句缔杉。
正如很長的過程一樣,巨大的條件語句是不受歡迎的。它們形成一大整塊并且使得代碼 不夠清晰,這又使得它們難以修改和擴展搁料。
S t a t e模 式 提 供 了 一 個 更 好 的 方 法 來 組 織 與 特 定 狀 態(tài) 相 關(guān) 的 代 碼 或详。 決 定 狀 態(tài) 轉(zhuǎn) 移 的 邏 輯 不 在 單 塊 的 i f 或 s w i t c h 語句中 , 而 是 分 布 在 S t a t e 子類之間。 將每一個狀態(tài)轉(zhuǎn)換和動作封裝到一個類中,就把著眼點從執(zhí)行狀態(tài)提高到整個對象的狀態(tài)郭计。 這將使代碼結(jié)構(gòu)化并使其意圖更加清晰霸琴。
優(yōu)缺點
-
簡化應用的邏輯控制
狀態(tài)模式使用單獨的類來封裝一個狀態(tài)的處理。這樣可以把一個很大的程序控制分割到很多小的單獨的狀態(tài)類中去實現(xiàn)拣宏,這樣把本來著眼于通過行為分類轉(zhuǎn)換到著眼于通過狀態(tài)分類沈贝,對于那種狀態(tài)經(jīng)常變化的程序來說,這樣的改變后勋乾,代碼邏輯更加清晰宋下,也可以消除巨大的if-else判斷語句
-
更好的分離狀態(tài)和行為
通過設(shè)置所有狀態(tài)類的公共接口嗡善,定義他們共有的行為,每個狀態(tài)類都實現(xiàn)這些行為但是表現(xiàn)不同学歧,這樣程序只需要設(shè)置合適的狀態(tài)類就可以執(zhí)行行為罩引,從而讓程序只需要關(guān)心狀態(tài)的切換,而不需要關(guān)心狀態(tài)對應的行為枝笨,處理起來更加簡單清晰袁铐。
-
更好的擴展性
以后如果新增一個狀態(tài)類,只需要實現(xiàn)公共接口定義的行為即可横浑,然后在需要的地方添加接口剔桨,做到了開閉原則。
-
更加明了的狀態(tài)切換
狀態(tài)切換只在一個地方進行(context或者狀態(tài)類中)徙融,狀態(tài)的切換都是通過一個變量來記錄洒缀,這樣就不會造成狀態(tài)切換混亂。
狀態(tài)的維護和轉(zhuǎn)換
有兩種方式來實現(xiàn)狀態(tài)的維護和轉(zhuǎn)換
- 在context中
- 在每個具體的狀態(tài)類中
上面的例子就是使用了第二種方法欺冀,如果放在context中怎么實現(xiàn)呢树绩?也很簡單,只需要把本來在每個具體的狀態(tài)類中的狀態(tài)轉(zhuǎn)換代碼提取到context中即可隐轩,修改gumabllMachine
如下所示饺饭,記得刪除每個狀態(tài)類的狀態(tài)轉(zhuǎn)換代碼
#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];
//初始化狀態(tài)為沒有硬幣狀態(tài)
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