设计模式系列9--状态模式

image

今天我们来做一个糖果机吧,用户只需要投入25美分,就可以购买糖果了,具体的构造如下图所示:

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%的几率会掉下来两颗糖果。此时的糖果机如下图所示:

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

我们使用示意图来展示下上面的流程

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结构图及说明

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容