文章目录
一、引言
状态模式是一种行为设计模式, 让你能在一个对象的内部状态变化时改变其行为, 使其看上去就像改变了自身所属的类一样。其实现可完成类似有限状态机的功能。换句话说,一个对象可以处于多种不同的状态(当然,同一时刻只能处于某一种状态),从而让对象产生不同的行为。通过状态模式可以表达出这些不同的状态并实现对象在这些状态之间的转换。状态模式最突出的特点是用类来表示状态,这一点与策略模式有异曲同工之妙(策略模式中用类来表示策略)。
二、状态模式
状态模式一般用于实现有限状态机。有限状态机(FiniteStateMachine,FSM)简称状态机。状态机有3部分组成:状态(State)、事件(Event)、动作(Action)。当某个事件(转移条件)发生时,会根据当前状态决定执行哪种动作,然后进人下一种状态,当然,有时不一定会执行某种动作,只是单纯地进人下一种状态。有限状态机的实现方式有多种,例如用if、else条件判断分支,也可以用状态模式等。
假设生活中的我们,开始学习工作时的状态是精神饱满的,随着时间的推移,逐渐从精神饱满变成饥饿状态,吃了饭之后,状态变为困倦,睡了一觉后又恢复到了精神饱满状态,于是又开始了工作。在这段描述中,可以将某个人看成系统中的某个对象。这个人存在多种状态,例如精神饱满、饥饿、困倦等。人可以在这些种状态之间转换,处于不同的状态下要做的事情不同,例如精神饱满时可以认真工作,饥饿和困倦时都无法正常工作等。
随着时间推移,吃了饭、睡了觉都属于发生的事件(转移条件,也可以看成是触发状态发生状态变化的行为或动作),而精神饱满、饥饿、困倦的不同属于是不同的状态。
本文以闯关游戏的开发为例,游戏中怪物的生命值是500血,怪物的状态会随着血量的变化而变化。
- 怪物血量多于400点时,怪物处于凶悍状态,此时怪物会对主角进行反击。
- 怪物血量小于或等于400点但多于100点时,怪物处于不安状态,此时怪物会对主角进行反击并开始呼唤附近的其他怪物支援。
- 怪物血量小于或等于100点时,怪物处于恐惧状态,此时怪物开始逃命。
- 怪物血量小于或等于0点时,怪物处于死亡状态,此时怪物不能再被攻击。
这里设计了四种状态,凶悍、不安、恐惧、死亡,而促使怪物状态发送变化的唯一的事件是主角对怪物的攻击。当怪物处于各种状态时,也会做出不同的动作。例如,怪物处于凶悍状态时会对主角进行反击。
我们用枚举值来表示怪物状态:
c++
enum class MonsterState
{
Mons_Fer, //凶悍
Mons_Worr, //不安
Mons_Fear, //恐惧
Mons_Dead //死亡
};
然后创建了一个怪物类Monster
并提供一个成员函数Attacked
用于表示怪物被攻击时的表现。
C++
class Monster
{
public:
Monster(int life)
:m_life(life), m_status(MonsterState::Mons_Fer) {}
~Monster() {}
void Attacked(int power) // 怪物被攻击。参数power表示主角对怪物的攻击力(即怪物丢失的血量)
{
if (m_status == MonsterState::Mons_Dead) {
cout << "怪物已死亡,不能再被攻击!" << endl;
return;
}
m_life -= power;
cout << "怪物受到" << power << "点伤害,";
if (m_status == MonsterState::Mons_Fer) {
if (m_life > 400) {
cout << "并对主角进行疯狂的反击!" << endl;
// 处理其他动作逻辑,例如反击
}
else if (m_life > 100) {
cout << "并对主角进行反击,怪物变得焦躁不安并开始呼唤支援!" << endl;
m_status = MonsterState::Mons_Worr;
// 处理其他动作逻辑,例如反击和呼唤支援
}
else if (m_life > 0) {
cout << "怪物变得恐惧并开始逃跑!" << endl;
m_status = MonsterState::Mons_Fear;
// 处理其他动作逻辑,例如逃跑
}
else {
cout << "已经死亡!" << endl;
m_status = MonsterState::Mons_Dead;
// 处理怪物死亡后事宜,例如怪物尸体定时消失等
}
}
else if (m_status == MonsterState::Mons_Worr) {
if (m_life > 100) {
cout << "并对主角进行反击,并继续急促的呼唤支援!" << endl;
// 处理其他动作逻辑,例如反击和呼唤支援
}
else if (m_life > 0) {
cout << "怪物变得恐惧并开始逃跑!" << endl;
m_status = MonsterState::Mons_Fear;
// 处理其他动作逻辑,例如逃跑
}
else {
cout << "已经死亡!" << endl;
m_status = MonsterState::Mons_Dead;
// 处理怪物死亡后事宜,例如怪物尸体定时消失等
}
}
else if (m_status == MonsterState::Mons_Fear) {
if (m_life > 0) {
cout << "怪物继续逃跑!" << endl;
// 处理其他动作逻辑,例如逃跑
}
else {
cout << "已经死亡!" << endl;
m_status = MonsterState::Mons_Dead;
// 处理怪物死亡后事宜,例如怪物尸体定时消失等
}
}
}
void DisplayStatus() const {
cout << "Monster Life: " << m_life << ", Status: " << static_cast<int>(m_status) << endl;
}
private:
int m_life;
MonsterState m_status;
};
我们使用如下代码来测试:
c++
Monster mons(500);
mons.Attacked(20);
mons.Attacked(100);
mons.Attacked(200);
mons.Attacked(170);
mons.Attacked(100);
mons.Attacked(100);
/*
怪物受到20点伤害,并对主角进行疯狂的反击!
怪物受到100点伤害,并对主角进行反击,怪物变得焦躁不安并开始呼唤支援!
怪物受到200点伤害,并对主角进行反击,并继续急促的呼唤支援!
怪物受到170点伤害,怪物变得恐惧并开始逃跑!
怪物受到100点伤害,已经死亡!
怪物已死亡,不能再被攻击!
*/
从上述代码可以看出来,其中用了很多的if、else语句,这导致如果有新的状态需要添加需要修改许多内容,难以维护。
下面我们通过使用状态模式对上述代码进行改造。
在状态模式中,怪物的每个状态都写成一个状态类(类似的情形,例如在策略模式中是将每个策略写成一个策略类),当然,应该为这些状态类抽象出一个统一的父类以便实现多态,然后在每个状态类中实现相关的业务逻辑。例如,对于怪物的"不安"状态可以实现为一个名字叫作Status_Worr的类,在该类中实现相关的业务逻辑,例如怪物对主角的反击和呼唤支援。这样就相当于把上述Monster
类的Attacked
成员函数的业务逻辑代码拆分到各个状态类中去实现,不但大大简化了Attacked
成员函数的实现代码,也实现了委托机制,即Attacked
成员函数把本该自已实现的功能委托给了各个状态类(中的成员函数)去实现。当然,必须持有该类的一个指针,才能把功能委托给该类。
首先,专门创建一个MonsterStatus.hpp
文件,
c++
#pragma once
// MonsterStatus.hpp
#include <iostream>
class Monster;
// 状态基类
class MonsterStatus {
public:
virtual ~MonsterStatus() {}
virtual void Attacked(int power, Monster* monster) = 0; // 纯虚函数,用于处理攻击逻辑
};
// 凶悍状态
class Status_Ferocious : public MonsterStatus {
public:
void Attacked(int power, Monster* monster) override {
int orglife = monster->getLife();
if ((orglife - power) > 400)//怪物原来处于凶悍状态,现在依旧处于凶悍状态状态未变
{
monster->setLife(orglife - power);// 怪物剩余的血量
cout << "怪物处于凶悍状态中,对主角进行疯狂的反击!" << std::endl;
//处理其他动作逻辑,例如反击
}
else
{
//不管下一个状态是什么,总之不会是凶悍状态,只可能是不安、恐惧、死亡状态之一,先把
//条件转到不安状态去(在不安状态中会进行再次判断)
delete monster->getCurrentState();
monster->setCurrentState(new Status_Worrisome);
monster->getCurrentState()->Attacked(power, monster);
}
std::cout << "怪物处于凶悍状态中,对主角进行疯狂的反击" << std::endl;
}
};
// 不安状态
class Status_Worrisome : public MonsterStatus {
public:
void Attacked(int power, Monster* monster) override {
int orgLife = monster->getLife();
if ((orgLife - power) > 100) { // 怪物仍然处于不安状态
monster->setLife(orgLife - power);
std::cout << "怪物处于不安状态中,对主角进行反击并呼唤支援!" << std::endl;
}
else {
delete monster->getCurrentState();
monster->setCurrentState(new Status_Fearful);
monster->getCurrentState()->Attacked(power, monster);
}
}
};
// 恐惧状态
class Status_Fearful : public MonsterStatus {
public:
void Attacked(int power, Monster* monster) override {
int orgLife = monster->getLife();
if ((orgLife - power) > 0) { // 怪物仍然处于恐惧状态
monster->setLife(orgLife - power);
std::cout << "怪物处于恐惧状态中,处于开始逃跑中!" << std::endl;
}
else { // 怪物死亡
delete monster->getCurrentState();
monster->setCurrentState(new Status_Dead);
monster->getCurrentState()->Attacked(power, monster);
}
}
};
// 死亡状态
class Status_Dead : public MonsterStatus {
public:
void Attacked(int power, Monster* monster) override {
// 怪物已经死亡,不做任何处理
std::cout << "怪物已死亡,不能再被攻击!" << std::endl;
}
};
从上面的代码中可以看到,原本在Monster
类Attacked
成员函数中实现的业务逻辑全部挪到了各个状态子类中去实现,这就大大简化了Attacked
成员函数中的业务逻辑代码,而且每个状态子类也只需要实现它自己的转移条件(动作)。
接着,需要重写原来的Monster
类 :
c++
#pragma once
#include "MonsterStatus.hpp"
enum class MonsterState
{
Mons_Fer, //凶悍
Mons_Worr, //不安
Mons_Fear, //恐惧
Mons_Dead //死亡
};
class Monster {
public:
Monster(int life)
:m_life(life), m_pState(nullptr)
{
m_pState = new Status_Ferocious();
}
~Monster() {
if (m_pState != nullptr)
delete m_pState;
}
void Attacked(int power) {
m_pState->Attacked(power, this);
}
int getLife() { return m_life; }
void setLife(int life) { m_life = life; }
MonsterStatus* getCurrentState() { return m_pState; }
void setCurrentState(MonsterStatus* pstate) { m_pState = pstate; }
private:
int m_life;//血量(生命值)
MonsterStatus* m_pState;//持有状态对象
};
下给出状态类:
c++
// 状态基类
class MonsterStatus {
public:
virtual ~MonsterStatus() {}
virtual void Attacked(int power, Monster* monster) = 0; // 纯虚函数,用于处理攻击逻辑
};
// 死亡状态
class Status_Dead : public MonsterStatus {
public:
void Attacked(int power, Monster* monster) override {
// 怪物已经死亡,不做任何处理
std::cout << "怪物已死亡,不能再被攻击!" << std::endl;
}
};
// 恐惧状态
class Status_Fearful : public MonsterStatus {
public:
void Attacked(int power, Monster* monster) override {
int orgLife = monster->getLife();
if ((orgLife - power) > 0) { // 怪物仍然处于恐惧状态
monster->setLife(orgLife - power);
std::cout << "怪物处于恐惧状态中,正在逃跑!" << std::endl;
}
else { // 怪物死亡
delete monster->getCurrentState();
monster->setCurrentState(new Status_Dead);
monster->getCurrentState()->Attacked(power, monster);
}
}
};
// 不安状态
class Status_Worrisome : public MonsterStatus {
public:
void Attacked(int power, Monster* monster) override {
int orgLife = monster->getLife();
if ((orgLife - power) > 100) {
monster->setLife(orgLife - power);
std::cout << "怪物处于不安状态中,对主角进行反击并呼唤支援!" << std::endl;
}
else {
delete monster->getCurrentState();
monster->setCurrentState(new Status_Fearful);
monster->getCurrentState()->Attacked(power, monster);
}
}
};
// 凶悍状态
class Status_Ferocious : public MonsterStatus {
public:
void Attacked(int power, Monster* monster) override {
int orgLife = monster->getLife();
if ((orgLife - power) > 400) { // 怪物仍然处于凶悍状态
monster->setLife(orgLife - power); // 怪物剩余的血量
std::cout << "怪物处于凶悍状态中,对主角进行疯狂的反击!" << std::endl;
}
else {
delete monster->getCurrentState();
monster->setCurrentState(new Status_Worrisome);
monster->getCurrentState()->Attacked(power, monster);
}
}
};
通过将业务逻辑代码委托给状态类,可以有效减少Monster
类Attacked
成员函数中的代码量。这就是状态类存在的价值一一使业务逻辑代码更加清晰和易于维护。
状态模式的一般包含3种角色:
- 上下文类 (Context ):该类的对象拥有多种状态以便对这些状态进行维护,这里指
Monster
类。 - 抽象状态类 (State ):定义接口以封装与环境类的一个特性状态相关的行为,在该类中声明各种不同状态对应的方法,在子类中实现这些方法,当然,如果相同或者默认的实现方法,也可以实现在抽象状态类中。这里指
MonsterStatus
类。 - 具体状态类 (ConcreteState ):抽象状态类的子类,用以实现与上下文类该状态相关的行为(环境类将行为委托给状态类实现),每个子类实现一个行为。这里指以
MonsterStatus
为父类的子类。
状态模式
引入状态设计模式的定义:允许一个对象(怪物)在其内部状态改变(例如,从凶悍状态改变为不安状态)时改变它的行为(例如,从疯狂反击变成反击并呼唤支援),对象看起来似乎修改了它的类。
上述定义的前半句不难理解,因为状态模式将各种状态封装为了独立的类,并将对主体的动作(这里指对怪物的攻击)委托给当前状态对象来做(调用每个状态子类的Attacked
成员函数)。
这个定义的后半句是什么意思呢?怪物对象因为状态的变化其行为也发生了变化(例如,从反击变为逃跑),从表面看起来就好像是这个怪物对象已经不属于当前这个类而是属于一个新类了(因为类对象行为都发生了变化),而实际上怪物对象只是通过引用不同的状态对象造成看起来像是怪物对象所属类发生改变的假象。
三、总结
在如下两种情况下,可以考虑使用状态模式:
- 如果对象需要根据自身当前状态进行不同行为, 同时状态的数量非常多且与状态相关的代码会频繁变更的话, 可使用状态模式。
- 如果某个类需要根据成员变量的当前值改变自身行为,从而需要使用大量的条件语句时,可使用该模式。
状态模式很好的遵守了单一职责原则和开闭原则。 将与特定状态相关的代码放在单独的类中,并且无需修改已有状态类和上下文就能引入新状态。通过消除臃肿的状态机条件语句简化上下文代码。
其行为也发生了变化(例如,从反击变为逃跑),从表面看起来就好像是这个怪物对象已经不属于当前这个类而是属于一个新类了(因为类对象行为都发生了变化),而实际上怪物对象只是通过引用不同的状态对象造成看起来像是怪物对象所属类发生改变的假象。
三、总结
在如下两种情况下,可以考虑使用状态模式:
- 如果对象需要根据自身当前状态进行不同行为, 同时状态的数量非常多且与状态相关的代码会频繁变更的话, 可使用状态模式。
- 如果某个类需要根据成员变量的当前值改变自身行为,从而需要使用大量的条件语句时,可使用该模式。
状态模式很好的遵守了单一职责原则和开闭原则。 将与特定状态相关的代码放在单独的类中,并且无需修改已有状态类和上下文就能引入新状态。通过消除臃肿的状态机条件语句简化上下文代码。
状态可被视为策略的扩展。 两者都基于组合机制: 它们都通过将部分工作委派给 "帮手" 对象来改变其在不同情景下的行为。 策略使得这些对象相互之间完全独立, 它们不知道其他对象的存在。 但状态模式没有限制具体状态之间的依赖, 且允许它们自行改变在不同情景下的状态。