C++设计模式行为模式———状态模式

文章目录

  • 一、引言
  • 二、状态模式
  • 三、总结
  • 三、总结

一、引言

状态模式是一种行为设计模式, 让你能在一个对象的内部状态变化时改变其行为, 使其看上去就像改变了自身所属的类一样。其实现可完成类似有限状态机的功能。换句话说,一个对象可以处于多种不同的状态(当然,同一时刻只能处于某一种状态),从而让对象产生不同的行为。通过状态模式可以表达出这些不同的状态并实现对象在这些状态之间的转换。状态模式最突出的特点是用类来表示状态,这一点与策略模式有异曲同工之妙(策略模式中用类来表示策略)。


二、状态模式

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

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

随着时间推移,吃了饭、睡了觉都属于发生的事件(转移条件,也可以看成是触发状态发生状态变化的行为或动作),而精神饱满、饥饿、困倦的不同属于是不同的状态。

本文以闯关游戏的开发为例,游戏中怪物的生命值是500血,怪物的状态会随着血量的变化而变化。

  • 怪物血量多于400点时,怪物处于凶悍状态,此时怪物会对主角进行反击。
  • 怪物血量小于或等于400点但多于100点时,怪物处于不安状态,此时怪物会对主角进行反击并开始呼唤附近的其他怪物支援。
  • 怪物血量小于或等于100点时,怪物处于恐惧状态,此时怪物开始逃命。
  • 怪物血量小于或等于0点时,怪物处于死亡状态,此时怪物不能再被攻击。

这里设计了四种状态,凶悍、不安、恐惧、死亡,而促使怪物状态发送变化的唯一的事件是主角对怪物的攻击。当怪物处于各种状态时,也会做出不同的动作。例如,怪物处于凶悍状态时会对主角进行反击。

我们用枚举值来表示怪物状态:

enum class MonsterState
{Mons_Fer,	//凶悍Mons_Worr,	//不安Mons_Fear,	//恐惧Mons_Dead	//死亡
};

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

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;
};

我们使用如下代码来测试:

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文件,

#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;}
};

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

接着,需要重写原来的Monster类 :

#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;//持有状态对象
};

下给出状态类:

// 状态基类
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);}}
};

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

在这里插入图片描述

状态模式的一般包含3种角色:

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

状态模式

在这里插入图片描述

引入状态设计模式的定义:允许一个对象(怪物)在其内部状态改变(例如,从凶悍状态改变为不安状态)时改变它的行为(例如,从疯狂反击变成反击并呼唤支援),对象看起来似乎修改了它的类。

上述定义的前半句不难理解,因为状态模式将各种状态封装为了独立的类,并将对主体的动作(这里指对怪物的攻击)委托给当前状态对象来做(调用每个状态子类的Attacked成员函数)。

这个定义的后半句是什么意思呢?怪物对象因为状态的变化其行为也发生了变化(例如,从反击变为逃跑),从表面看起来就好像是这个怪物对象已经不属于当前这个类而是属于一个新类了(因为类对象行为都发生了变化),而实际上怪物对象只是通过引用不同的状态对象造成看起来像是怪物对象所属类发生改变的假象。


三、总结

在如下两种情况下,可以考虑使用状态模式:

  • 如果对象需要根据自身当前状态进行不同行为, 同时状态的数量非常多且与状态相关的代码会频繁变更的话, 可使用状态模式。
  • 如果某个类需要根据成员变量的当前值改变自身行为,从而需要使用大量的条件语句时,可使用该模式。

状态模式很好的遵守了单一职责原则和开闭原则。 将与特定状态相关的代码放在单独的类中,并且无需修改已有状态类和上下文就能引入新状态。通过消除臃肿的状态机条件语句简化上下文代码。

其行为也发生了变化(例如,从反击变为逃跑),从表面看起来就好像是这个怪物对象已经不属于当前这个类而是属于一个新类了(因为类对象行为都发生了变化),而实际上怪物对象只是通过引用不同的状态对象造成看起来像是怪物对象所属类发生改变的假象。


三、总结

在如下两种情况下,可以考虑使用状态模式:

  • 如果对象需要根据自身当前状态进行不同行为, 同时状态的数量非常多且与状态相关的代码会频繁变更的话, 可使用状态模式。
  • 如果某个类需要根据成员变量的当前值改变自身行为,从而需要使用大量的条件语句时,可使用该模式。

状态模式很好的遵守了单一职责原则和开闭原则。 将与特定状态相关的代码放在单独的类中,并且无需修改已有状态类和上下文就能引入新状态。通过消除臃肿的状态机条件语句简化上下文代码。

状态可被视为策略的扩展。 两者都基于组合机制: 它们都通过将部分工作委派给 “帮手” 对象来改变其在不同情景下的行为。 策略使得这些对象相互之间完全独立, 它们不知道其他对象的存在。 但状态模式没有限制具体状态之间的依赖, 且允许它们自行改变在不同情景下的状态。

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

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

相关文章

vscode自动打印日志插件

自动日志工具&#xff08;Auto Logger Log&#xff09; 概述 自动日志工具&#xff08;Auto Logger Log&#xff09; 是一款 VS Code 扩展&#xff0c;用于简化生成调试日志的过程。它可以为选中的变量自动生成打印语句&#xff0c;帮助开发者快速记录和调试代码。该扩展支持多…

优雅的不等式——Hard

上一文《Easy》末尾出现了又要我们证明的例子&#xff0c;Hard难度就是继续答题答下去 其实一样可以用那篇文章https://zhuanlan.zhihu.com/p/669285539中的式子继续算下去&#xff0c;但是有三个系数&#xff0c;实在是太费时间和人力了 翻到下面的第十九种类型&#xff0c;可…

虚拟局域网PPTP配置与验证(二)

虚拟局域网PPTP配置与验证(二) windows VPN客户端linux 客户端openwrt客户端性能验证虚拟局域网PPTP配置与验证(一)虚拟局域网PPTP配置与验证(二) : 本文介绍几种客户端连接PPTP服务端的方法,同时对linux/windows/openwrt 操作系统及x86、arm硬件平台下PPTP包转发性能进…

Move 合约部署踩坑笔记:如何解决 Sui 客户端发布错误Committing lock file

Move 共学活动&#xff1a;快速上手 Move 开发 为了帮助更多开发者快速了解和掌握 Move 编程语言&#xff0c;Move 共学活动由 HOH 社区、HackQuest、OpenBuild、KeyMap 联合发起。该活动旨在为新手小白提供一个良好的学习平台&#xff0c;带领大家一步步熟悉 Move 语言&#…

介绍一下strupr(arr);(c基础)

hi , I am 36 适合对象c语言初学者 strupr(arr)&#xff1b;函数是把arr数组变为大写字母 格式 #include<string.h> strupr(arr); 返回值为arr 链接分享一下arr的意义(c基础)(必看)(牢记)-CSDN博客 #define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #incl…

进程间通信5:信号

引入 我们之前学习了信号量&#xff0c;信号量和信号可不是一个东西&#xff0c;不能混淆。 信号是什么以及一些基础概念 信号是一种让进程给其他进程发送异步消息的方式 信号是随时产生的&#xff0c;无法预测信号可以临时保存下来&#xff0c;之后再处理信号是异步发送的…

浅谈网络 | 传输层之套接字Socket

目录 基于 TCP 协议的 Socket 程序调用过程基于 UDP 协议的 Socket 程序函数调用过程服务器如何接入更多的项目构建高并发服务端&#xff1a;从多进程到 IO 多路复用 在前面&#xff0c;我们已经介绍了 TCP 和 UDP 协议&#xff0c;但还没有实践过。接下来这一节&#xff0c;我…

Spire.PDF for .NET【页面设置】演示:打开 PDF 时自动显示书签或缩略图

用户打开 PDF 文档时&#xff0c;他们会看到 PDF 的初始视图。默认情况下&#xff0c;打开 PDF 时不会显示书签面板或缩略图面板。在本文中&#xff0c;我们将演示如何设置文档属性&#xff0c;以便每次启动文件时都会打开书签面板或缩略图面板。 Spire.PDF for .NET 是一款独…

FileLink内外网文件共享系统与FTP对比:高效、安全的文件传输新选择

随着信息技术的不断进步&#xff0c;文件传输和共享已经成为企业日常工作中不可或缺的一部分。传统的FTP&#xff08;File Transfer Protocol&#xff09;协议在一定程度上为文件共享提供了便利&#xff0c;但随着企业对文件传输的需求越来越复杂&#xff0c;FileLink内外网文件…

神经网络归一化方法总结

在深度学习中&#xff0c;归一化 是提高训练效率和稳定性的关键技术。以下是几种常见的神经网络归一化方法的总结&#xff0c;包括其核心思想、适用场景及优缺点。 四种归一化 特性Batch NormalizationGroup NormalizationLayer NormalizationInstance Normalization计算维度…

ORB-SLAM2源码学习:Initializer.cc:Initializer::ComputeF21地图初始化——计算基础矩阵

前言 在平面场景我们通过求解单应矩阵H来求解位姿&#xff0c;但是我们在实际中常见的都是非平面场景&#xff0c; 此时需要用基础矩阵F求解位姿。 1.函数声明 cv::Mat Initializer::ComputeF21(const vector<cv::Point2f> &vP1, const vector<cv::Point2f>…

离散化 C++

题目 解题思路 将所有对坐标的访问用map映射到一个新的坐标轴上再在新的坐标轴上进行加法用前缀和快速求出区间的和 代码实现 #include<iostream> #include<algorithm> #include<unordered_map>using namespace std;typedef pair<int, int> PII;con…

uniop触摸屏维修eTOP40系列ETOP40-0050

在现代化的工业与商业环境中&#xff0c;触摸屏设备已成为不可或缺的人机交互界面。UNIOP&#xff0c;作为一个知名的触摸屏品牌&#xff0c;以其高性能、稳定性和用户友好性&#xff0c;广泛应用于各种自动化控制系统、自助服务终端以及高端展示系统中。然而&#xff0c;即便如…

机器学习与图像处理中上采样与下采样

一、机器学习中的上采样 目的&#xff1a;在机器学习中&#xff0c;上采样用于处理不平衡数据集&#xff0c;即某些类别的样本数量远多于其他类别。上采样的目标是通过增加少数类样本的数量来平衡类别分布&#xff0c;从而提高模型对少数类的识别能力。 1.随机过采样&#xff0…

Unity中动态生成贴图并保存成png图片实现

实现原理&#xff1a; 要生成长x宽y的贴图&#xff0c;就是生成x*y个像素填充到贴图中&#xff0c;如下图&#xff1a; 如果要改变局部颜色&#xff0c;就是从x1到x2(x1<x2),y1到y2(y1<y2)这个范围做处理&#xff0c; 或者要想做圆形就是计算距某个点&#xff08;x1,y1&…

DHCP服务(包含配置过程)

目录 一、 DHCP的定义 二、 使用DHCP的好处 三、 DHCP的分配方式 四、 DHCP的租约过程 1. 客户机请求IP 2. 服务器响应 3. 客户机选择IP 4. 服务器确定租约 5. 重新登录 6. 更新租约 五、 DHCP服务配置过程 一、 DHCP的定义 DHCP&#xff08;Dynamic Host Configur…

认识RabbitMq和RabbitMq的使用

1 认识RabbitMq RabbitMQ是⼀个消息中间件&#xff0c;也是⼀个生产者消费者模型&#xff0c;它负责接收&#xff0c;存储并转发消息。 2.1 Producer和Consumer Producer&#xff1a;生产者&#xff0c;是RabbitMQServer的客户端&#xff0c;向RabbitMQ发送消息 Consumer&…

HTML飞舞的爱心

目录 系列文章 写在前面 完整代码 代码分析 写在后面 系列文章 序号目录1HTML满屏跳动的爱心&#xff08;可写字&#xff09;2HTML五彩缤纷的爱心3HTML满屏漂浮爱心4HTML情人节快乐5HTML蓝色爱心射线6HTML跳动的爱心&#xff08;简易版&#xff09;7HTML粒子爱心8HTML蓝色…

Excel把其中一张工作表导出成一个新的文件

excel导出一张工作表 一个Excel表里有多个工作表&#xff0c;怎么才能导出一个工作表&#xff0c;让其生成新的Excel文件呢&#xff1f; 第一步&#xff1a;首先打开Excel表格&#xff0c;然后选择要导出的工作表的名字&#xff0c;比如“Sheet1”&#xff0c;把鼠标放到“She…

Redis-09 SpringBoot集成Redis

Jedis 和 lettuce 基本已经过时 集成RedisTemplate 单机 1.建 Modul 2.改pom <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instanc…