【设计模式】状态模式

第 12 章 状态模式

12.1 状态模式概述

状态(State)模式是一种行为型模式,其实现可完成类似有限状态机的功能。换句话说,一个对象可以处于多种不同的状态(当然,同一时刻只能处于某一种状态),从而让对象产生不同的行为。通过状态模式可以表达出这些不同的状态并实现对象在这些状态之间的转换。状态模式最突出的特点是用类来表示状态,这一点与策略模式有异曲同工之妙(策略模式中用类来表示策略)。状态模式与策略模式从 UML 图上看完全相同,只不过两者所运用的场合以及所表达的目的不同。

状态模式有一定的难度,不太好理解,请读者认真分析和理解范例代码。

12.2 一个基本的状态转换范例
12.2.1 有限状态机概念

状态模式一般用于实现有限状态机。有限状态机(Finite State Machine, FSM)简称状态机。状态机有 3 部分组成:状态(State)、事件(Event)、动作(Action)。当某个事件(转移条件)发生时,会根据当前状态决定执行哪种动作,然后进入下一种状态,当然,有时不一定会执行某种动作,只是单纯地进入下一种状态。有限状态机的实现方式有多种,例如用 if…else…条件判断分支,也可以用状态模式等。

要理解状态转换,可以设想一下生活中的人类或者说某个人,开始工作时状态是精神饱满,随着时间的推移,逐渐从精神饱满变成饥饿状态,吃了饭之后,状态变为困倦,睡了一觉后又恢复到了精神饱满状态,于是又开始了工作。在这段描述中,可以将某个人看成系统中的某个对象,这个人存在多种状态,例如精神饱满、饥饿、困倦等。人可以在这些种状态之间转换,处于不同的状态下要做的事情不同,例如精神饱满时可以认真工作,饥饿和困倦时都无法正常工作等。状态转换图如下:

精神饱满  (初始状态)|| 随时间推移v
饥饿|| 吃了顿饭v
困倦|| 睡了一觉v
精神饱满

在这个图中,随时间推移、吃了顿饭、睡了一觉都属于发生的事件(转移条件,也可以看成是触发状态发生变化的行为或动作),而精神饱满、饥饿、困倦是不同的状态。当然,在这个图中,状态变化后,并没有执行什么具体动作。请注意区分触发状态发生变化的动作与状态发生变化后的动作并不是一回事。例如我揍你,你感觉疼(从不疼到疼,你的状态发生了变化),于是擦了点药。这里“揍你”这个动作是转移条件,而擦了点药是状态变化后执行的具体动作。

12.2.2 游戏怪物状态转换示例

这里还是以闯关打斗类游戏的开发为例,游戏中主角的主要任务是杀怪。为了简单起见,约定怪物的生命值是 500 点血。当主角对怪物进行攻击时,怪物会损失血量并有如下行为:
(1) 当怪物血量多于 400 点时,怪物处于凶悍状态,此时怪物会对主角进行反击。
(2) 当怪物血量小于或等于 400 点但多于 100 点时,怪物处于不安状态,此时怪物会对主角进行反击并开始呼唤附近的其他怪物支援。
(3) 当怪物血量小于或等于 100 点时,怪物处于恐惧状态,此时怪物开始逃命。
(4) 当怪物血量小于或等于 0 点时,怪物处于死亡状态,此时怪物不能再被攻击。

这里怪物一共涉及了 4 种状态,分别是凶悍、不安、恐惧、死亡,而促使怪物状态发生变化的唯一事件是主角对怪物的攻击。当怪物处于各个状态时,也会做出不同的动作,例如,怪物处于凶悍状态时会对主角进行反击,而当怪物处于不安状态时不但会对主角进行反击还会求援,当怪物处于恐惧状态时会开始逃跑,当然怪物处于死亡状态时就不会做出什么动作了。状态转换图如下:

凶悍
(初始状态)|| 被攻击v
不安|| 被攻击v
恐惧|| 被攻击v
死亡

从这个图中可以看到,怪物的初始状态是凶悍,随着被不断攻击,最后会变成死亡状态,状态之间的转换不一定是一个闭合的环。怪物如果逃跑成功没有被攻击死,则可能就一直处于恐惧状态。当然如果游戏策划约定怪物一段时间不被攻击血量会慢慢恢复,那么只要怪物不死亡,其状态也会慢慢从其他状态恢复到凶悍状态。另外,怪物的状态转换也不一定是按照顺序进行,例如,刚开始怪物是凶悍状态,如果主角攻击力特别强,攻击一下就打掉了怪物 450 点血,那么怪物就剩 50 点血了,这时怪物一下子就变成恐惧状态了。所以这个状态转换图只是一个示意图,实际的状态转换可能比较复杂。图中促使怪物状态发生变化的唯一事件是攻击怪物,如果给主角增加一种给怪物加血的能力(只要游戏策划允许这么干),那么怪物从恐惧状态恢复到凶悍状态也没什么不可以。

12.2.3 传统代码实现方式

首先简单的定义几个代表怪物状态的枚举值:

// 怪物状态枚举值
enum MonsterState {MonS_Fer,    // 凶悍MonS_Worr,   // 不安MonS_Fear,   // 恐惧MonS_Dead    // 死亡
};

然后创建一个怪物类 Monster 并提供一个成员函数 Attacked 用于表示怪物被攻击时的表现。Monster 类的实现代码如下:

// 怪物类
class Monster
{
public:// 构造函数,怪物的初始状态从"凶悍"开始Monster(int life) : m_life(life), m_status(MonS_Fer) {}// 怪物被攻击。参数 power 表示主角对怪物的攻击力(即怪物丢失的血量)void Attacked(int power){m_life -= power;  // 怪物剩余的血量if (m_status == MonS_Fer){if (m_life > 400){std::cout << " 怪物受到" << power << " 点伤害并对主角进行疯狂的反击!" << std::endl;// 处理其他动作逻辑,例如反击}else if (m_life > 100){std::cout << " 怪物受到" << power << " 点伤害并对主角进行反击,怪物变得焦躁不安并开始呼唤支援!" << std::endl;m_status = MonS_Worr;// 处理其他动作逻辑,例如反击和呼唤支援}else if (m_life > 0){std::cout << " 怪物受到" << power << " 点伤害,怪物变得恐惧并开始逃跑!" << std::endl;m_status = MonS_Fear;// 处理其他动作逻辑,例如逃跑}else{std::cout << " 怪物受到" << power << " 点伤害,已经死亡!" << std::endl;m_status = MonS_Dead;// 处理怪物死亡后事宜,例如怪物尸体定时消失等}}else if (m_status == MonS_Worr)  // 目前怪物已经处于不安状态,这说明怪物的血量 <= 400 并且 > 100{if (m_life > 100){std::cout << " 怪物受到" << power << " 点伤害并对主角进行反击,并继续急促的呼唤支援!" << std::endl;// 处理其他动作逻辑,例如反击和呼唤支援}else if (m_life > 0){std::cout << " 怪物受到" << power << " 点伤害,怪物变得恐惧并开始逃跑!" << std::endl;m_status = MonS_Fear;// 处理其他动作逻辑,例如逃跑}else{std::cout << " 怪物受到" << power << " 点伤害,已经死亡!" << std::endl;m_status = MonS_Dead;// 处理怪物死亡后事宜,例如怪物尸体定时消失等}}else if (m_status == MonS_Fear){if (m_life > 0){std::cout << " 怪物受到" << power << " 点伤害,怪物继续逃跑!" << std::endl;// 处理其他动作逻辑,例如逃跑}else{std::cout << " 怪物受到" << power << " 点伤害,已经死亡!" << std::endl;m_status = MonS_Dead;// 处理怪物死亡后事宜,例如怪物尸体定时消失等}}else  // 怪物已经处于死亡状态{std::cout << " 怪物已死亡,不能再被攻击!" << std::endl;}}private:int m_life;            // 血量(生命值)MonsterState m_status; // 初始状态
};

main 主函数中,增加如下代码:

#include <iostream>int main()
{Monster monster(500);std::cout << " 怪物出生,当前处于凶悍状态,500 点血!" << std::endl;monster.Attacked(20);monster.Attacked(100);monster.Attacked(200);monster.Attacked(170);monster.Attacked(100);monster.Attacked(100);return 0;
}

执行起来,看一看结果:

从上述代码可以看到,Attacked 的实现逻辑比较复杂,主要问题有以下几点:
(1) 其中用到了诸多的 if…else … 语句来进行各种条件判断。
(2) 诸如反击、呼唤支援、逃跑、死亡后续处理等事宜可能会涉及相当多的业务逻辑代码编写工作,这些业务逻辑代码如果一并放在 Attacked 成员函数中实现,可能会导致 Attacked 成员函数的实现代码达到数百甚至数千行,非常难以维护。
(3) 如果日后为怪物增加新的状态,那么又会导致增加 Attacked 中 if…else…语句的条件判断,进一步加剧了 Attacked 的维护难度。

12.2.4 状态模式改造代码

通过状态模式,可以对上述的代码进行改造。在状态模式中,怪物的每个状态都写成一个状态类(类似的情形,例如在策略模式中是将每个策略写成一个策略类),当然,应该为这些状态类抽象出一个统一的父类以便实现多态,然后在每个状态类中实现相关的业务逻辑。例如,对于怪物的“不安”状态可以实现为一个名字叫作 Status_Worr 的类,在该类中实现相关的业务逻辑,例如怪物对主角的反击和呼唤支援。这样就相当于把上述 Monster 类的 Attacked 成员函数的业务逻辑代码拆分到各个状态类中去实现,不但大大简化了 Attacked 成员函数的实现代码,也实现了委托机制,即 Attacked 成员函数把本该自己实现的功能委托给了各个状态类(中的成员函数)去实现。当然,必须持有该类的一个指针,才能把功能委托给该类。

现在看一看状态类父类及各个子类如何书写。专门创建一个 MonsterStatus.h 文件,代码如下:

#ifndef MONSTERSTATUS
#define MONSTERSTATUSclass 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

从上面的代码中可以看到,原本在 MonsterAttacked 成员函数中实现的业务逻辑全部挪到了各个状态子类中去实现,这就大大简化了 Attacked 成员函数中的业务逻辑代码,而且每个状态子类也只需要实现它自己的转移条件(动作)。

接着,需要重写原来的 Monster 类(可以将当前的 Monster 类实现代码注释掉)。为了看起来更接近一个真实项目,把该类专门放入到新建立的 Monster.h 文件中,并在 MyProject.cpp 的开头位置使用 #include "Monster.h" 代码行将该文件包含进来。Monster.h 文件的代码如下:

#ifndef MONSTER
#define MONSTERclass MonsterStatus;  // 类前向声明// 怪物类
class Monster {
public:Monster(int life);~Monster();void Attacked(int power);  // 怪物被攻击private:int m_life;            // 血量(生命值)MonsterStatus* m_pState;  // 持有状态对象
};#endif

增加新文件 Monster.cpp,并将该文件增加到当前的项目中。Monster.cpp 的实现代码如下:

#include <iostream>
#include "Monster.h"
#include "MonsterStatus.h"using namespace std;// 构造函数,怪物的初始状态从"凶悍"开始
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)
{int orglife = m_life;  // 暂存原来的怪物血量值用于后续比较m_life -= power;  // 怪物剩余的血量if (orglife > 400){if (m_life > 400){// 状态未变// cout << " 怪物受到" << power << " 点伤害并对主角进行疯狂的反击!" << endl;m_pState->Attacked(power, this);  // 其他的逻辑代码被本 Monster 类委托给了具体状态类来处理}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)  // 状态从凶悍状态改变到恐惧状态,主角的攻击太恐怖了{// cout << " 怪物受到" << power << " 点伤害,怪物变得恐惧并开始逃跑!" << endl;delete m_pState;  // 释放原有的状态对象m_pState = new MonsterStatus_Fear();  // 怪物转到恐惧状态m_pState->Attacked(power, this);}else  // 状态从凶悍改变到死亡{// cout << " 怪物受到" << power << " 点伤害,已经死亡!" << endl;delete m_pState;  // 释放原有的状态对象m_pState = new MonsterStatus_Dead();  // 怪物转到死亡状态m_pState->Attacked(power, this);}}else if (orglife > 100)  // 怪物原来处于不安状态{if (m_life > 100)  // 状态未变{// cout << " 怪物受到" << power << " 点伤害并对主角进行反击,并继续急促的呼唤支援!" << endl;m_pState->Attacked(power, this);}else if (m_life > 0)  // 状态从不安改变到恐惧{// cout << " 怪物受到" << power << " 点伤害,怪物变得恐惧并开始逃跑!" << endl;delete m_pState;  // 释放原有的状态对象m_pState = new MonsterStatus_Fear();  // 怪物转到恐惧状态m_pState->Attacked(power, this);}else  // 状态从不安改变到死亡{// cout << " 怪物受到" << power << " 点伤害,已经死亡!" << endl;delete m_pState;  // 释放原有的状态对象m_pState = new MonsterStatus_Dead();  // 怪物转到死亡状态m_pState->Attacked(power, this);}}else if (orglife > 0)  // 怪物原来处于恐惧状态{if (m_life > 0)  // 状态未变{// cout << " 怪物受到" << power << " 点伤害,怪物继续逃跑!" << endl;m_pState->Attacked(power, this);}else  // 状态从恐惧改变到死亡{// cout << " 怪物受到" << power << " 点伤害,已经死亡!" << endl;delete m_pState;  // 释放原有的状态对象m_pState = new MonsterStatus_Dead();  // 怪物转到死亡状态m_pState->Attacked(power, this);}}else  // 怪物已经死亡{// 已经死亡的怪物,状态不会继续发生改变// cout << " 怪物已死亡,不能再被攻击!" << endl;m_pState->Attacked(power, this);}
}

main 主函数中的代码不变,执行起来,看一看结果:

从结果可以看到,通过将业务逻辑代码委托给状态类,可以有效减少 MonsterAttacked 成员函数中的代码量。这就是状态类存在的价值——使业务逻辑代码更加清晰和易于维护。

12.2.5 代码改进思考

思考一下,上述的实现代码是否可以进一步改进呢?代码改进这件事见仁见智,每位程序员都可以有自己的想法。这里有几个值得思考的问题:
(1) Monster 类的 Attacked 成员函数中仍旧有诸多的 if…else … 语句来进行各种条件判断,看起来比较繁杂。
(2) 如果引入一个怪物的新状态例如“自爆”状态——怪物的血量如果小于 10 但大于 0 时会进入该状态。在该状态下怪物若再被攻击致死,则如果主角离怪物距离过近,可能会因为怪物自爆而受到反向伤害。此时不但要引入新的状态类,还要修改 Monster 类的 Attacked 成员函数,增加新的 if…else … 判断分支,使程序代码变得更加繁杂。
(3) 此时的怪物状态转换代码全部集中在 Monster 类的 Attacked 成员函数中。

考虑到每次攻击怪物的血量都会减少(怪物应该会逐步经历凶悍、不安、恐惧、死亡中的某一个),所以能否基于本范例对代码进行改进,简化 Monster 类的 Attacked 成员函数的 if…else…判断分支,把怪物状态转换代码放到各个具体的状态子类中去实现呢?请注意,怪物的状态转换代码是放在 Monster 类中(Monster 类此时扮演状态管理器角色),还是放在具体的状态类,例如 MonsterStatus_FerocMonsterStatus_WorrMonsterStatus_FearMonsterStatus_Dead 中去实现,没有固定的套路,完全可以由程序开发人员根据业务需要自由决定。这里尝试对代码进行改进,将怪物的状态转换放到具体状态类中(具体状态类此时扮演状态管理器的角色),当然这种实现方法也有其缺点——状态类之间不可避免地会出现依赖关系。

首先在 Monster.h 中,为 Monster 类新增 4 个成员函数(接口)。

public:int GetLife()  // 获取怪物血量{return m_life;}void SetLife(int life)  // 设置怪物血量{m_life = life;}MonsterStatus* getCurrentState()  // 获取怪物当前状态{return m_pState;}void setCurrentState(MonsterStatus* pstate)  // 设置怪物当前状态{m_pState = pstate;}

接着,为了方便,需要将 MonsterStatus.h 中的一些实现代码放入一个新的 .cpp 源文件中,增加新文件 MonsterStatus.cpp,并将该文件增加到当前的项目中。MonsterStatus.cpp 的实现代码如下:

#include <iostream>
#include "Monster.h"
#include "MonsterStatus.h"using namespace std;// 各个状态子类的 Attacked 成员函数实现代码
void MonsterStatus_Feroc::Attacked(int power, Monster* mainobj)
{int orglife = mainobj->GetLife();  // 暂存原来的怪物血量值用于后续比较if ((orglife - power) > 400)  // 怪物原来处于凶悍状态,现在依旧处于凶悍状态{// 状态未变mainobj->SetLife(orglife - power);  // 怪物剩余的血量cout << " 怪物处于凶悍状态中,对主角进行疯狂的反击!" << std::endl;// 处理其他动作逻辑,例如反击}else{// 不管下一个状态是什么,总之不会是凶悍状态,只可能是不安、恐惧、死亡状态之一,先无条件转到不安状态去(在不安状态中会进行再次判断)delete mainobj->getCurrentState();mainobj->setCurrentState(new MonsterStatus_Worr());mainobj->getCurrentState()->Attacked(power, mainobj);}
}void MonsterStatus_Worr::Attacked(int power, Monster* mainobj)
{int orglife = mainobj->GetLife();if ((orglife - power) > 100)  // 怪物原来处于不安状态,现在依旧处于不安状态{// 状态未变mainobj->SetLife(orglife - power);  // 怪物剩余的血量cout << " 怪物处于不安状态中,对主角进行反击并呼唤支援!" << std::endl;// 处理其他动作逻辑,例如反击和不停地呼唤支援}else{// 不管下一个状态是什么,总之不会是凶悍和不安状态,只可能是恐惧、死亡状态之一,先无条件转到恐惧状态去delete mainobj->getCurrentState();mainobj->setCurrentState(new MonsterStatus_Fear());mainobj->getCurrentState()->Attacked(power, mainobj);}
}void MonsterStatus_Fear::Attacked(int power, Monster* mainobj)
{int orglife = mainobj->GetLife();if ((orglife - power) > 0)  // 怪物原来处于恐惧状态,现在依旧处于恐惧状态{// 状态未变mainobj->SetLife(orglife - power);  // 怪物剩余的血量cout << " 怪物处于恐惧状态中,处于逃跑之中!" << std::endl;// 处理其他动作逻辑,例如逃跑}else{// 不管下一个状态是什么,总之不会是凶悍、不安和恐惧状态,只可能是死亡状态delete mainobj->getCurrentState();mainobj->setCurrentState(new MonsterStatus_Dead());mainobj->getCurrentState()->Attacked(power, mainobj);}
}void MonsterStatus_Dead::Attacked(int power, Monster* mainobj)
{int orglife = mainobj->GetLife();if (orglife > 0){// 还要把怪物生命值减掉mainobj->SetLife(orglife - power);  // 怪物剩余的血量// 处理怪物死亡后事宜,例如怪物尸体定时消失等}cout << " 怪物死亡!" << std::endl;
}

MonsterStatus.h 文件的内容也需要作出改变,全新的 MonsterStatus.h 文件内容如下:

#ifndef MONSTERSTATUS_
#define MONSTERSTATUS_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);
};// 不安状态类
class MonsterStatus_Worr : public MonsterStatus {
public:virtual void Attacked(int power, Monster* mainobj);
};// 恐惧状态类
class MonsterStatus_Fear : public MonsterStatus {
public:virtual void Attacked(int power, Monster* mainobj);
};// 死亡状态类
class MonsterStatus_Dead : public MonsterStatus {
public:virtual void Attacked(int power, Monster* mainobj);
};#endif

接着,修改 monster.cpp 文件中 Monster 类的 Attacked 成员函数,修改后的该成员函数代码已经大量减少,再也看不到以往各种 if…else…判断分支,代码如下:

// 怪物被攻击
void Monster::Attacked(int power)
{m_pState->Attacked(power, this);
}

main 主函数中的代码不变,执行起来,结果不变:

上面举的这个怪物的状态转换范例,状态相对比较简单,通过改善代码将 Monster 类的 Attacked 成员函数中的 if…else … 判断分支进行了大量的消除,但这种消除工作不是绝对的,即使运用了状态模式,有些必需的 if…else … 判断分支也是无法消除掉的,尤其是在某些实际项目中,涉及对象(例如这里的怪物)状态比较繁多,而引发状态变化的事件也比较多(本范例事件比较单一,只有主角攻击怪物这一个事件)的时候,事件不同,可能会导致状态产生不同的变化,此时,也许必须用到 if…else … 判断分支来判断即将进入的下一个状态是哪一个。

12.3 引入状态模式
12.3.1 状态模式的优势

从上面的代码中可以看到,通过引入状态模式,将怪物的反击、呼唤支援、逃跑、怪物死亡后尸体定时消失等业务逻辑独立到 MonsterStatus 的各个子类中,而不在 Monster 类中实现。

当为怪物增加新的状态时,需要增加一个新的状态子类。如果状态转换代码是放在 Monster 类中,那么当然增加新状态要修改 Monster 类中的代码,这种情况下增加新状态对已有的状态类没什么影响,如果状态转换代码放在具体的状态类中,那么增加新状态可能要修改某些具体的状态类中代码(不算完全符合开闭原则,只能算基本符合)。

在必要的情况下,Monster 类可以将自身传递给 MonsterStatus,以便 MonsterStatus 子类能够对 Monster 类对象做必要的修改,例如,设置怪物剩余血量等。

12.3.2 状态模式的定义

引入“状态”设计模式的定义:允许一个对象(怪物)在其内部状态改变(例如,从凶悍状态改变为不安状态)时改变它的行为(例如,从疯狂反击变成反击并呼唤支援),对象看起来似乎修改了它的类。上述定义的前半句不难理解,因为状态模式将各种状态封装为了独立的类,并将对主体的动作(这里指对怪物的攻击)委托给当前状态对象来做(调用每个状态子类的 Attacked 成员函数)。这个定义的后半句是什么意思呢?怪物对象因为状态的变化其行为也发生了变化(例如,从反击变为逃跑),从表面看起来就好像是这个怪物对象已经不属于当前这个类而是属于一个新类了(因为类对象行为都发生了变化),而实际上怪物对象只是通过引用不同的状态对象造成看起来像是怪物对象所属类发生改变的假象。

状态模式允许一个对象基于各种内部状态来拥有不同的行为,它将一个对象的状态从对象中分离出来,封装到一系列状态类中,达到状态的使用以及扩充都很灵活的效果。客户端(指 main 主函数中的调用代码)不用关心当前对象处于何种状态以及状态如何转换。对象在同一时刻只能处于某一种状态下。

12.3.3 状态模式的 UML 图

针对前面的代码范例绘制状态模式的 UML 图,如下:

-m_pState
Monster
-m_pState: MonsterStatus
+Attacked()
«abstract»
MonsterStatus
+Attacked()
MonsterStatus_Feroc
+Attacked()
MonsterStatus_Worr
+Attacked()
MonsterStatus_Fear
+Attacked()
MonsterStatus_Dead
+Attacked()

状态模式的 UML 图中包含 3 种角色。

(1) Context (环境类):也叫上下文类,该类的对象拥有多种状态以便对这些状态进行维护。这里指 Monster 类。
(2) State (抽象状态类):定义接口以封装与环境类的一个特性状态相关的行为,在该类中声明各种不同状态对应的方法,在子类中实现这些方法,当然,如果相同或者默认的实现方法,也可以实现在抽象状态类中。这里指 MonsterStatus 子类。
(3) ConcreteState (具体状态类):抽象状态类的子类,用以实现与环境类该状态相关的行为(环境类将行为委托给状态类实现),每个子类实现一个行为。这里指 MonsterStatus_FerocMonsterStatus_WorrMonsterStatus_FearMonsterStatus_Dead 类。当然,一般来说,每个具体状态类所实现的行为应该是不同的。

这里再次提醒,一个状态类中可能会包含多种业务逻辑接口,而上述范例中只演示了一种业务逻辑接口(即每个状态子类的用来攻击怪物的 Attacked 成员函数)。即便是把状态转换代码放到具体的状态类中,当状态比较复杂时,仍不可避免地要在状态类中使用条件判断(if…else…)确定下一个要转换到的状态(参考每个状态子类的 Attacked 成员函数)。

12.3.4 状态模式的使用场景

在如下两种情况下,可以考虑使用状态模式:
(1) 对象的行为取决于其状态,该对象需要根据其状态来改变行为。
(2) 一个操作中含有庞大的条件分支语句,而这些分支语句的执行依赖于对象的当前状态。

状态模式虽然将对象在某种状态下的行为放到了状态类中实现,避免了产生巨大的条件判断语句块(因为在各种条件下要执行的逻辑非常多),在某种程度上大量减少了条件分支语句,但是增加了多个状态类来表示对象的多种状态,对于系统的开发、维护以及执行效率还是会产生一定的影响。另外,状态模式的实现代码有一定的复杂度,如果编写不当容易产生错误,所以当条件判断不多,条件成立时要执行的业务逻辑也不太复杂时,不需要用状态模式来实现。如果一个项目中某对象的状态特别复杂,则可以考虑一种叫作查表法的方式进行对象状态的转换(对这方面内容有兴趣的读者可以通过搜索引擎深入研究),这也是为了避免引入过多状态类而造成代码难以维护。

12.3.5 状态模式与策略模式的区别

状态模式的 UML 图与策略模式是相同的,但这两种模式的运用场合以及所表达的含义并不相同。虽然两者都可以改变环境类对象的行为,但存在以下核心区别:

特征状态模式策略模式
行为触发环境类根据当前状态自动触发行为客户端主动选择具体策略对象
状态管理环境类维护当前状态,状态转换由状态类或环境类控制环境类不维护状态,策略选择完全由客户端决定
模式目的封装对象状态相关的行为,状态转换是内在逻辑封装可互换的算法,强调不同策略的替换
客户端感知客户端无需了解具体状态类,只需触发事件客户端必须了解具体策略类,并主动选择策略
状态持续性状态转换通常是有状态的(依赖历史状态)策略选择是无状态的,每次选择独立
典型应用有限状态机、协议处理、工作流管理算法替换、排序策略、日志记录策略

示例对比:

// 状态模式:自动根据血量切换行为
monster.Attacked(100); // 自动切换状态// 策略模式:客户端主动选择策略
monster.setStrategy(new AggressiveStrategy());
12.3.6 状态类的单件实现方式

在上面的范例中,每迁移到一个新的状态,就用 new 创建了一个新的状态对象。由于状态类通常无成员变量(仅行为),可以优化为单件模式:

// MonsterStatus.h 新增单件接口
class MonsterStatus_Feroc : public MonsterStatus {
public:static MonsterStatus_Feroc* getInstance() {static MonsterStatus_Feroc instance;return &instance;}// ...其他状态类类似实现
};// 修改状态转换逻辑
void MonsterStatus_Feroc::Attacked(int power, Monster* mainobj) {// 转换时使用单件实例mainobj->setCurrentState(MonsterStatus_Worr::getInstance());
}// Monster.cpp 构造函数优化
Monster::Monster(int life) : m_life(life) {m_pState = MonsterStatus_Feroc::getInstance(); // 使用单件
}

优点:

  1. 减少对象创建开销
  2. 避免重复销毁/创建状态对象
  3. 线程安全(需配合线程安全的单件实现)
12.4 状态模式的扩展与优化
12.4.1 状态模式 + 工厂模式
// 状态工厂类
class StateFactory {
public:static MonsterStatus* createState(StateType type) {switch(type) {case STATE_FEROCIOUS: return MonsterStatus_Feroc::getInstance();case STATE_ANXIOUS: return MonsterStatus_Worr::getInstance();// ...其他状态}}
};// 使用示例
m_pState = StateFactory::createState(currentStateType);
12.4.2 状态模式 + 观察者模式
// 状态变更通知
class StateObserver {
public:virtual void onStateChanged(Monster* monster, StateType oldState) = 0;
};// Monster 类添加通知机制
class Monster {
private:std::vector<StateObserver*> observers;public:void addObserver(StateObserver* observer) {observers.push_back(observer);}void setCurrentState(MonsterStatus* state) {StateType oldType = m_pState->getType();m_pState = state;for(auto& obs : observers) {obs->onStateChanged(this, oldType);}}
};
12.4.3 状态模式性能优化
  1. 缓存当前状态:避免频繁获取状态对象
  2. 预创建状态实例:初始化时创建所有可能的状态对象
  3. 查表法优化状态转换:用二维数组替代条件判断
// 状态转移表
using TransitionTable = std::map<CurrentState, std::map<Event, NextState>>;TransitionTable transitionTable = {{STATE_FEROCIOUS, {{ATTACKED, STATE_ANXIOUS}}},{STATE_ANXIOUS, {{ATTACKED, STATE_FEARFUL}}},// ...其他转移规则
};
12.5 工业级应用案例
案例1:网络协议状态机
// TCP状态机
class TCPConnection {State* currentState;public:void receiveSYN() {currentState->handleSYN(this);}
};class ListenState : public State {
public:void handleSYN(TCPConnection* conn) override {conn->setState(new SYNReceivedState());conn->sendSYNACK();}
};
案例2:电梯控制系统
// 电梯状态管理
class Elevator {State* currentState;public:void requestFloor(int floor) {currentState->handleRequest(this, floor);}
};class MovingState : public State {
public:void handleRequest(Elevator* elevator, int floor) override {elevator->addDestination(floor);if(elevator->isDoorOpen()) {elevator->closeDoor();}}
};
12.6 总结与最佳实践
  1. 状态原子性:每个状态应代表单一职责
  2. 状态正交性:避免状态间循环依赖
  3. 状态验证:在关键操作前后进行状态检查
  4. 性能优化:使用单件模式共享无状态实例
  5. 可观测性:添加状态变更通知机制
  6. 文档化:为每个状态类编写清晰的状态转移说明

通过状态模式,系统的可维护性和扩展性显著提升,复杂的状态逻辑被封装到独立类中,符合开闭原则。在实际项目中,建议结合领域驱动设计(DDD)进行状态模式的应用,以提升系统的业务适应性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/42965.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

陈宛汮签约2025火凤凰风赏大典全球形象大使

原标题&#xff1a;陈宛汮签约2025火凤凰风赏大典全球形象大使 共工新闻社香港3月29日电 陈宛汮&#xff0c;华语原创女歌手。“星宝在闪耀”公益活动联合发起人&#xff0c;自闭症儿童康复推广大使。代表作:《荣耀火凤凰》《爱在醉千年》。 从2025年1月1日至2025年12月31日&a…

【深度学习入门_机器学习理论】极致梯度提升原理(XGBoost)

XGBoost&#xff08;eXtreme Gradient Boosting&#xff09;是一种高效、灵活且广泛应用的机器学习算法&#xff0c;属于梯度提升决策树&#xff08;Gradient Boosting Decision Tree, GBDT&#xff09; 的优化实现。它在分类、回归、排序等结构化/表格数据的预测任务中表现尤为…

Oracle初识:登录方法、导入dmp文件

目录 一、登录方法 以sys系统管理员的身份登录 &#xff0c;无需账户和密码 以账户密码的用户身份登录 二、导入dmp文件 方法一&#xff1a;PLSQL导入dmp文件 一、登录方法 Oracle的登录方法有两种。 以sys系统管理员的身份登录 &#xff0c;无需账户和密码 sqlplus / a…

STM32F103_LL库+寄存器学习笔记01 - 梳理CubeMX生成的LL库最小的裸机系统框架

《STM32 - 在机器人领域&#xff0c;LL库相比HAL优势明显》在机器人、自动化设备领域使用MCU开发项目&#xff0c;必须用LL库。 本系列笔记记录使用LL库的开发过程&#xff0c;首先通过CubeMX生成LL库代码&#xff0c;梳理LL库源码。通过学习LL库源码&#xff0c;弄清楚寄存器的…

Vue3当中el-tree树形控件使用

tree悬停tooltip效果 文本过长超出展示省略号 如果文本超出悬停显示tooltip效果 反之不显示 这里直接控制固定宽度限制 试了监听宽度没效果<template><el-treeshow-checkbox:check-strictly"true":data"data"node-key"id":props"…

最大数字(java)(DFS实现)

1.最大数字 - 蓝桥云课 因为N最大是10 的17次方&#xff0c; 所以可以利用字符串来处理输入的数字的每一位 并且是从高到低依次处理的 然后通过函数charAt(i)来获取第i位的字符 再减去‘0’就可以将字符转化为整型了 假设每一位数字都是x 然后通过两种操作 加或者减来操…

04 单目标定实战示例

看文本文,您将获得以下技能: 1:使用opencv进行相机单目标定实战 2:标定结果参数含义和数值分析 3:Python绘制各标定板姿态,查看图像采集多样性 4:如果相机画幅旋转90,标定输入参数该如何设置? 5:图像尺寸缩放,标定结果输出有何影响? 6:单目标定结果应用类别…

手机销售终端MPR+LTC项目项目总体方案P183(183页PPT)(文末有下载方式)

资料解读&#xff1a;手机销售终端 MPRLTC 项目项目总体方案 详细资料请看本解读文章的最后内容。在当今竞争激烈的市场环境下&#xff0c;企业的销售模式和流程对于其发展起着至关重要的作用。华为终端正处于销售模式转型的关键时期&#xff0c;波士顿 - 华为销售终端 MPRLTC …

如何在 vue 渲染百万行数据,vxe-table 渲染百万行数据性能对比,超大量百万级表格渲染

vxe-table 渲染百万行数据性能对比&#xff0c;超大量百万级表格渲染&#xff1b;如何在 vue 渲染百万行数据&#xff1b;当在开发项目时&#xff0c;遇到需要流畅支持百万级数据的表格时&#xff0c; vxe-table 就可以非常合适了&#xff0c;不仅支持强大的功能&#xff0c;虚…

ubuntu24 部署vnc server 使用VNC Viewer连接

前提条件 已创建一台Ubuntu 24.04(20.22通用)操作系统的云服务器&#xff0c;并且为云服务器绑定弹性公网IP&#xff0c;确保可以连接互联网。 已在本地PC安装VNC Viewer客户端。 操作步骤 服务器内安装vnc server以及桌面环境 apt update sudo apt install xfce4 xfce4-…

【数据结构】栈 与【LeetCode】20.有效的括号详解

目录 一、栈1、栈的概念及结构2、栈的实现3、初始化栈和销毁栈4、打印栈的数据5、入栈操作---栈顶6、出栈---栈顶6.1栈是否为空6.2出栈---栈顶 7、取栈顶元素8、获取栈中有效的元素个数 二、栈的相关练习1、练习2、AC代码 个人主页&#xff0c;点这里~ 数据结构专栏&#xff0c…

06-SpringBoot3入门-常见注解(简介)

1、Controller ResponseBody Controller是Spring MVC 中的注解&#xff0c;负责处理 HTTP 请求。 ResponseBody是Spring MVC 中的注解&#xff0c;用于直接将方法的返回值作为 HTTP 响应体。 2、RestController RestController Controller ResponseBody 3、RequestMappin…

工作记录 2017-03-10

工作记录 2017-03-10 序号 工作 相关人员 1 修改邮件上的问题。 更新RD服务器。 郝 更新的问题 1、修改了payment detail和patient insurance的health plan的输入方式。 2、new payment detail时&#xff0c;增加了allowable的处理。 3、选择payer的窗体&#xff0c;增…

MySQL数据库和表的操作

#使用数据库 use 数据库名; # 查询当前数据库是哪个数据库 select database(); 查看数据库版本 SELECT VERSION(); 查看当前用户 SELECT USER(); 查看所有用户() SELECT User,Host,Password FROM mysql.user;数据库 MySQL自带数据库&#xff1a; Information_schema: 主要存储…

lxd-dashboard 图形管理LXD/LXC

前言 LXD-WEBGUI是一个完全用AngularJS编写的Web应用程序,无需应用服务器、数据库或其他后端服务支持。只需要简单地托管静态HTML和JavaScript文件,就能立即投入使用。这个项目目前处于测试阶段,提供了直观的用户界面,帮助用户便捷地管理和控制LXD实例。 安装lxd-dashboa…

[GESP202503 C++一级题解]:B4257:图书馆里的老鼠

[GESP202503 C++一级题解]:B4257:图书馆里的老鼠 题目描述 图书馆里有 n n n 本书,不幸的是,还混入了一只老鼠,老鼠每 x x x 小时能啃光一本书,假设老鼠在啃光一本书之前,不会啃另一本。请问 y y y 小时后图书馆里还剩下多少本完整的书。 输入格式 三行,第一行一…

从零构建大语言模型全栈开发指南:第二部分:模型架构设计与实现-2.2.2文本生成逻辑:Top-k采样与温度控制

👉 点击关注不迷路 👉 点击关注不迷路 👉 点击关注不迷路 文章大纲 2.2.2 文本生成逻辑:Top-k采样与温度控制1. 文本生成的核心挑战与数学框架1.1 自回归生成的基本流程2. `Top-k`采样原理与工程实现2.1 数学定义与算法流程2.2 PyTorch实现优化3. 温度控制的数学本质与参…

Unity中实现UI的质感和圆角

质感思路有两种&#xff1a; 一种是玻璃质感的做法&#xff0c;抓取UI后面的图像做模糊&#xff08;build是GrabPass&#xff0c;urp抓图像我有写过在往期文章&#xff09;&#xff0c;这个方式网络上有很多就不写了&#xff1b; 另外一种是使用CubeMap的方式去模拟质感&…

[异步监听事件、异步绑定属性]通过vue的this.$refs.组件.$props和.$on实现异步绑定组件属性和事件监听

child.vue <template><div><el-button type"primary" click.stop"$emit(get, data)">点击传参</el-button></div> </template> <script> export default { name: "child", props: ["data"…

第六届 蓝桥杯 嵌入式 省赛

参考 第六届蓝桥杯嵌入式省赛程序设计题解析&#xff08;基于HAL库&#xff09;_蓝桥杯嵌入式第六届真题-CSDN博客 一、分析功能 RTC 定时 1&#xff09;时间初始化 2&#xff09;定时上报电压时间 ADC测量 采集电位器的输出电压信号。 串行功能 1&#xff09;传送要设置…