设计模式——状态模式
引言
状态模式是一种行为设计模式, 让你能在一个对象的内部状态变化时改变其行为, 使其看上去就像改变了自身所属的类一样。
问题
状态模式与有限状态机 的概念紧密相关。

其主要思想是程序在任意时刻仅可处于几种有限的状态中。 在任何一个特定状态中, 程序的行为都不相同, 且可瞬间从一个状态切换到另一个状态。 不过, 根据当前状态, 程序可能会切换到另外一种状态, 也可能会保持当前状态不变。 这些数量有限且预先定义的状态切换规则被称为转移。
你还可将该方法应用在对象上。 假如你有一个 文档Document类。 文档可能会处于 草稿Draft 、 审阅中Moderation和 已发布Published三种状态中的一种。 文档的 publish发布方法在不同状态下的行为略有不同:
- 处于
草稿状态时, 它会将文档转移到审阅中状态。 - 处于
审阅中状态时, 如果当前用户是管理员, 它会公开发布文档。 - 处于
已发布状态时, 它不会进行任何操作。

状态机通常由众多条件运算符 ( if或 switch ) 实现, 可根据对象的当前状态选择相应的行为。 “状态” 通常只是对象中的一组成员变量值。 即使你之前从未听说过有限状态机, 你也很可能已经实现过状态模式。 下面的代码应该能帮助你回忆起来。
class Document isfield state: string// ……method publish() isswitch (state)"draft":state = "moderation"break"moderation":if (currentUser.role == "admin")state = "published"break"published":// 什么也不做。break// ……
当我们逐步在 文档类中添加更多状态和依赖于状态的行为后, 基于条件语句的状态机就会暴露其最大的弱点。 为了能根据当前状态选择完成相应行为的方法, 绝大部分方法中会包含复杂的条件语句。 修改其转换逻辑可能会涉及到修改所有方法中的状态条件语句, 导致代码的维护工作非常艰难。
这个问题会随着项目进行变得越发严重。 我们很难在设计阶段预测到所有可能的状态和转换。 随着时间推移, 最初仅包含有限条件语句的简洁状态机可能会变成臃肿的一团乱麻。
解决方案
状态模式建议为对象的所有可能状态新建一个类, 然后将所有状态的对应行为抽取到这些类中。
原始对象被称为上下文 (context), 它并不会自行实现所有行为, 而是会保存一个指向表示当前状态的状态对象的引用, 且将所有与状态相关的工作委派给该对象。

如需将上下文转换为另外一种状态, 则需将当前活动的状态对象替换为另外一个代表新状态的对象。 采用这种方式是有前提的: 所有状态类都必须遵循同样的接口, 而且上下文必须仅通过接口与这些对象进行交互。
这个结构可能看上去与策略模式相似, 但有一个关键性的不同——在状态模式中, 特定状态知道其他所有状态的存在, 且能触发从一个状态到另一个状态的转换; 策略则几乎完全不知道其他策略的存在。
真实世界类比
智能手机的按键和开关会根据设备当前状态完成不同行为:
- 当手机处于解锁状态时, 按下按键将执行各种功能。
- 当手机处于锁定状态时, 按下任何按键都将解锁屏幕。
- 当手机电量不足时, 按下任何按键都将显示充电页面。
状态模式结构

-
上下文 (Context) 保存了对于一个具体状态对象的引用, 并会将所有与该状态相关的工作委派给它。 上下文通过状态接口与状态对象交互, 且会提供一个设置器用于传递新的状态对象。
-
状态 (State) 接口会声明特定于状态的方法。 这些方法应能被其他所有具体状态所理解, 因为你不希望某些状态所拥有的方法永远不会被调用。
-
具体状态 (Concrete States) 会自行实现特定于状态的方法。 为了避免多个状态中包含相似代码, 你可以提供一个封装有部分通用行为的中间抽象类。
状态对象可存储对于上下文对象的反向引用。 状态可以通过该引用从上下文处获取所需信息, 并且能触发状态转移。
-
上下文和具体状态都可以设置上下文的下个状态, 并可通过替换连接到上下文的状态对象来完成实际的状态转换。
伪代码
在本例中, 状态模式将根据当前回放状态, 让媒体播放器中的相同控件完成不同的行为。

播放器的主要对象总是会连接到一个负责播放器绝大部分工作的状态对象中。 部分操作会更换播放器当前的状态对象, 以此改变播放器对于用户互动所作出的反应。
// 音频播放器(AudioPlayer)类即为上下文。它还会维护指向状态类实例的引用,
// 该状态类则用于表示音频播放器当前的状态。
class AudioPlayer isfield state: Statefield UI, volume, playlist, currentSongconstructor AudioPlayer() isthis.state = new ReadyState(this)// 上下文会将处理用户输入的工作委派给状态对象。由于每个状态都以不// 同的方式处理输入,其结果自然将依赖于当前所处的状态。UI = new UserInterface()UI.lockButton.onClick(this.clickLock)UI.playButton.onClick(this.clickPlay)UI.nextButton.onClick(this.clickNext)UI.prevButton.onClick(this.clickPrevious)// 其他对象必须能切换音频播放器当前所处的状态。method changeState(state: State) isthis.state = state// UI 方法会将执行工作委派给当前状态。method clickLock() isstate.clickLock()method clickPlay() isstate.clickPlay()method clickNext() isstate.clickNext()method clickPrevious() isstate.clickPrevious()// 状态可调用上下文的一些服务方法。method startPlayback() is// ……method stopPlayback() is// ……method nextSong() is// ……method previousSong() is// ……method fastForward(time) is// ……method rewind(time) is// ……// 所有具体状态类都必须实现状态基类声明的方法,并提供反向引用指向与状态相
// 关的上下文对象。状态可使用反向引用将上下文转换为另一个状态。
abstract class State isprotected field player: AudioPlayer// 上下文将自身传递给状态构造函数。这可帮助状态在需要时获取一些有用的// 上下文数据。constructor State(player) isthis.player = playerabstract method clickLock()abstract method clickPlay()abstract method clickNext()abstract method clickPrevious()// 具体状态会实现与上下文状态相关的多种行为。
class LockedState extends State is// 当你解锁一个锁定的播放器时,它可能处于两种状态之一。method clickLock() isif (player.playing)player.changeState(new PlayingState(player))elseplayer.changeState(new ReadyState(player))method clickPlay() is// 已锁定,什么也不做。method clickNext() is// 已锁定,什么也不做。method clickPrevious() is// 已锁定,什么也不做。// 它们还可在上下文中触发状态转换。
class ReadyState extends State ismethod clickLock() isplayer.changeState(new LockedState(player))method clickPlay() isplayer.startPlayback()player.changeState(new PlayingState(player))method clickNext() isplayer.nextSong()method clickPrevious() isplayer.previousSong()class PlayingState extends State ismethod clickLock() isplayer.changeState(new LockedState(player))method clickPlay() isplayer.stopPlayback()player.changeState(new ReadyState(player))method clickNext() isif (event.doubleclick)player.nextSong()elseplayer.fastForward(5)method clickPrevious() isif (event.doubleclick)player.previous()elseplayer.rewind(5)
状态模式适合应用场景
- 如果对象需要根据自身当前状态进行不同行为, 同时状态的数量非常多且与状态相关的代码会频繁变更的话, 可使用状态模式。
- 模式建议你将所有特定于状态的代码抽取到一组独立的类中。 这样一来, 你可以在独立于其他状态的情况下添加新状态或修改已有状态, 从而减少维护成本。
- 如果某个类需要根据成员变量的当前值改变自身行为, 从而需要使用大量的条件语句时, 可使用该模式。
- 状态模式会将这些条件语句的分支抽取到相应状态类的方法中。 同时, 你还可以清除主要类中与特定状态相关的临时成员变量和帮手方法代码。
- 当相似状态和基于条件的状态机转换中存在许多重复代码时, 可使用状态模式。
- 状态模式让你能够生成状态类层次结构, 通过将公用代码抽取到抽象基类中来减少重复。
实现方式
-
确定哪些类是上下文。 它可能是包含依赖于状态的代码的已有类; 如果特定于状态的代码分散在多个类中, 那么它可能是一个新的类。
-
声明状态接口。 虽然你可能会需要完全复制上下文中声明的所有方法, 但最好是仅把关注点放在那些可能包含特定于状态的行为的方法上。
-
为每个实际状态创建一个继承于状态接口的类。 然后检查上下文中的方法并将与特定状态相关的所有代码抽取到新建的类中。
在将代码移动到状态类的过程中, 你可能会发现它依赖于上下文中的一些私有成员。 你可以采用以下几种变通方式:
- 将这些成员变量或方法设为公有。
- 将需要抽取的上下文行为更改为上下文中的公有方法, 然后在状态类中调用。 这种方式简陋却便捷, 你可以稍后再对其进行修补。
- 将状态类嵌套在上下文类中。 这种方式需要你所使用的编程语言支持嵌套类。
-
在上下文类中添加一个状态接口类型的引用成员变量, 以及一个用于修改该成员变量值的公有设置器。
-
再次检查上下文中的方法, 将空的条件语句替换为相应的状态对象方法。
-
为切换上下文状态, 你需要创建某个状态类实例并将其传递给上下文。 你可以在上下文、 各种状态或客户端中完成这项工作。 无论在何处完成这项工作, 该类都将依赖于其所实例化的具体类。
状态模式优缺点
- 单一职责原则。 将与特定状态相关的代码放在单独的类中。
- 开闭原则。 无需修改已有状态类和上下文就能引入新状态。
- 通过消除臃肿的状态机条件语句简化上下文代码。
- 如果状态机只有很少的几个状态, 或者很少发生改变, 那么应用该模式可能会显得小题大作。
与其他模式的关系
-
桥接模式、 状态模式和策略模式 (在某种程度上包括适配器模式) 模式的接口非常相似。 实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题。
-
状态可被视为策略的扩展。 两者都基于组合机制: 它们都通过将部分工作委派给 “帮手” 对象来改变其在不同情景下的行为。 策略使得这些对象相互之间完全独立, 它们不知道其他对象的存在。 但状态模式没有限制具体状态之间的依赖, 且允许它们自行改变在不同情景下的状态。
相关文章:
设计模式——状态模式
引言 状态模式是一种行为设计模式, 让你能在一个对象的内部状态变化时改变其行为, 使其看上去就像改变了自身所属的类一样。 问题 状态模式与有限状态机 的概念紧密相关。 其主要思想是程序在任意时刻仅可处于几种有限的状态中。 在任何一个特定状态中…...
2020-XNUCA babyv8
做的第一道存在指针压缩机制的V8题,通过小越界写修改length构造大越界读写,然后利用arraybuffer的backing store构造任意地址写,利用wasm rwx段地址的特点以及堆空间的分布,搜索到rwx段的具体地址,然后利用任意地址写将…...
货物数据处理pandas版
1求和 from openpyxl import load_workbook import pandas as pddef print_hi(name):# Use a breakpoint in the code line below to debug your script.print(fHi, {name}) # Press CtrlF8 to toggle the breakpoint.# Press the green button in the gutter to run the scr…...
MC-30A (32.768 kHz用于汽车应用的晶体单元)
MC-30A 32.768 kHz用于汽车应用的晶体,车规晶振中的热销型号之一。该款石英晶体谐振器,可以在-40 to 85 C的温度内稳定工作,能满足起动振动的要求。同时满足AEC-Q200无源元件质量标准认证,满足汽车仪表系统的所有要求。 频率范围…...
TrustZone之其他设备及可信基础系统架构
一、其他设备 最后,我们将查看系统中的其他设备,如下图所示: 我们的示例TrustZone启用的系统包括一些尚未涵盖的设备,但我们需要这些设备来构建一个实际的系统。 • 一次性可编程存储器(OTP)或保险丝 这些是一旦写入就无法更改的存储器。与每个芯片上都包含相同…...
自由编程学习资源:free-programming-books
最近,我发现了一个在GitHub上备受欢迎的项目,它为程序员和编程爱好者提供了丰富、免费且高质量的学习资料,这就是"free-programming-books"。目前,这个项目在GitHub上已经有305k的star,显示出它在开发者社区…...
饥荒Mod 开发(十三):木牌传送
饥荒Mod 开发(十二):一键制作 饥荒Mod 开发(十四):制作屏幕弹窗 一键传送源码 饥荒的地图很大,跑地图太耗费时间和饥饿值,如果大部分时间都在跑图真的是很无聊,所以需要有一个能够传送的功能,不仅可以快速…...
Qt/C++音视频开发60-坐标拾取/按下鼠标获取矩形区域/转换到视频源真实坐标
一、前言 通过在通道画面上拾取鼠标按下的坐标,然后鼠标移动,直到松开,根据松开的坐标和按下的坐标,绘制一个矩形区域,作为热点或者需要电子放大的区域,拿到这个坐标区域,用途非常多࿰…...
Java实现订单超时未支付自动取消的8种方法总结
Java实现订单超时未支付自动取消的8种方法总结 定时轮询 数据库定时轮询方式,实现思路比较简单。启动一个定时任务,每隔一定时间扫描订单表,查询到超时订单就取消。优点:实现简单。缺点:轮询时间间隔不好确定&#x…...
android动态权限申请并展示权限使用说明
# ExplainPermissions 动态权限申请并展示权限使用说明 随着工信部对APP的一系列整治,现在用户对于APP在使用时动态申请的权限是比较敏感的,为了更好的用户体验,我们可以在权限动态申请的时候一并向用户展示所需要申请权限的使用说明。这样用…...
论文阅读《DPS-Net: Deep Polarimetric Stereo Depth Estimation》
论文地址:https://openaccess.thecvf.com/content/ICCV2023/html/Tian_DPS-Net_Deep_Polarimetric_Stereo_Depth_Estimation_ICCV_2023_paper.html 概述 立体匹配模型难以处理无纹理场景的匹配,现有的方法通常假设物体表面是光滑的,或者光照是…...
docker文档转译1
写在最前面 本文主要是转译docker官方文档。主题是Docker overview,这里是链接 Docker概述 Docker是一个用于开发、发布和运行应用程序的开放平台。Docker使你能够将应用程序与基础设施分离,从而可以快速交付软件。你可以使用相同的方法像管理应用程序…...
UE4 图片环形轮播 蓝图
【需求】 图片环形轮播 任意图片之间相互切换 切换图片所需时间均为1s 两个图片之间切换使用就近原则 播放丝滑无闪跳 【Actor的组成】 每个图片的轴心都在原点 【蓝图节点】...
饥荒Mod 开发(十):制作一把AOE武器
饥荒Mod 开发(九):物品栏排列 饥荒Mod 开发(十一):修改物品堆叠 前面的文章介绍了很多基础知识以及如何制作一个物品,这次制作一把武器,装备之后可以用来攻击怪物。 制作武器贴图和动画 1.1 制作贴图。 先准备一张武器的贴图&a…...
微服务实战系列之ZooKeeper(下)
前言 通过前序两篇关于ZooKeeper的介绍和总结,我们可以大致理解了它是什么,它有哪些重要组成部分。 今天,博主特别介绍一下ZooKeeper的一个核心应用场景:分布式锁。 应用ZooKeeper Q:什么是分布式锁 首先了解一下&…...
FFmpeg项目的组成
主要由三个部分组成: 工具 ffmpeg:用于音视频转码、转换ffplay:音视频播放器ffserver:流媒体服务器ffprobe:多媒体码流分析器 SDK 这个部分是供开发者使用的SDK,SDK是编译好的库。基本上每个平台都有对…...
计算机网络:数据链路层(广域网、PPP协议、HDLC协议)
今天又学会了一个知识,加油! 目录 一、广域网 二、PPP协议 1、PPP协议应满足的要求 2、PPP协议无需满足的要求 3、PPP协议的三个组成部分 4、PPP协议的状态图 5、PPP协议的帧格式 三、HDLC协议 1、HDLC的站(主站、从站、复合站&…...
Spring Boot i18n中文文档
本文为官方文档直译版本。原文链接 Spring Boot 支持本地化消息,因此您的应用程序可以满足不同语言偏好的用户。默认情况下,Spring Boot 会在类路径的根目录下查找是否存在消息资源包。 自动配置适用于已配置资源包的默认属性文件(默认为 mes…...
持久化存储 StorageClass
kubernetes从v1.4版本开始引入了一个新的资源对象StorageClass,用于标记存储资源的特性和性能。到v1.6版本时,StorageClass和动态资源供应的机制得到了完善,实现了存储卷的按需创建,在共享存储的自动化管理进程中能够实现了重要的…...
uni-app点击预览图片
<image :src"info.shopLogoUrl" tap"_previewImage(info.shopLogoUrl)" mode"widthFix" >_previewImage(image) {var imgArr [];imgArr.push(image);//预览图片uni.previewImage({urls: imgArr,current: imgArr[0]});},大佬地址...
LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明
LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造,完美适配AGV和无人叉车。同时,集成以太网与语音合成技术,为各类高级系统(如MES、调度系统、库位管理、立库等)提供高效便捷的语音交互体验。 L…...
Docker 离线安装指南
参考文章 1、确认操作系统类型及内核版本 Docker依赖于Linux内核的一些特性,不同版本的Docker对内核版本有不同要求。例如,Docker 17.06及之后的版本通常需要Linux内核3.10及以上版本,Docker17.09及更高版本对应Linux内核4.9.x及更高版本。…...
RocketMQ延迟消息机制
两种延迟消息 RocketMQ中提供了两种延迟消息机制 指定固定的延迟级别 通过在Message中设定一个MessageDelayLevel参数,对应18个预设的延迟级别指定时间点的延迟级别 通过在Message中设定一个DeliverTimeMS指定一个Long类型表示的具体时间点。到了时间点后…...
Leetcode 3576. Transform Array to All Equal Elements
Leetcode 3576. Transform Array to All Equal Elements 1. 解题思路2. 代码实现 题目链接:3576. Transform Array to All Equal Elements 1. 解题思路 这一题思路上就是分别考察一下是否能将其转化为全1或者全-1数组即可。 至于每一种情况是否可以达到…...
阿里云ACP云计算备考笔记 (5)——弹性伸缩
目录 第一章 概述 第二章 弹性伸缩简介 1、弹性伸缩 2、垂直伸缩 3、优势 4、应用场景 ① 无规律的业务量波动 ② 有规律的业务量波动 ③ 无明显业务量波动 ④ 混合型业务 ⑤ 消息通知 ⑥ 生命周期挂钩 ⑦ 自定义方式 ⑧ 滚的升级 5、使用限制 第三章 主要定义 …...
【ROS】Nav2源码之nav2_behavior_tree-行为树节点列表
1、行为树节点分类 在 Nav2(Navigation2)的行为树框架中,行为树节点插件按照功能分为 Action(动作节点)、Condition(条件节点)、Control(控制节点) 和 Decorator(装饰节点) 四类。 1.1 动作节点 Action 执行具体的机器人操作或任务,直接与硬件、传感器或外部系统…...
微服务商城-商品微服务
数据表 CREATE TABLE product (id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 商品id,cateid smallint(6) UNSIGNED NOT NULL DEFAULT 0 COMMENT 类别Id,name varchar(100) NOT NULL DEFAULT COMMENT 商品名称,subtitle varchar(200) NOT NULL DEFAULT COMMENT 商…...
九天毕昇深度学习平台 | 如何安装库?
pip install 库名 -i https://pypi.tuna.tsinghua.edu.cn/simple --user 举个例子: 报错 ModuleNotFoundError: No module named torch 那么我需要安装 torch pip install torch -i https://pypi.tuna.tsinghua.edu.cn/simple --user pip install 库名&#x…...
08. C#入门系列【类的基本概念】:开启编程世界的奇妙冒险
C#入门系列【类的基本概念】:开启编程世界的奇妙冒险 嘿,各位编程小白探险家!欢迎来到 C# 的奇幻大陆!今天咱们要深入探索这片大陆上至关重要的 “建筑”—— 类!别害怕,跟着我,保准让你轻松搞…...
Kafka主题运维全指南:从基础配置到故障处理
#作者:张桐瑞 文章目录 主题日常管理1. 修改主题分区。2. 修改主题级别参数。3. 变更副本数。4. 修改主题限速。5.主题分区迁移。6. 常见主题错误处理常见错误1:主题删除失败。常见错误2:__consumer_offsets占用太多的磁盘。 主题日常管理 …...
