文章目录
- 前言
- 1.原理阐述
- 2.引例
- 3.状态模式对象化
- 4.状态模式优化
- 总结
前言
状态模式,乍一听名字其实好像很好理解的样子,状态嘛,人在不同状态会有不同的行为模式。那软件的状态模式又是什么样子的?根据一个变量的值,执行不同的业务处理。这个变量的值有很多类型,不同的类型就是不同的状态。
上面是我对状态模式的初次印象,那么具体是个什么情况,接着往下看。
1.原理阐述
状态(State)模式是一种行为型模式,其实现可完成类似有限状态机的功能。换句话说,一个对象可以处于多种不同的状态(当然,同一时刻只能处于某一种状态),从而让对象产生不同的行为。通过状态模式可以表达出这些不同的状态并实现对象在这些状态之间的转换。状态模式最突出的特点是用类来表示状态,这一点与策略模式有异曲同工之妙(策略模式中用类来表示策略)。
状态模式与策略模式从UML图上看完全相同,只不过两者所运用的场合以及所表达的目的不同。
策略模式链接
下面是书上的原话,摘一下
状态模式(State):
当一个对象的内在状态改变时,允许改变其行为,这个对象看起来像是改变了其类
说句实在话,原来看《大话设计模式》这本书,觉得通俗易懂,但是随着逐渐深入的了解设计模式,看了其他类似的书以及自己在写笔记的时候,又觉得这本书差点意思,不是说这本书不好,可能是有更加专业的书籍?《C++新经典:设计模式》这本书就真不错,还是配套C++语言的,哈哈哈哈
在前些博客里面,对《大话设计模式》中的原话去摘录的时候,很难一次性立马看懂。感觉说了又好像没说明的意思。这里就不拿小菜和大鸟的例子来阐述了。
2.引例
我们讲设计模式,只告诉你理论确实是非常的抽象,尤其是之前备考软考的时候,有些模式没理解,光靠记忆,很容易忘,到最后还是需要看代码看例子去实践。
刚才提到,一个对象的状态变了,那么它的行为也会发生改变。这就是状态模式的精髓,行为因为状态而改变。
现在考虑这样一个例子,经典传奇游戏,一刀XXX的打怪兽例子
怪兽有四种状态,凶悍,不安,恐惧,死亡。
这种情况,我们用最简单的代码逻辑,无非就是if+else组合,例如下面这种
enum MonsterState //怪兽状态
{MonS_Fer, //凶悍MonS_Worr, //不安MonS_Fear, //恐惧MonS_Dead //死亡
};
class Monster
{
public:Monster(int life) :m_life(life), m_status(MonS_Fear) {}
public:void Attacked(int power)//怪物被攻击。参数power表示主角对怪物的攻击力{m_life -= power;if (m_status == MonS_Fer){if (m_life > 400){cout << "怪物受到" << power << "点伤害并对主角进行反击" << endl;//其他业务代码}else if (m_life > 100){cout << "怪物受到" << power << "点伤害并对主角进行反击,怪物进入不安状态" << endl;m_status = MonS_Worr;//其他业务代码}else if (m_life > 0){cout << "怪物受到" << power << "点伤害,怪物进入恐惧状态,开始逃跑" << endl;m_status = MonS_Fear;//其他业务代码}else{cout << "怪物受到" << power << "点伤害,已经死亡!" << endl;m_status = MonS_Dead;//其他业务代码}}else if ((m_status == MonS_Worr)){//........}else if ((m_status == MonS_Fear)){//........}else if ((m_status == MonS_Dead)){//........}}
private:int m_life; //血量MonsterState m_status; //初始状态
};
这就是状态模式了吗?好像有点东西了,行为因为状态而改变。
但是这种代码是会被人诟病的,在一个函数里一堆if else,真的很让人头大。如果新增一个状态,那么又是各种改,完全没有"开闭原则"的体现。
设计模式嘛,面向对象设计,那就把这些if else的语句块用面向对象的思想来做,每一种状态就是一个具体类型的对象,在if else语句块中的业务逻辑,是不是也可以放到具体的类中去实现呢?
通过状态模式,可以对上述的代码进行改造。在状态模式中,怪物的每个状态都写成一个状态类(类似的情形,例如在策略模式中是将每个策略写成一个策略类),当然,应该为这些状态类抽象出一个统一的父类以便实现多态,然后在每个状态类中实现相关的业务逻辑例如,对于怪物的“不安”状态可以实现为一个名字叫作Status_Worr 的类,在该类中实现相关的业务逻辑,例如怪物对主角的反击和呼唤支援。这样就相当于把上述Monster 类的Attacked 成员函数的业务逻辑代码拆分到各个状态类中去实现,不但大大简化了 Attacked成员函数的实现代码,也实现了委托机制,即Attacked成员函数把本该自己实现的功能委托给了各个状态类(中的成员函数)去实现。当然,必须持有该类的一个指针,才能把功能委托给该类。
3.状态模式对象化
现在看一看状态类父类及各个子类如何书写。专门创建一个MonsterStatus.h文件,代码如下:
#ifndef __MONSTERSTATUS__
#define __MONSTERSTATUS__#include<iostream>
class Monster; //类前向声明class MonsterStatus //怪物状态类的父类
{
public:virtual void Attacked(int power, Monster* mainobj) = 0;virtual ~MonsterStatus() {}};
// 凶悍状态类
class MonsterStatus_Feroc : public MonsterStatus
{
public://传递进来的参数是否有必要使用,开发者自行斟酌virtual void Attacked(int power, Monster * mainobj){std::cout <<"怪物处于凶悍状态中,对主角进行疯狂的反击!" << std::endl; //处理其他动作逻辑}
};
// 不安状态类
class MonsterStatus_Worr : public MonsterStatus
{
public:virtual void Attacked(int power, Monster* mainobj){std::cout << "怪物处于不安状态中,对主角进行反击并呼唤支援!" << std::endl;//处理其他动作逻辑}
};
// 恐惧状态类
class MonsterStatus_Fear : public MonsterStatus
{
public:virtual void Attacked(int power, Monster* mainobj){std::cout << "怪物处于恐惧状态中,处于逃跑之中" << std::endl;//处理其他动作逻辑}
};
// 不安状态类
class MonsterStatus_Dead : public MonsterStatus
{
public:virtual void Attacked(int power, Monster* mainobj){std::cout << "怪物死亡" << std::endl;//处理其他动作逻辑}
};
#endif
这样我们的怪兽Monster类的写法以及实现也需要改改,大体如下:
#ifndef __MONSTER__
#define __MONSTER__
class MonsterStatus;
//怪物类
class Monster
{
public:Monster(int life);~Monster();public:void Attacked(int power);private:int m_life;//血量MonsterStatus* m_pState;//持有状态对象
};#endif // !__MONSTER__
#include "Monster.h"
#include "MonsterStatus.h"Monster::Monster(int life):m_life(life),m_pState(nullptr)
{m_pState = new MonsterStatus_Feroc();
}Monster::~Monster()
{delete m_pState;
}void Monster::Attacked(int power)//怪物被攻击。参数power表示主角对怪物的攻击力
{int orglife = m_life;//暂存原来的怪物血量值用于后续比较m_life -= power;if (orglife > 400)//怪物原来处于凶悍状态{if (m_life > 400)//状态未发生改变{//cout << "怪物受到" << power << "点伤害并对主角进行反击" << endl;m_pState->Attacked(power, this);//将具体的业务委托给状态类处理}else if (m_life > 100){//cout << "怪物受到" << power << "点伤害并对主角进行反击,怪物进入不安状态" << endl;delete m_pState;m_pState = new MonsterStatus_Worr();m_pState->Attacked(power, this);//将具体的业务委托给状态类处理}else if (m_life > 0){//与上面类似}else{//与上面类似}}else if (orglife > 100)//怪物原来处于不安状态{//........}else if (orglife > 0)//怪物原来处于恐惧状态{//........}else //怪物原来处于死亡状态{//........}
}
我们把if else语句块中的各种业务逻辑交给具体的状态类去做,这些状态类通过一个抽象状态接口去调用,这样就做到
通过将业务逻辑代码委托给状态类,可以有效减少Monster类Attacked成员函数中的代码量。这就是状态类存在的价值—使业务逻辑代码更加清晰和易于维护。
但是这么多if else 其实让人也很头疼,后面代码给别人接管的时候,估计少不了一顿吐槽。那么怎么把这个if else这么多语句段都优化掉呢?
4.状态模式优化
我们上面Attack函数里面,其实就是根据怪物当前的血量来判断怪物处于什么状态,我们把这个状态转换的过程从外部抽象类Monster的Attack函数转换到具体的实现类的话,那这个抽象的接口Attack是不是就非常简单了呢?
先在怪物类中新增一些get,set接口
int Monster::getLife()
{return m_life;
}void Monster::setLife(int life)
{m_life = life;
}MonsterStatus * Monster::getCurrentStatus()
{return m_pState;
}void Monster::setCurrentState(MonsterStatus * pstate)
{m_pState = pstate;
}
我们以凶悍状态类的转换为例进行说明,代码如下
// 凶悍状态类
class MonsterStatus_Feroc : public MonsterStatus
{
public://传递进来的参数是否有必要使用,开发者自行斟酌virtual void Attacked(int power, Monster * mainobj){int orglife = mainobj->getLife();if ((orglife - power) > 400){//状态未发生改变mainobj->setLife(orglife - power);std::cout << "怪物处于凶悍状态中,对主角进行疯狂的反击!" << std::endl;}else{//如果状态发生改变,那么下个状态绝对不会是当前状态,这里先无条件转到//不安状态去,如果血量更少,那么状态的转换就交给不安状态类去做delete mainobj->getCurrentStatus();mainobj->setCurrentState(new MonsterStatus_Worr);mainobj->getCurrentStatus()->Attacked(power, mainobj);}//处理其他动作逻辑}
};
上面代码中,我们其实是把怪物目前血量的判定放到了具体的类中,如果状态发生转换,实际上是交给了另一个类去判断。这种处理方式简化了代码,把职责分的比较明确一些,有点递归的意思。
那么这样一写之后,Monster类的Attack函数实际上就简化为这么一句话,至于里面的各种状态转换,判定什么的,都交给了具体的类去做
void Monster::Attacked(int power)//怪物被攻击。参数power表示主角对怪物的攻击力
{m_pState->Attacked(power, this);
}
下面是UML类图,可以参考一下
总结
状态模式什么时候用?
一般在下面两种情况:
(1)对象的行为取决于其状态,该对象需要根据其状态来改变行为
(2)一个操作中含有庞大的条件分支语句,而这些分支语句的执行依赖于对象的当前状态。
状态模式虽然可以处理庞大的条件分支语句,但是它同样会带来很多个状态类,用来表示对象的多种状态。在代码编写层面,它并不是很方便。所以如果条件分支语句不是很庞大的话,我们大可不必用状态模式。
看上面的UML类图,其实和策略模式有点相像。下面是策略模式的UML,我只能说,一模一样。
这两者虽然UML一样,但是运用场合和表达的含义是不同的。
在策略模式中,客户端通常需要主动指定环境类对象所要使用的策略对象
是哪个,而在状态模式中,环境类对象在各个状态之间切换,客户端基本不需要了解各种状态对象,也不需要和状态对象交互。