设计模式学习[14]---状态模式

文章目录

  • 前言
    • 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一样,但是运用场合和表达的含义是不同的。
在策略模式中,客户端通常需要主动指定环境类对象所要使用的策略对象是哪个,而在状态模式中,环境类对象在各个状态之间切换,客户端基本不需要了解各种状态对象,也不需要和状态对象交互。

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

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

相关文章

【.NET】Kafka消息队列介绍,使用Confluent.Kafka集成Kafka消息队列

一、Kafka介绍 kafka是一种高吞吐量、分布式、可扩展的消息中间件系统&#xff0c;最初由LinkedIn公司开发。随着不断的发展&#xff0c;在最新的版本中它定义为分布式的流处理平台&#xff0c;现在在大数据应用中也是十分广泛。 它可以处理大量的实时数据流&#xff0c;被广…

基于ESP32的桌面小屏幕实战[5]:PCB下单

1. 焊接调试前准备 PCB下单 点击“PCB下单” 检查一下DRC 确认无错误之后&#xff0c;确认下单 然后就会跳转到下面的网页 基本上保持默认选项即可。可以看到“焊盘喷镀”有3个选项。 在选择表面处理工艺时&#xff0c;应综合考虑产品的具体需求、环保法规以及成本等因素。例…

下载b站高清视频

需要使用的edge上的一个扩展插件&#xff0c;所以选择使用edge浏览器。 1、在edge浏览器上下载 强力视频下载合并 扩展插件 2、在edge上打开b站&#xff0c;登录自己账号&#xff08;登录后才能下载到高清&#xff01;&#xff01;&#xff09;。打开一个视频&#xff0c;选择自…

【蓝桥杯研究生组】第14届Java试题答案整理

试题链接&#xff1a;链接 A题 满足条件的答案有&#xff1a;35813116 public class TianShu {public static void main(String[] args) {int ans 0;// 2000.1.1 - 2000000.1.1// 年份是月份的倍数&#xff0c;也是日的倍数for (int year2000; year<2000000; year) {for …

从0到机器视觉工程师(二):封装调用静态库和动态库

目录 静态库 编写静态库 使用静态库 方案一 方案二 动态库 编写动态库 使用动态库 方案一 方案二 方案三 总结 静态库 静态库是在编译时将库的代码合并到最终可执行程序中的库。静态库的优势是在编译时将所有代码包含在程序中&#xff0c;可以使程序独立运行&…

【LeetCode Hot100 二分查找】搜索插入位置、搜索二维矩阵、搜索旋转排序数组、寻找两个正序数组的中位数

二分查找 搜索插入位置搜索二维矩阵在排序数组中查找元素的第一个和最后一个位置寻找旋转排序数组中的最小值搜索旋转排序数组寻找两个正序数组的中位数&#xff08;hard&#xff09; 搜索插入位置 给定一个排序数组和一个目标值&#xff0c;在数组中找到目标值&#xff0c;并…

你已经分清JAVA中JVM、JDK与JRE的作用和关系了吗?

你已经分清JAVA中JVM、JDK与JRE的作用和关系了吗&#xff1f; 一. JVM、JDK与JRE的关系二. JVM、JDK与JRE的作用2.1 什么是JVM&#xff1f;2.2 什么是JDK&#xff1f;2.3 什么是JRE&#xff1f; 前言 点个免费的赞和关注&#xff0c;有错误的地方请指出&#xff0c;看个人主页有…

在不到 5 分钟的时间内将威胁情报 PDF 添加为 AI 助手的自定义知识

作者&#xff1a;来自 Elastic jamesspi 安全运营团队通常会维护威胁情报报告的存储库&#xff0c;这些报告包含由报告提供商生成的大量知识。然而&#xff0c;挑战在于&#xff0c;这些报告的内容通常以 PDF 格式存在&#xff0c;使得在处理安全事件或调查时难以检索和引用相关…

数据挖掘——朴素贝叶斯分类

数据挖掘——朴素贝叶斯分类 朴素贝叶斯分类极大后验假设独立性假设贝叶斯分类器总结 朴素贝叶斯分类 什么是分类&#xff1f; 找出描述和区分数据类或概念的模型&#xff0c;以便能够使用模型预测未知的对象的类标号 概念区分 分类与回归 分类是预测分类&#xff08;离散、…

LabVIEW在反馈控制时如何解决带约束的控制问题

在LabVIEW中&#xff0c;解决带约束的反馈控制问题通常需要使用先进的控制算法或特定的方法来满足约束条件&#xff0c;同时保证控制系统的性能和稳定性。以下是解决这类问题的一些常用方法和步骤&#xff1a; ​ 1. 定义控制问题及约束条件 确定被控对象的动态特性&#xff08…

机器人对物体重定向操作的发展简述

物体重定向操作的发展简述 前言1、手内重定向和外部重定向2、重定向原语3、重定向状态转换网络4、连续任意姿态的重定向5、利用其他环境约束重定向总结Reference 前言 对于一些特殊的任务&#xff08;如装配和打包&#xff09;&#xff0c;对物体放置的位姿由明确的要求&#…

Mysql数据实时同步到Es上

同步方案 ① 同步双写 同步双写实一种数据同步策略&#xff0c;它指的是在主数据库(如mysql) 上进行数据修改操作&#xff0c;同时将这些修改同步写入到ES 中&#xff0c;这种策略旨在确保两个数据库之间的数据一致性&#xff0c;并且优化系统的读写性能。 目标 同步双写是…

力扣66 加一

class Solution:def plusOne(self, digits: List[int]) -> List[int]:# 从最低位开始加一for i in range(len(digits) - 1, -1, -1):if digits[i] < 9:digits[i] 1return digitsdigits[i] 0# 如果所有位都是9&#xff0c;需要增加一位&#xff0c;例如 999 -> 1000r…

代码段中使用数据、栈

代码段中使用数据 改进之后 代码段中使用栈 在数据段中专门空出一段&#xff0c;作为栈 将数据、代码、栈放入不同段中

OpenCV的TickMeter计时类

OpenCV的TickMeter计时类 1. TickMeter是一个计时的类1.1 计算耗时1.2 计算循环的平均耗时和FPS1.3 function 2. 案例 1. TickMeter是一个计时的类 https://docs.opencv.org/4.x/d9/d6f/classcv_1_1TickMeter.html#details 1.1 计算耗时 TickMeter tm;tm.start();// do some…

Fabric部署-docker安装

一&#xff1a;安装docker 1.先卸载旧docker apt-get remove docker docker-engine docker.io containerd runc PS&#xff1a;新开的虚拟机输入命令后是这样的。 2.更新软件包 在终端中执行以下命令来更新Ubuntu软件包列表和已安装软件的版本: sudo apt update sudo apt …

【CSS】 ---- CSS 实现图片背景清除的滑动效果三种方法

1. 实现效果 1.1 removebg 实现图片背景的去除 1.2 gitee 登录界面的项目协同效果 2. 实现分析 最常见的方法就是通过 JS 定位获取设置对应盒子的宽度&#xff1b;removebg 使用的方法是 clip-path: polygon 来设置图片的显示区域&#xff1b;gitee 使用的方法是 clip: rect …

开源模型迎来颠覆性突破:DeepSeek-V3与Qwen2.5如何重塑AI格局?

不用再纠结选择哪个AI模型了&#xff01;chatTools 一站式提供o1推理模型、GPT4o、Claude和Gemini等多种选择&#xff0c;快来体验吧&#xff01; 在全球人工智能模型快速发展的浪潮中&#xff0c;开源模型正逐渐成为一股不可忽视的力量。近日&#xff0c;DeepSeek-V3和Qwen 2.…

微信开发工具git提交到码云

超简单&#xff0c;适用新手快速实现新项目备份到码云。步骤如下&#xff1a; 1、先在码云创建一个仓库&#xff0c;不要初始化readme文件 2、点击微信开发工具版本管理&#xff0c;如果第一次&#xff0c;会提示初始化仓库&#xff0c;照做就行 3、配置一些git信息 输入你的码…

PHP7和PHP8的最佳实践

php 7 和 php 8 的最佳实践包括&#xff1a;使用类型提示以避免运行时错误&#xff1b;利用命名空间组织代码并避免命名冲突&#xff1b;采用命名参数、联合类型等新特性增强可读性&#xff1b;用错误处理优雅地处理异常&#xff1b;关注性能优化&#xff0c;如避免全局变量和选…