当前位置: 首页 > news >正文

C++20之设计模式:状态模式

状态模式

      • 状态模式
        • 状态驱动的状态机
        • 手工状态机
        • Boost.MSM 中的状态机
        • 总结

状态模式

我必须承认:我的行为是由我的状态支配的。如果我没有足够的睡眠,我会有点累。如果我喝了酒,我就不会开车了。所有这些都是状态(states),它们支配着我的行为:我的感受,我能做什么,我不能做什。

当然,我可以从一种状态转换到另一种状态。我可以去喝杯咖啡,它能让我从瞌睡中清醒过来(我希望如此!)所以我们可以把咖啡当作触发器,让你真正从困倦过渡到清醒。这里,让我笨拙地为你解释一下:

         coffee
sleepy ----------> alert

所以,状态设计模式是一个非常简单的想法:状态控制行为;状态可以改变;唯一的问题是引发了状态的变更。

基本上有两种方式:

  • 状态是带有行为的实际类,这些行为将实际状态从一个转换到另一个
  • 状态和转换只是枚举。我们有一个称为状态机(state machine)的特殊组件,它执行实际的转换。

这两种方法都是可行的,但实际上第二种方法是最常见的。这两种我们都会过一遍,但我必须承认我只会简单浏览第一个,因为这不是人们通常做事情的方式。

状态驱动的状态机

我们将从最简单的例子开始:一个电灯开关。它只能处于开和关的状态。我们将构建一个任何状态都能够切换到其他状态的模型:虽然这反映了状态设计模式的经典实现(根据GoF的书),但我并不推荐这样做。

首先,让我们为电灯开关建模:它只有一种状态和一些从一种状态切换到另一种状态的方法:

class LightSwitch
{State* state;public:LightSwitch(){state = new OffState();}void set_state(State* state){this->state = state;}
};

这一切看起来都很合理。我们现在可以定义状态,在这个特定的情况下,它将是一个实际的类:

struct State
{virtual void on(LightSwitch* ls){cout << "Light is already on\n";}virtual void off(LightSwitch* ls){cout << "Light is already off\n";}
};

这个实现很不直观,所以我们需要慢慢地仔细地讨论它,因为从一开始,关于State类的任何东西都没有意义。

首先,State不是抽象的!你会认为一个你没有办法(或理由)达到的状态是抽象的。但事实并非如此。

第二,状态允许从一种状态切换到另一种状态。这对一个通情达理的人来说,毫无意义。想象一下电灯开关:它是改变状态的开关。人们并不指望State本身会改变自己,但它似乎就是这样做的。

第三,也许是最令人困惑的,State::on/off的默认行为声称我们已经处于这种状态!在我们实现示例的其余部分时,这一点将在某种程度上结合在一起。

现在我们实现OnOff状态:

struct OnState : State
{OnState() { cout << "Light turned on\n"; }void off(LightSwitch* ls) override;
};struct OffState : State
{OffState() { cout << "Light turned off\n"; }void on(LightSwitch* ls) override;
};

实现OnState::off和OffState::on允许状态本身切换到另一个状态!它看起来是这样的:

void OnState::off(LightSwitch* ls)
{cout << "Switching light off...\n";ls->set_state(new OffState());delete this;
} // same for OffState::on

这就是转换发生的地方。这个实现包含了对delete This的奇怪调用,这在真实的c++中是不常见的。这对初始分配状态的位置做出了非常危险的假设。例如,可以使用智能指针重写这个示例,但是使用指针和堆分配清楚地表明状态在这里被积极地销毁。如果状态有析构函数,它将触发,你将在这里执行额外的清理。

当然,我们确实希望开关本身也能切换状态,就像这样:

class LightSwitch
{...void on() { state->on(this); }void off() { state->off(this); }
};

因此,把所有这些放在一起,我们可以运行以下场景:

1 LightSwitch ls; // Light turned off
2 ls.on(); // Switching light on...
3 // Light turned on
4 ls.off(); // Switching light off...
5 // Light turned off
6 ls.off(); // Light is already off

我必须承认:我不喜欢这种方法,因为它不是直观的。当然,状态可以被告知(观察者模式)我们正在进入它。但是,状态转换到另一种状态的想法——根据GoF的书,这是状态模式的经典实现——似乎不是特别令人满意。

如果我们笨拙地说明从OffStateOnState的转换,则需要将其说明为

          LightSwitch::on() -> OffState::on()
OffState -------------------------------------> OnState

另一方面,从OnState到OnState的转换使用基状态类,这个类告诉你你已经处于那个状态

        LightSwitch::on() -> State::on()
OnState ----------------------------------> OnState

这里给出的示例可能看起来特别人为,所以我们现在将看看另一个手工创建的设置,其中的状态和转换被简化为枚举成员。

手工状态机

让我们尝试为一个典型的电话会话定义一个状态机。首先,我们将描述电话的状态:

enum class State
{off_hook,connecting,connected,on_hold,on_hook
};

我们现在还可以定义状态之间的转换,也可以定义为enum class:

enum class Trigger
{call_dialed,hung_up,call_connected,placed_on_hold,taken_off_hold,left_message,stop_using_phone
};

现在,这个状态机的确切规则,即可能的转换,需要存储在某个地方。

map<State, vector<pair<Trigger, State>>> rules;

这有点笨拙,但本质上map的键是我们移动的状态,值是一组表示Trigger-State的对,在此状态下可能的触发器以及使用触发器时所进入的状态。

让我们来初始化这个数据结构:

rules[State::off_hook] = {{Trigger::call_dialed, State::connecting},{Trigger::stop_using_phone, State::on_hook}
};rules[State::connecting] = {{Trigger::hung_up, State::off_hook},{Trigger::call_connected, State::connected}
};
//  more rules here

我们还需要一个启动状态,如果我们希望状态机在达到该状态后停止执行,我们还可以添加一个退出(终止)状态:

State currentState { State::off_hook },
exitState { State::on_hook };

完成这些之后,我们就不必为实际运行(我们使用orchestrating这个术语)状态机而构建单独的组件了。例如,如果我们想要构建电话的交互式模型,我们可以这样做:

while(true)
{cout << "The phone is currently " << currentState << endl;select_trigger:cout << "Select a trigger:" << "\n";int i = 0;for(auto &&item : rules[currentState]){cout << i++ << ". " << item.first << "\n";}int input;cin >> input;for(input < 0 || (input+1) > rules[currentState].size()){goto select_trigger;}currentState = rules[currentState][input].second;if(currentState == exitState) break;
}

首先:是的,我确实使用goto,这是一个很好的例子,说明在什么地方使用goto是合适的(译者注:一般不建议在程序里面使用goto,这样会使得程序的控制流比较混乱)。对于算法本身,这是相当明显的:我们让用户在当前状态上选择一个可用的触发器(operator<<状态和触发器都在幕后实现了),并且,如果触发器是有效的,我们通过使用前面创建的规则映射转换到它。

如果我们到达的状态是退出状态,我们就跳出循环。下面是一个与程序交互的示例。

1 The phone is currently off the hook
2 Select a trigger:
3 0. call dialed
4 1. putting phone on hook
5 0
6 The phone is currently connecting
7 Select a trigger:
8 0. hung up
9 1. call connected
10 1
11 The phone is currently connected
12 Select a trigger:
13 0. left message
14 1. hung up
15 2. placed on hold
16 2
17 The phone is currently on hold
18 Select a trigger:
19 0. taken off hold
20 1. hung up
21 1
22 The phone is currently off the hook
23 Select a trigger:
24 0. call dialed
25 1. putting phone on hook
26 1
27 We are done using the phone

这种手工状态机的主要优点是非常容易理解:状态和转换是普通的枚举类,转换集是在一个简单的std::map中定义的,开始和结束状态是简单的变量

Boost.MSM 中的状态机

在现实世界中,状态机要复杂得多。有时,你希望在达到某个状态时发生某些操作。在其他时候,你希望转换是有条件的,也就是说,你希望转换只在某些条件存在时发生。

Boost.MSM (Meta State Machine),一个状态机库,是Boost的一部分,你的状态机是一个通过CRTP继承自state_ machine_def的类:

struct PhoneStateMachine : state_machine_def<PhoneStateMachine>
{bool angry{ false };
}

我添加了一个bool变量来指示调用者是否angry(例如,在被搁置时); 我们稍后会用到它。现在,每个状态也可以驻留在状态机中,并且可以从state类继承:

struct OffHook : state<> {};
struct Connecting : state<>
{template<class Event, class FSM>void on_entry(Event const& evt, FSM&){cout << "We are connecting..." << endl;}// also on_exit
};
// other states omitted

如你所见,状态还可以定义在进入或退出特定状态时发生的行为。你也可以定义在转换时执行的行为(而不是当你到达一个状态时):这些也是类,但它们不需要从任何东西继承;相反,它们需要提供具有特定签名的operator():

struct PhoneBeingDestoryed
{template<class EVT, class FSM, class SourceState, class TargetState>void operator()(EVT const&, FSM& SourceState&, TargetState&){cout << "Phone breaks into a million pieces" << endl;}
};

正如你可能已经猜到的那样,这些参数提供了对状态机的引用,以及你将要进入和进入的状态。

最后,我们有守卫条件(guard condition):这些条件决定我们是否可以在第一时间使用一个转换。现在,我们的布尔变量angry不是MSM可用的形式,所以我们需要包装它:

struct CanDestoryPhone
{template<class EVT, class FSM, class SourceState, class TargetState>bool operator()(EVT const&, FSM& fsm, SourceState&, TargetState&){return fsm.angry;}
};

前面的例子创建了一个名为CanDestroyPhone的守卫条件,稍后我们可以在定义状态机时使用它。

为了定义状态机规则,Boost.MSM使用MPL(元编程库)。具体来说,转换表被定义为mpl::vector,每一行依次包含:

  • 源状态
  • 状态转换
  • 目标状态
  • 一个要执行的可选操作
  • 一个可选守卫条件

因此,有了所有这些,我们可以像下面这样定义一些电话呼叫规则:

struct transition_table : mpl::vector<Row<OffHook, CallDialed, Connecting>,Row<Connecting, CallConnected, Connected>,Row<Connected, PlacedOnHold, OnHold>,Row<OnHold, PhoneThrownIntoWall, PhoneDestoryed, PhoneBeingDestoryed, CanDestoryPhone>
>
{};

在前面的方法中,与状态不同,CallDialed之类的转换是可以在状态机类之外定义的类。它们不必继承自任何基类,而且很容易为空,但它们必须是类型。

transition_table的最后一行是最有趣的:它指定我们只能尝试在CanDestroyPhone保护条件下销毁电话,并且当电话实际上被销毁时,应该执行PhoneBeingDestroyed操作。

现在,我们可以添加更多的东西。首先,我们添加起始条件:因为我们正在使用Boost.MSM,起始条件是一个类型定义,而不是一个变量:

typedef OffHook initial_state;

最后,如果没有可能的转换,我们可以定义要发生的操作。它可能发生!比如,你把手机摔坏了,就不能再用了,对吧?

template <class FSM, class Event>
void no_transition(Event const& e, FSM&, int state)
{cout << "No transition from state " << state_names[state]<< " on event " << typeid(e).name() << endl;
}

Boost MSM将状态机分为前端(我们刚刚写的)和后端(运行它的部分)。使用后端API,我们可以根据前面的状态机定义构造状态机:

msm::back::state_machine<PhoneStateMachine> phone;

现在,假设存在info()函数,它只打印我们所处的状态,我们可以尝试orchestrating以下场景

1 info(); // The phone is currently off hook
2 phone.process_event(CallDialed{}); // We are connecting...
3 info(); // The phone is currently connecting
4 phone.process_event(CallConnected{});
5 info(); // The phone is currently connected
6 phone.process_event(PlacedOnHold{});
7 info(); // The phone is currently on hold
8 9
phone.process_event(PhoneThrownIntoWall{});
10 // Phone breaks into a million pieces
11
12 info(); // The phone is currently destroyed
13
14 phone.process_event(CallDialed{});
15 // No transition from state destroyed on event struct CallDialed

因此,这就是定义更复杂、具有工业强度的状态机的方式。

总结

首先,这是值得强调的Boost.MSM是Boost中两种状态机实现之一,另一种是Boost.statechart。我很确定还有很多其他的状态机实现。

其次,状态机的功能远不止这些。例如,许多库支持分层状态机的思想:例如,一个生病(Sick)的状态可以包含许多不同的子状态,如流感(Flu)水痘(Chickenpox)。如果你在处于感染流感的状态,你也同时处于生病的状态。

最后,有必要再次强调现代状态机与状态设计模式的原始形式之间的差异。重复api的存在(例如LightSwitch::on/off vs. State::on/off)以及自删除的存在在我的书中是明确的代码气味。不要误会我的方法是有效的,但它是不直观的和繁琐的。

相关文章:

C++20之设计模式:状态模式

状态模式 状态模式状态驱动的状态机手工状态机Boost.MSM 中的状态机总结 状态模式 我必须承认:我的行为是由我的状态支配的。如果我没有足够的睡眠&#xff0c;我会有点累。如果我喝了酒&#xff0c;我就不会开车了。所有这些都是状态(states)&#xff0c;它们支配着我的行为:…...

数据库安全综合治理方案(可编辑54页PPT)

引言&#xff1a;数据库安全综合治理方案是一个系统性的工作&#xff0c;需要从多个方面入手&#xff0c;综合运用各种技术和管理手段&#xff0c;确保数据库系统的安全稳定运行。 方案介绍&#xff1a; 数据库安全综合治理方案是一个综合性的策略&#xff0c;旨在确保数据库系…...

人工智能:大语言模型提示注入攻击安全风险分析报告下载

大语言模型提示注入攻击安全风险分析报告下载 今天分享的是人工智能AI研究报告&#xff1a;《大语言模型提示注入攻击安全风险分析报告》。&#xff08;报告出品方&#xff1a;大数据协同安全技术国家工程研究中心安全大脑国家新一代人工智能开放创新平台&#xff09; 研究报告…...

【购买源码时有许多需要注意的坑】

购买源码时有许多需要注意的“坑”&#xff0c;这些坑可能会对项目的后续开发和使用造成严重影响。以下是一些需要特别注意的方面&#xff1a; 源码的完整性 编译测试&#xff1a;确保到手的源码能够从头至尾编译、打包、部署和功能测试无误。这一步非常关键&#xff0c;因为只…...

CAS的三大问题和解决方案

一、ABA问题的解决方案 变量第一次读取的值是1&#xff0c;后来其他线程改成了3&#xff0c;然后又被其他线程修改成了1&#xff0c;原来期望的值是第一个1才会设置新值&#xff0c;第二个1跟期望不符合&#xff0c;但是&#xff0c;可以设置新值。 解决方案&#xff1a; &a…...

EDA和统计分析有什么区别

EDA&#xff08;Electronic Design Automation&#xff09;和统计分析在多个方面存在显著的区别&#xff0c;这些区别主要体现在它们的应用领域、目的、方法以及所使用的工具上。 EDA&#xff08;电子设计自动化&#xff09; 定义与目的&#xff1a; EDA是电子设计自动化&…...

CentOS 7 修改DNS

1、nmcli connection show 命令找到设备名称 # nmcli connection show NAME UUID TYPE DEVICE enp4s0 99559edf-4e0a-4bae-a528-6d75065261e9 ethernet enp4s0 2、nmcli connection modify 命令修改dns nmcli connection modif…...

PHP基础语法-Part2

if-else语句、switch语句 与其他语言相同 循环结构 for循环while循环do-while循环foreach循环&#xff0c;搭配数组使用 foreach ($age as $avlue) //只输出值 {xxx; } foreach ($age as $key > $avlue) //键和值都输出 {xxx; }foreach ($age as $key >…...

数据结构门槛-顺序表

顺序表 1. 线性表2. 顺序表2.1 静态顺序表2.2 动态顺序表2.2.1 动态数据表初始化和销毁2.2.2 动态数据表的尾插尾删2.2.3 动态数据表的头插头删2.2.4 动态数据表的中间部分插入删除2.2.5 动态数据表的查询数据位置 3. 总结 1. 线性表 线性表&#xff08;linear list&#xff0…...

软件测试面试准备工作

1、 什么是数据库? 答&#xff1a;数据库是按照某种数据模型组织起来的并存放二级存储器中的数据集合。 2、 什么是关系型数据库? 答&#xff1a;关系型数据库是建立在关系数据库模型基础上的数据库&#xff0c; 借助集合代数等概念和方法处理数据库中的数据。目前主流的关…...

Java面试八股之后Spring、spring mvc和spring boot的区别

Spring、spring mvc和spring boot的区别 Spring, Spring Boot和Spring MVC都是Spring框架家族的一部分&#xff0c;它们各自有其特定的用途和优势。下面是它们之间的主要区别&#xff1a; Spring: Spring 是一个开源的轻量级Java开发框架&#xff0c;最初由Rod Johnson创建&…...

linux对齐TOF和RGB摄像头画面

问题&#xff1a;TOF和RGB画面不对齐 linux同时接入TOF和RGB&#xff0c;两者出图时间是由驱动层控制&#xff08;RGB硬件触发出图&#xff09;&#xff0c;应用层只负责读取数据。 现在两者画面不对齐&#xff0c;发现是开始的时候两者出图数量不一致导致的。底层解决不了&a…...

配置linux客户端免密登录服务端linux主机的root用户

1、客户端与服务端的ip 客户端IP地址服务端IP地址 2、定位客户端&#xff0c;由客户端制作公私钥对 [rootclient ~]# ssh-keygen -t rsa &#xff08;RSA是非对称加密算法&#xff09; # 一路回车 3、定位客户端&#xff0c;将公钥上传到服务器端root账户 [rootc…...

SpringMVC实现文件上传

导入文件上传相关依赖 <!--文件上传--> <dependency><groupId>commons-fileupload</groupId><artifactId>commons-fileupload</artifactId><version>1.3.1</version> </dependency> <dependency><groupId>…...

计算机实验室排课查询小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;学生管理&#xff0c;教师管理&#xff0c;实验室信息管理&#xff0c;实验室预约管理&#xff0c;取消预约管理&#xff0c;实验课程管理&#xff0c;实验报告管理&#xff0c;报修信息管理&#xff0…...

分享几种电商平台商品数据的批量自动抓取方式

在当今数字化时代&#xff0c;电商平台作为商品交易的重要渠道&#xff0c;其数据对于商家、市场分析师及数据科学家来说具有极高的价值。批量自动抓取电商平台商品数据成为提升业务效率、优化市场策略的重要手段。本文将详细介绍几种主流的电商平台商品数据批量自动抓取方式&a…...

mysql面试(五)

前言 本章节从数据页的具体结构&#xff0c;分析到如何生成索引&#xff0c;如何构成B树的索引结构。 以及什么是聚簇索引&#xff0c;什么是联合索引 InnoDB数据结构 行数据 我看各种文档中有好多记录数据结构的&#xff0c;但是这些都是看完就忘的东西。在这里详细讲也没…...

微软全球蓝屏带来的思考及未来战争走向

微软全球蓝屏事件不仅揭示了技术层面的问题和挑战&#xff0c;还引发了对未来战争走向的一些深入思考。以下是关于这些思考的内容&#xff1a; 微软全球蓝屏带来的思考&#xff1a; 系统稳定性与安全性&#xff1a;微软全球蓝屏事件凸显了操作系统稳定性和安全性的重要性。一…...

以FastGPT为例提升Rag知识库应用中的检索召回命中率

提升Rag知识库应用中的检索召回命中率 在构建Rag&#xff08;Retrieval-Augmented Generation&#xff09;知识库应用时&#xff0c;检索召回知识片段的命中率是至关重要的。高效、准确的检索机制是确保AI系统能够精准响应用户查询的基础。当前&#xff0c;FastGPT主要采用三种…...

ffmpeg更改视频的帧率

note 视频帧率调整 帧率(fps-frame per second) 例如&#xff1a;原来帧率为30&#xff0c;调整后为1 现象&#xff1a;原来是每秒有30张图像&#xff0c;调整后每秒1张图像&#xff0c;看着图像很慢 实现&#xff1a;在每秒的时间区间里&#xff0c;取一张图像…...

设计模式13-单件模式

设计模式13-单件模式 写在前面对象性能模式典型模式1. 单例模式&#xff08;Singleton Pattern&#xff09;2. 享元模式&#xff08;Flyweight Pattern&#xff09;3. 原型模式&#xff08;Prototype Pattern&#xff09;4. 对象池模式&#xff08;Object Pool Pattern&#xf…...

怎么给PDF文件加密码?关于PDF文件加密的四种方法推荐

怎么给PDF文件加密码&#xff1f;给PDF文件加上密码是保护文件安全的一种重要方法&#xff0c;特别是当需要在不受授权的访问下保护敏感信息时。这个过程不仅仅是简单地设置密码&#xff0c;而是涉及到对文档内容和访问控制的深思熟虑。加密PDF文件可以有效防止未经授权的用户查…...

GoFly快速开发框架基于Go语言和Vue3开发后台管理附件管理插件包

说明 为了给客户提供更好的交互体验&#xff0c;框架把附件管理独立打包成插件包&#xff0c;这样附件管理接可以做个不通需求的附件管理插件包来满足不同甲方客户需求。 目前附件插件包有2个&#xff1a;一个基础包、一个高级包 附件插件包功能 1.基础包 统一管理业务系统…...

matlab实验:实验六MATLAB 数值计算与符号运算

题目1&#xff1a;&#xff08;线性方程组数值求解&#xff09; 1&#xff0e; 用不同的方法求解下面方程&#xff1a;&#xff08;方程原式参考 P369 实验 10&#xff0c;第 1 题&#xff09; 第 1 种&#xff0c;左除和求逆函数(inv) 第 2 种 &#xff0c; 用 符 号 运 算 的…...

基于STM32设计的老人摔倒检测系统(4G+华为云IOT)(193)

文章目录 一、前言1.1 项目介绍【1】项目功能介绍【2】项目硬件模块组成1.2 设计思路【1】整体设计思路【2】整体构架【3】上位机开发思路【4】供电方式1.3 项目开发背景【1】选题的意义【2】可行性分析【3】参考文献【4】课题研究的意义【5】国内外技术发展现状【6】课题研究思…...

PyTorch和TensorFlow概念及对比

PyTorch和TensorFlow是两个流行的深度学习框架&#xff0c;用于构建和训练机器学习和深度学习模型。它们各自有一些独特的特点和优点&#xff1a; 一 、PyTorch 动态计算图&#xff1a; PyTorch使用动态计算图&#xff08;Dynamic Computation Graph&#xff09;&#xff0c;…...

github的Codespaces是什么

目录 github的Codespaces是什么 一、定义与功能 二、特点与优势 三、工作原理 四、使用场景与限制 github的Codespaces是什么 GitHub的Codespaces是一个基于云的即时开发环境,它利用容器技术为开发者提供一个完全配置好的开发环境,以便他们能够直接在浏览器或通过Visua…...

Unity UGUI 之 图集

本文仅作学习笔记与交流&#xff0c;不作任何商业用途 本文包括但不限于unity官方手册&#xff0c;唐老狮&#xff0c;麦扣教程知识&#xff0c;引用会标记&#xff0c;如有不足还请斧正 本文在发布时间选用unity 2022.3.8稳定版本&#xff0c;请注意分别 1.什么是图集 精灵图…...

rust日常提问

rust 如何为类 添加一个函数 举例说明 在 Rust 中&#xff0c;我们通常使用 struct&#xff08;结构体&#xff09;来创建类似其他语言中的类&#xff08;class&#xff09;。Rust 中的结构体可以拥有关联函数&#xff08;associated functions&#xff09;&#xff0c;这些函数…...

Vue3与Element-plus配合 直接修改表格中的一项数据——控制输入框的显示与隐藏

利用控制与隐藏输入框,直接修改表格中的每一项数据。 <!-- 表格模块 --> <div><el-table :data"tablelist" style"width: 100%"><el-table-column align"center" prop"deposit" label"接单押金">&l…...