设计模式——2_7 状态(State)

欲买桂花同载酒,终不似,少年游

——刘过《唐多令·芦叶满汀州》

文章目录

  • 定义
  • 图纸
  • 一个例子:如何模拟一个转笔刀
    • 自动转笔刀
          • Pencil
          • PencilSharpener
    • 投诉和改善
      • 钝刀
          • Blade
          • PencilSharpener
      • 没有铅笔
          • PencilSharpener
    • if if if
          • State
          • PencilSharpener
  • 碎碎念
    • 状态和if
    • 状态和可复用性
      • 状态类的复用
      • 状态对象

定义

允许一个对象在其内部状态改变时改变它的行为,让这个对象看起来似乎修改了她的类

简单来说,就是在你修改一个对象的状态前后调用同一个方法,这个对象会出现完全不同的行为,看起来就像换了一个类一样


那你会说,这跟if-else有什么区别?

还真就没什么区别。不只是没区别,状态模式还让我们的程序变得更复杂。对啊,那为什么要用状态模式呢?




图纸

在这里插入图片描述




一个例子:如何模拟一个转笔刀

道友,你应该用过那种铅笔转笔刀吧?他的主要工作就是让我们插进去一根铅笔,然后用内部的刀片把铅笔削尖

他就是我们这次例子,准备好了吗,我们开始了:


自动转笔刀

假定我们要开发一款自动转笔刀:只要我们把铅笔戳进去,按动按钮,理论上来说这个转笔刀就会自动帮我们削铅笔并在削好后把铅笔弹出来,为了用代码来操控这个转笔刀,我们实现了如下结构:

在这里插入图片描述

Pencil
/*** 铅笔*/
public class Pencil {/*** 锋利度,默认没有削的铅笔就是0*/private int sharpness = 0;/*** 是锋利的吗* <p>* >=10 视为锋利的*/public boolean isSharp() {return sharpness >= 10;}/*** 增加锋利度*/public void addSharpness(int sharp) {sharpness += sharp;}
}
PencilSharpener
/*** 铅笔转笔刀*/
public class PencilSharpener {/*** 当前正在削的铅笔*/private Pencil pencil;/*** 弹出铅笔*/public void popup() {this.pencil = null;System.out.println("弹出铅笔");}/*** 旋转一圈,增加铅笔3点的锋利值*/public void rotate() {if (pencil != null) {pencil.addSharpness(3);}}/*** 削铅笔*/public void sharpen() {while (pencil != null && !pencil.isSharp()) {//如果有铅笔,而且铅笔不锋利rotate();//转圈削}//弹出铅笔popup();}/*** 插入铅笔*/public void setPencil(Pencil pencil) {this.pencil = pencil;}
}

转笔刀,首先得有铅笔,所以我们创建了一个 Pencil(铅笔) ,在 Pencil 里面,我们通过一个整型值 sharpness(锋利度) 来表示铅笔的锋利度,并规定当 sharpness < 10的时候,这跟铅笔就是不锋利的,应该被削

我们创建了 PencilSharpener(铅笔转笔刀) 用来表示一个转笔刀,并赋予他 popup 弹出铅笔 和 rotate 削一圈的方法,最终把他们都集成到 sharpen 削铅笔方法里


这个代码简单直接,一看就是我喜欢的风格,这玩意他耿直你晓得吧

这段代码也一直恪尽职守,直到某天有用户投诉这个自动转笔刀转起来没完没了以至于一个月偷跑了他几百块电费


投诉和改善

钝刀

经过排查,我们发现原因出在转笔刀的刀片上。因为转笔刀在削铅笔的时候他内部刀片的锋利度也在不断的被损耗,直到刀钝到无法再增加铅笔的锋利度。于是在 sharpen 方法内出现一个死循环,转笔刀不断的调用 rotate 可是铅笔的锋利度并不改变

也就是说,我们需要增加对刀片状态的监控,就像这样:

在这里插入图片描述

Blade
/*** 刀片*/
public class Blade {/*** 锋利度,默认默认刀的锋利值是10*/private int sharpness = 10;/*** 是锋利的吗* <p>* >=5 视为锋利的*/public boolean isSharp() {return sharpness >= 5;}/*** 减少锋利度*/public void lessenSharpness(int sharp) {sharpness -= sharp;}
}
PencilSharpener
/*** 铅笔转笔刀*/
public class PencilSharpener {……/*** 刀片*/private Blade blade = new Blade();/*** 旋转一圈,增加铅笔3点的锋利值,减少刀子1点锋利值*/public void rotate() {if (pencil != null) {pencil.addSharpness(3);blade.lessenSharpness(1);}}/*** 削铅笔*/public void sharpen() {while (pencil != null && !pencil.isSharp()) {if (blade.isSharp()) {//如果刀子锋利//如果有铅笔,而且铅笔不锋利rotate();//转圈削} else {System.out.println("刀子不锋利了,请换刀");break;}}//弹出铅笔popup();}
}

我们新增了 Blade 刀片类,并且用和铅笔类似的方式管理他的锋利度(理论来说无法连续削两支铅笔),现在他又正常了


没有铅笔

可是几天后,新的投诉来了,客户说为什么在没有插入铅笔的情况下,依然会在点击削铅笔按钮的时候触发弹出铅笔的动作

接着改,这次要改这里:

PencilSharpener
……public void sharpen() {if(pencil != null){while (!pencil.isSharp()) {if (blade.isSharp()) {//如果刀子锋利//如果有铅笔,而且铅笔不锋利rotate();//转圈削} else {System.out.println("刀子不锋利了,请换刀");break;}}//弹出铅笔popup();}}……

我们把对铅笔的判断提到最外层,让他包含popup,这样以解决用户的投诉


可是改到这里你发现一个可怕的事实,我们终于把所有的行为都包含在if-else里面了


if if if

为什么我们会举步维艰?所有的行为在执行前我们都要谨慎的判断当前外界的状态,走错一步就会报错崩溃。有没有一种方法,可以让算法抽象出来,统一某一种状态下的所有行为?


答案当然是肯定的,先分析一下一共可能出现多少种状态,就像这样:

状态调用sharpen后的动作
没有插入铅笔不做操作
插入铅笔但刀片不锋利弹出铅笔
插入铅笔且刀片锋利削铅笔,削完弹出
插入铅笔但铅笔足够锋利弹出铅笔

接着我们把他们做成类簇,就像这样:

在这里插入图片描述

State
/*** 转笔刀状态*/
public abstract class PencilSharpenerState {protected final PencilSharpener pencilSharpener;public PencilSharpenerState(PencilSharpener pencilSharpener) {this.pencilSharpener = pencilSharpener;}public abstract void handle();
}/*** 无铅笔状态*/
public class NoPencil extends PencilSharpenerState{public NoPencil(PencilSharpener pencilSharpener) {super(pencilSharpener);}@Overridepublic void handle() {//什么都不做System.out.println("没有铅笔");}
}/*** 钝刀状态*/
public class Dull extends PencilSharpenerState{public Dull(PencilSharpener pencilSharpener) {super(pencilSharpener);}@Overridepublic void handle() {System.out.println("刀子钝了,没法削了");pencilSharpener.popup();//弹出铅笔}
}/*** 正常状态*/
public class Normal extends PencilSharpenerState {public Normal(PencilSharpener pencilSharpener) {super(pencilSharpener);}@Overridepublic void handle() {pencilSharpener.rotate();//转一圈}
}/*** 完成状态*/
public class Finish extends PencilSharpenerState {public Finish(PencilSharpener pencilSharpener) {super(pencilSharpener);}@Overridepublic void handle() {//弹出铅笔,结束pencilSharpener.popup();}
}

我们通过 PencilSharpenerState 这个状态类簇,把原先应该被放在 rotate 方法里面的那些动作都独立了出来,那现在我们要把她们绑定到 PencilSharpener 上,就像这样:

PencilSharpener
/*** 铅笔转笔刀*/
public class PencilSharpener {private final PencilSharpenerState noPencil = new NoPencil(this);//无铅笔状态private final PencilSharpenerState dull = new Dull(this);//钝刀状态private final PencilSharpenerState normal = new Normal(this);//正常状态private final PencilSharpenerState finish = new Finish(this);//完成状态private PencilSharpenerState current = noPencil;//当前状态 默认为无铅笔/*** 刀片*/private Blade blade = new Blade();/*** 当前正在削的铅笔*/private Pencil pencil;/*** 弹出铅笔*/public void popup() {this.pencil = null;System.out.println("弹出铅笔");//弹出铅笔时状态变为无铅笔current = noPencil;}/*** 旋转一圈*/public void rotate() {pencil.addSharpness(3);//增加铅笔锋利度blade.lessenSharpness(1);//减少铅笔锋利度updateState();//更新状态current.handle();//接着执行}/*** 削铅笔*/public void sharpen() {current.handle();//调用状态对象里面的执行方法}/*** 插入铅笔*/public void setPencil(Pencil pencil) {this.pencil = pencil;//插入铅笔的时候状态根据刀片的状态进行切换updateState();}/*** 更新状态*/private void updateState() {//优先判断铅笔的状态if (pencil == null) {current = noPencil;//无铅笔} else if (pencil.isSharp()) {//铅笔足够锋利了current = finish;//完成状态} else if (blade.isSharp()) {//刀片足够锋利current = normal;//正常状态} else {current = dull;//钝刀状态}}
}

PencilSharpener 的开头,我们就定义出所有可能出现的状态所对应的算法对象,并和this建立了绑定。这让 PencilSharpener 的方法显得相当清爽,因为每种状态里需要做的事情都被单独分割出来了

但是我们总要去判断当前的状态并对其做出反应的,于是我们定义了 updateState 方法,这个方法在每个切换状态后需要判断时被调用,以确定当前正确的状态对象


而这正是一个标准的状态模式实现


最后,再给他补个main方法看看效果:

public static void main(String[] args) {PencilSharpener pencilSharpener = new PencilSharpener();for (int i = 1; i <= 3; i++) {System.out.printf("第%s根铅笔:\n", i);Pencil pencil = new Pencil();System.out.printf("铅笔锋利度为:%s\n", pencil.getSharpness());pencilSharpener.setPencil(pencil);pencilSharpener.sharpen();System.out.printf("铅笔锋利度为:%s\n", pencil.getSharpness());System.out.println("*****************");}pencilSharpener.sharpen();
}

我们特地建了三根铅笔让转笔刀去削并在最后不插入铅笔再让他执行一次,理论上来说四次调用 pencilSharpener.sharpen(); 的结果分别会是:

  1. 正常削完
  2. 削到一半弹出
  3. 完全没削弹出
  4. 不执行

就像这样:

在这里插入图片描述




碎碎念

状态和if

请回顾一下本文最开始的问题,为什么我们要用状态模式呢?

答:状态模式让整个程序的结构变得更清晰,而且打断了算法和算法间的耦合。在使用状态模式之前,你的算法之间的关联都是写死的,当出现新的状态时,你只能增加新的if-else块,而且他们未必都是同级的,你需要去协调他们。

状态模式提供了另一种解题思路,他让你把if-else块里面做的事情抽象成一个对象。那么算法的调用对象,和被你抽象出来的算法对象之间就可以用组合了,我可以根据不同的状态,把不同的算法对象组合到算法调用对象内部。这样一来我不需要再去整理if-else,而是要确认对象此刻的状态。如果将来有新的状态出现,那也是新增算法对象的事情,至此实现解耦


那我们是不是可以说。所以程序里要减少if-else,无所不用其极的去减少这种结构出现的次数。就像上例,似乎都是在强调如何减少程序里面出现的各种条件判断。我们的敌人就是if-else,他是导致程序变得复杂的根本原因


上面那段话可以翻译成这样:

因为张三被枪打死了,这把枪导致了张三的死亡,所以我们判这把枪死刑

if-else就相当于这把枪,虽然在上例中的确是大量if-else直接导致结构的混乱,可他绝不是根本原因,别把他当敌人。该被审判的是那个扣动扳机的人


那是谁开的枪?

答:从第一篇设计模式开始,开枪的人从来没有变过,就是那些散落各处的“变化”。

在上例中,我们把所有“变化”集中到一处,使整个结构变得清晰。通过解读 updateState 任何人都能对转笔刀接下来会做的行为一目了然,而这正是我们在上例使用状态模式之前的实现里无法做到的,也这正是状态模式的魅力所在

这也符合我们的设计原则:找出应用中可能需要变化的地方,把他们独立出来,不要和那些不需要变化的代码混合在一起


回到上例

请注意上例中的 updateState 方法,我们在根据 PencilSharpener 当前的状态并切换对应的状态对象的,所以不可避免的出现了大量的if-else,而这恰巧是整个转笔刀正常运转的关键。

也就是说哪怕是在上例中 if-else 也不是被消灭了,而是被整合了



状态和可复用性

状态类的复用

状态类是可以被复用的,这一点在上例中没有体现出来

简单来说,比如我有多种转笔刀,可能他们削铅笔的方式不一样,有些用刀片,有些用滚轴,哪怕使用激光去削铅笔,在 没有插入铅笔的情况 下他们要做的事情都是一样的,也就是说上例中的 NoPencil 状态类显然是可以复用的

说到底,这是因为状态类是对算法的抽象,所以只有有算法相同的部分,都可以复用


状态对象

不只是状态类,状态对象也是可以复用的。虽然状态类叫状态类,但是状态类大都是没有自身状态的,他有价值的只是里面的方法,这样一来状态对象就是可复用的。所以状态对象常常的单例的




万分感谢您看完这篇文章,如果您喜欢这篇文章,欢迎点赞、收藏。还可以通过专栏,查看更多与【设计模式】有关的内容

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

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

相关文章

48-基于腾讯云EKS的容器化部署实战

准备工作 在部署IAM应用之前&#xff0c;我们需要做以下准备工作&#xff1a; 开通腾讯云容器服务镜像仓库。安装并配置Docker。准备一个Kubernetes集群。 开通腾讯云容器服务镜像仓库 在Kubernetes集群中部署IAM应用&#xff0c;需要从镜像仓库下载指定的IAM镜像&#xff…

亚马逊、速卖通、lazada测评自养号与机刷有何区别?

在亚马逊平台&#xff0c;买家评价的重要性无需多言。许多消费者在决定购买产品前&#xff0c;都会习惯性地查看相关评论&#xff0c;对比同类产品的买家反馈&#xff0c;从而做出更明智的选择。正因如此&#xff0c;测评成为各大电商平台不可或缺的一种推广策略&#xff0c;亚…

C++ 模拟实现 STL 中的 set、map 与 multiset、multimap

目录 一&#xff0c;RB_tree 的实现 1&#xff0c;RB_tree 的节点与数据结构 2&#xff0c;RB_tree 的迭代器 3&#xff0c;RB_tree 的构造 4&#xff0c;RB_tree 的元素操作 5&#xff0c;完整代码 二&#xff0c;set 与 multiset 的实现 1&#xff0c;set 2&#x…

从零开始:Elasticsearch简介与详解

大家好,我是小米,今天我来和大家聊一聊阿里巴巴面试题中常见的一个话题:Elasticsearch。作为一名喜欢分享技术的小伙伴,我深知在技术的道路上,多一份了解就多一份优势。那么,让我们一起来探索一下Elasticsearch的特点、功能、场景以及与竞品的对比分析吧! 特点 特点是…

js爬虫puppeteer库 解决网页动态渲染无法爬取

我们爬取这个网址上面的股票实时部分宇通客车(600066)_股票价格_行情_走势图—东方财富网 我们用正常的方法爬取会发现爬取不下来&#xff0c;是因为这个网页这里是实时渲染的&#xff0c;我们直接通过网址接口访问这里还没有渲染出来 于是我们可以通过下面的代码来进行爬取: …

1. VirtualBox安装CentOS

安装 VirtualBox 地址:https://www.virtualbox.org/wiki/Downloads 版本: 6.1和7.0+版本都可以 安装: windows上安装需要admin权限,右键菜单选中 “Run as administrator” 安装 CentOS 6.10 地址:https://vault.centos.org/6.10/isos/x86_64/ 版本: 如果不需要GUI,选择…

混合云构建-如何通过Site to Site VPN 连接 AWS 和GCP云并建立一个高可用的VPN通信

如果我们的业务环境既有AWS云又有GCP云,那么就需要将他们打通,最经济便捷的方式就是通过Site-to-Site VPN连接AWS和GCP云,你需要在两个云平台上分别配置VPN网关,并建立一个VPN隧道来安全地连接这两个环境,我们下面演示一个高可用场景下的S2S VPN线路构建,采用动态BGP协议…

利用dbschema工具导出数据库结构

dbschema是SinoDB数据库的一个命令行工具&#xff0c;可以用来导出SinoDB数据库的所有对象&#xff08;如表、触发器、视图等&#xff09;的元数据。以下是常见的使用方法&#xff1a; 1、导出数据库中所有的表结构到文件db.sql $dbschema -d your_database_name -t all db.sq…

岛屿个数c++

参考文章 岛屿个数1岛屿个数2 题目 输入样例&#xff1a; 2 5 5 01111 11001 10101 10001 11111 5 6 111111 100001 010101 100001 111111输出样例&#xff1a; 1 3样例解释 对于第一组数据&#xff0c;包含两个岛屿&#xff0c;下面用不同的数字进行了区分&#xff1a; 0…

Torch not compiled with CUDA enabled问题解决过程记录

1. 背景 运行大模型的时候&#xff0c;出现错误:Torch not compiled with CUDA enabled 原因&#xff1a;并不是电脑安装了nvdia显卡驱动就可以的&#xff0c;还需要安装 NVDIA GPU Computing Toolkit&#xff08;即CUDA Toolkit&#xff09;cudnn 另外还需要确保安装的pyt…

ROS2 采集虚拟仿真环境图像并发布

简介&#xff1a;ROS2功能的学习我们还是在基于OpenAI的gym虚拟仿真环境中来完成&#xff0c;gym虚拟仿真环境安装请参考另一篇教程&#xff0c;这里不再重复说明&#xff0c;接下来我们开始创建一个ROS2的功能节点&#xff0c;并发布虚拟仿真环境小车摄像头的图像&#xff0c;…

day02 VS Code开发单片机

VS Code开发单片机 1.1 安装 MinGW-w64 1)MinGW-w64介绍 VS Code 用于编辑 C 代码,我们还需要 C 编译器来运行 C 代码,所以安装 VS Code之前我们需要先安装 C 编译器。这里我们使用 MinGW-w64(Minimalist GNU for Windows 64-bit)。 MinGW-w64 是一个用于Windows操作系…

SpringBoot和Vue2项目配置https协议

1、SpringBoot项目 ① 去你自己的云申请并下载好相关文件&#xff0c;SpringBoot下载的是Tomcat&#xff08;默认&#xff09;&#xff0c;Vue2下载的是Nginx ② 将下载的压缩包里面的.pfx后缀文件拷贝到项目的resources目录下 ③ 编辑配置文件 &#xff08;主要是框里面的内…

解决苹果iMac的M1芯片Node Sass does not yet support your current environment的问题

问题背景 如图所示&#xff0c;这是我的电脑&#xff0c;M1芯片 启动前端项目老是报错&#xff0c;说node Sass不支持我当前的环境&#xff0c;同事的macBook是intel芯片的&#xff0c;就能跑起项目来 很烦 但是不慌&#xff01;&#xff01;&#xff01; 咱有解决方法啦&a…

云原生__K8S

createrepo --update /var/localrepo/# 禁用 firewall 和 swap [rootmaster ~]# sed /swap/d -i /etc/fstab [rootmaster ~]# swapoff -a [rootmaster ~]# dnf remove -y firewalld-*[rootmaster ~]# vim /etc/hosts 192.168.1.30 harbor 192.168.1.50 master 192.168.1.…

数字货币:金融创新的未来?

随着科技的进步&#xff0c;数字货币作为一种新型的金融工具正逐渐走进人们的视线。那么&#xff0c;数字货币究竟是什么&#xff1f;它有哪些优势&#xff1f;它是否真的能够引领金融创新的未来&#xff1f;本文将从专业角度出发&#xff0c;深入探讨这些问题。 一、数字货币的…

iOS 启动速度优化

启动耗时&#xff1a;点击App后到首帧显示耗费的时间。 阶段分析&#xff1a;premain、postmain&#xff0c;也就是main函数执行前和main函数执行后。 耗时检测&#xff1a;Instrument->App Launch Premain 减少动态库数量&#xff1a;启动时程序会加载动态库&#xff0c;…

一分钟了解机器人自由度

目录 自由度的定义 自由度的分类 自由度的影响 影响自由度的主要参数 关节类型和数量 机械结构 控制系统 自由度控制的硬件架构原理 传感器 执行器 控制器 通信接口 软件和算法 机器人的自由度是指机器人在空间中可以独立移动的方向和角度的数量&#xff0c;它是衡…

html--虎鲸

<!DOCTYPE html> <html> <head> <meta charset"UTF-8"> <title>虎鲸</title> <link rel"stylesheet" href"css/normalize.css"> <link rel"stylesheet" href"css/style.css" …

Linux C++ 029-STL之queue容器

Linux C 029-STL之queue容器 本节关键字&#xff1a;Linux、C、queue 相关库函数&#xff1a;push、pop、back、front queue基本概念 概念&#xff1a;queue是一种先进先出&#xff08;First In Fisrst Out&#xff0c;FIFO&#xff09;的数据结构&#xff0c;它有两个端口 关…