设计模式学习笔记--状态模式

来源:互联网 发布:上海瀚威酩轩 知乎 编辑:程序博客网 时间:2024/05/17 09:35

今天来学习一下设计模式中的状态模式。之前经常听说状态机之类的东东,自己也有用过,但是状态机和状态模式还是有一些区别,今天主要看一下状态模式的定义,例子,应用,最后再分析一下状态模式和我们所说的有限状态机之间到底是什么关系。


一.状态模式的定义


所谓状态,我们经常提到。比如水有几种状态,液态,固态,气态,而且达到一定条件之后,水可以在这三种状态之间相互转化。所谓状态模式,是指当一个对象的内在的状态改变时允许改变其行为,这个对象看起来像是改变了其类。状态模式主要是当一个对象状态转换条件判断过于复杂时,把状态的判断逻辑通过判断不同类对象,通过多态自动完成判断的一种设计模式。
下图是状态模式的UML图:


二.状态模式的例子

      

     1.没有使用状态模式的情况


来看一个例子,我们设计一个小游戏,人物有若干个状态:idle(休闲),walk(行走),attack(攻击),die(死亡)几种状态。每种状态人物都需要播放相应的动画,在游戏的循环中还需要根据不同的状态来进行不同的操作。
// Design Pattern.cpp : Defines the entry point for the console application.//#include "stdafx.h"#include <iostream>using namespace std;enum CharState{Invalid = -1,Idle,Walk,Attack,Did,};class Character{private:CharState m_CurrentState;public:Character(): m_CurrentState(Idle){}void SetState(CharState state){m_CurrentState = state;PlayAnimation();}void Tick(){switch (m_CurrentState){case Invalid:break;case Idle:cout << "Idle Tick,无操作" << endl;break;case Walk:cout << "Walk Tick, 更改位置" << endl;break;case Attack:cout << "Attack Tick,释放技能" << endl;break;case Did:cout << "Die Tick, 销毁对象" << endl;break;default:break;}}void PlayAnimation(){switch (m_CurrentState){case Invalid:break;case Idle:cout << "播放休闲动画" << endl;break;case Walk:cout << "播放行走动画" << endl;break;case Attack:cout << "播放攻击动画" << endl;break;case Did:cout << "播放死亡动画" << endl;break;default:break;}}};int _tmain(int argc, _TCHAR* argv[]){Character* man = new Character();man->SetState(Attack);man->Tick();man->SetState(Walk);man->Tick();system("pause");return 0;}
结果如下:
播放攻击动画
Attack Tick,释放技能
播放行走动画
Walk Tick, 更改位置
请按任意键继续. . .

恩,虽然很好地实现了我们想要的功能,在人物不同的状态下,对象相同的操作都会有不同的结果。但是,每个操作中都附带一个这么长的Switch语句(换成if,else会更长...),这就是《重构》中所谓坏代码的味道。试想一下,如果我们突然想增加几个新的状态,run(跑动),skill(技能),hurt(受伤),那么我们的switch语句就需要重新修改,而且变得更加冗长,而如果我们的操作变多了,那么每个操作对应不同的状态,我们要写的复杂判断语句就更多了。当看到这种冗长的条件判断时,我们第一时间应该想到的是通过设计模式来重构这种代码,使之更加优雅并且易扩展。

     2.使用了状态模式的情况


我们使用状态模式重构一下上面的代码,为每个状态创建一个状态类,设置状态的时候直接设置一个状态对象作为当前的状态,这时,我们仍然是只调用一个接口,但是具体调用什么方法就都是根据多态来判断了,省去了冗长的if-else或者switch-case,我们的代码就更加清晰明了,并且在增加新状态的时候,我们只需要增加新的状态类就行,不需要去搞那个长长的判断语句了。
        不多说,上代码:
// Design Pattern.cpp : Defines the entry point for the console application.//#include "stdafx.h"#include <iostream>using namespace std;//人物状态基类class CharState{public:virtual void PlayAnimation() = 0;virtual void Tick() = 0;};//休闲状态类class IdleState : public CharState{public:void PlayAnimation(){ cout << "播放休闲动画" << endl; }void Tick(){ cout << "Idle Tick,无操作" << endl; }};//行走状态类class WalkState : public CharState{public:void PlayAnimation(){ cout << "播放行走动画" << endl; }void Tick(){ cout << "Walk Tick,修改位置" << endl; }};//攻击状态类class AttackState : public CharState{public:void PlayAnimation(){ cout << "播放攻击动画" << endl; }void Tick(){ cout << "Attack Tick,释放技能" << endl; }};//死亡状态类class DieState : public CharState{public:void PlayAnimation(){ cout << "播放死亡动画" << endl; }void Tick(){ cout << "Die Tick,销毁对象" << endl; }};//人物类class Character{private:CharState* m_CurrentState;public:Character(): m_CurrentState(NULL){}void SetState(CharState* state){m_CurrentState = state;PlayAnimation();}void Tick(){m_CurrentState->Tick();}void PlayAnimation(){m_CurrentState->PlayAnimation();}};int _tmain(int argc, _TCHAR* argv[]){Character* man = new Character();CharState* Idle = new IdleState();CharState* Walk = new WalkState();CharState* Attack = new AttackState();CharState* Die = new DieState();man->SetState(Attack);man->Tick();man->SetState(Walk);man->Tick();system("pause");return 0;}
结果如下:
播放攻击动画
Attack Tick,释放技能
播放行走动画
Walk Tick,修改位置
请按任意键继续. . .

我们看到,通过状态模式,我们终于摆脱了那个冗长的switch-case分之语句,直接将对应状态的操作放在子类中,什么状态什么操作,一目了然,而调用的时候还是跟原来一样,只通过接口调用,还是一个PlayAnimation和Tick的操作,但是会根据对象内部状态的不同触发不同的操作。

     3.将状态放在Context类中


所谓Context类(见UML图),在上面的例子中就是Character类。上面的例子,我们在使用的时候额外再客户端里面new了一堆Character状态对象给Character类使用,虽然简单粗暴易理解,不过怎么想还是怎么别扭,人物的状态最好还是存储在人物中嘛。虽然在C#里面经常看到在设置状态的时候直接像下面这样,new一个新的状态塞进去:
man.SetState(new AttackState());

看到这里,我只想说有自带的垃圾回收就是任性!然而C++木有,我们如果也这么任性的话,迟早会出事的。而且,虽然这个状态对象没有字段属性,但是好歹也是个对象,每次都创建一个对象,难免还是有一些开销的。所以,我们考虑一个空间换时间的办法,将这个对象缓存起来,存储在Context类中,即例子中的Character类。这样,我们就可以在切换状态的时候,直接通过对象中缓存的状态对象来切换。
不过还有一个问题,由于我们的人物类可能有很多个对象,而我们并不需要很多个状态对象,一个类有一个就够了,怎么实现呢?对了,就是static字段。我们把这些状态对象存储为static类型的,这样,在一个类中只保存一个副本,既节省了空间,又减少了不必要的操作开销。
再次重构后的代码如下,我们将这几个状态对象设置为public的,这样我们就可以通过Character::状态名直接获得状态的指针了,有木有一点像enum呢?
// Design Pattern.cpp : Defines the entry point for the console application.//#include "stdafx.h"#include <iostream>using namespace std;//人物状态基类class CharState{public:virtual void PlayAnimation() = 0;virtual void Tick() = 0;};//休闲状态类class IdleState : public CharState{public:void PlayAnimation(){ cout << "播放休闲动画" << endl; }void Tick(){ cout << "Idle Tick,无操作" << endl; }};//行走状态类class WalkState : public CharState{public:void PlayAnimation(){ cout << "播放行走动画" << endl; }void Tick(){ cout << "Walk Tick,修改位置" << endl; }};//攻击状态类class AttackState : public CharState{public:void PlayAnimation(){ cout << "播放攻击动画" << endl; }void Tick(){ cout << "Attack Tick,释放技能" << endl; }};//死亡状态类class DieState : public CharState{public:void PlayAnimation(){ cout << "播放死亡动画" << endl; }void Tick(){ cout << "Die Tick,销毁对象" << endl; }};//人物类class Character{private:CharState* m_CurrentState;public:Character(): m_CurrentState(NULL){}void SetState(CharState* state){m_CurrentState = state;PlayAnimation();}void Tick(){m_CurrentState->Tick();}void PlayAnimation(){m_CurrentState->PlayAnimation();}public:static CharState* Idle;static CharState* Walk;static CharState* Attack;static CharState*  Die;};CharState* Character::Idle = new IdleState();CharState* Character::Walk = new WalkState();CharState* Character::Attack = new AttackState();CharState* Character::Die= new DieState();int _tmain(int argc, _TCHAR* argv[]){Character* man = new Character();Character* woman = new Character();man->SetState(Character::Attack);man->Tick();man->SetState(Character::Die);man->Tick();woman->SetState(Character::Walk);woman->Tick();woman->SetState(Character::Idle);woman->Tick();man->SetState(new AttackState());system("pause");return 0;}
结果如下:
播放攻击动画
Attack Tick,释放技能
播放死亡动画
Die Tick,销毁对象
播放行走动画
Walk Tick,修改位置
播放休闲动画
Idle Tick,无操作
播放攻击动画
请按任意键继续. . .

三.状态模式的总结


我们来总结一下状态模式的优缺点,使用时机以及状态模式和有限状态机的关系。

优点:
1)状态模式将对象不同状态下的行为分别放在不同状态对象中,方便管理,而客户端调用时不需要知道状态对象的存在,与不使用状态模式一样。
2)可以去掉判断对象不同状态进行相应操作的冗长的条件判断语句,新增状态时直接增加一个状态类,便于扩展。

缺点:
1)状态模式需要增加一些状态类,操作时也需要通过更换当前状态对象来切换状态,增加了系统的开销和复杂度。
2)状态的管理比较麻烦,如果设计不恰当,会导致系统结构混乱。而且新增状态不完全符合“开放-封闭原则”,新增状态时仍然需要修改状态管理部分的代码。

使用时机:
当我们的对象针对不同状态会有不同的行为时,我们就可以考虑使用状态模式。并且,当我们看到代码中有过于冗长的关于状态条件的判断时,我们就可以考虑使用状态模式来重构代码。

状态模式和有限状态机的关系:
下面的只是属于个人理解,如果不对还望指出。状态机实际上是一种抽象的概念,当对象根据不同状态有不同操作行为,并且可以在不同状态之间转换时,它就可以是一个状态机。而状态模式只是状态机的一种实现方式,我们最早写的那种switch-case语句也是属于状态机的一种实现,只是没有那么优雅罢了。

2 0
原创粉丝点击