设计模式——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);
    }

    @Override
    public void handle() {
        //什么都不做
        System.out.println("没有铅笔");
    }
}

/**
 * 钝刀状态
 */
public class Dull extends PencilSharpenerState{

    public Dull(PencilSharpener pencilSharpener) {
        super(pencilSharpener);
    }

    @Override
    public void handle() {
        System.out.println("刀子钝了,没法削了");
        pencilSharpener.popup();//弹出铅笔
    }
}

/**
 * 正常状态
 */
public class Normal extends PencilSharpenerState {

    public Normal(PencilSharpener pencilSharpener) {
        super(pencilSharpener);
    }

    @Override
    public void handle() {
        pencilSharpener.rotate();//转一圈
    }
}

/**
 * 完成状态
 */
public class Finish extends PencilSharpenerState {

    public Finish(PencilSharpener pencilSharpener) {
        super(pencilSharpener);
    }

    @Override
    public 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 状态类显然是可以复用的

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


状态对象

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




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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

动态物体检测 | 复杂环境下多目标动态物体实时检测算法实现

项目应用场景 面向复杂环境下的多目标动态物体实时检测场景&#xff0c;项目采用一种在线体积映射的算法实现。 项目效果 项目细节 > 具体参见项目 README.md (1) 安装、编译工程&#xff0c;包括 ROS&#xff0c;具体步骤参见 README(2) 执行 DOALS Sequence roslaunch d…

Point cloud转 Laser sacn

文章目录 概要安装pointcloud_to_laserscan修改配置运行结果 概要 在ROS中将点云&#xff08;PointCloud&#xff09;转换为激光扫描&#xff08;LaserScan&#xff09;是一个常见的任务&#xff0c;尤其是在某些机器人系统中&#xff0c;激光雷达数据被用于导航和避障&#x…

Windows 2003 R2与Windows 2022建立域信任报错:本地安全机构无法跟域控制器获得RPC连接。请检查名称是否可以解析,服务器是否可用。

在Windows Server 2003 R2与Windows Server 2022之间建立域信任时遇到“本地安全机构无法与域控制器获得RPC连接”的错误&#xff0c;可能是由于以下几种原因&#xff1a; DNS 解析问题&#xff1a; 确保源域和目标域的DNS配置正确&#xff0c;能够互相解析对方的域名和IP地址。…

达梦数据库的V$DM_INI和V$PARAMETER系统视图

V$DM_INI和V$PARAMETER是达梦数据库中两个常用的系统视图&#xff0c;用于查看数据库的配置参数。这两个视图的主要区别在于它们展示参数的来源和用途。 V$DM_INI V$DM_INI视图主要用于展示数据库启动时加载的初始化参数信息。这些信息通常来自于数据库的初始化参数文件&…

【运输层】TCP 的可靠传输是如何实现的?

目录 1、发送和接收窗口&#xff08;滑动窗口&#xff09; &#xff08;1&#xff09;滑动窗口的工作流程 &#xff08;2&#xff09;滑动窗口和缓存的关系 &#xff08;3&#xff09;滑动窗口的注意事项 2、如何选择超时重传时间 &#xff08;1&#xff09;加权平均往返…

MemFire Cloud让静态托管动起来!

静态托管 我们最常接触到的静态托管是github pages&#xff0c;它的常见工作模式是在github上创建一个仓库&#xff0c;使用hexo类的工具初始化仓库&#xff0c;编写markdown文件&#xff0c;生成静态页面&#xff0c;推送到github上完成页面更新&#xff0c;比如https://blog…

ViSNet:用于分子性质预测和动力学模拟的通用分子结构建模网络

编者按&#xff1a;尽管几何深度学习已经彻底颠覆了分子建模领域&#xff0c;但最先进的算法在实际应用中仍然面临着几何信息利用不足和高昂计算成本的阻碍。为此&#xff0c;微软研究院科学智能中心&#xff08;Microsoft Research AI4Science&#xff09;的研究员们提出了通用…

OCR常用识别算法综述

参考&#xff1a;https://aistudio.baidu.com/education/lessonvideo/3279888 语种&#xff1a;常用字符36与常用汉字6623&#xff0c;区别。 标注&#xff1a;文本型位置/单字符位置&#xff0c;后者标注成本大 挑战&#xff1a;场景文字识别&#xff1a;字符大小、颜色、字体…

【经典算法】LCR187:破冰游戏(约瑟夫问题,Java/C/Python3/JavaScript实现含注释说明,Easy)

目录 题目思路及实现方式一&#xff1a;迭代模拟&#xff08;用链表模拟这个游戏&#xff09;思路代码实现Java版本C语言版本Python3版本 复杂度分析 方式二&#xff1a;数学迭代思路代码实现Java版本C语言版本Python3版本 复杂度分析 方式三&#xff1a;递归思路代码实现Java版…

C语言 函数——函数的定义、调用和参数传递

目录 模块化编程&#xff08;Modular Programming&#xff09; 函数的分类 函数的定义 使用函数编程的好处 函数调用的基本方式 函数调用时的数据传递 函数调用的过程 main函数的特殊性 大话三国 分而治之 如果将main&#xff08;&#xff09;函数比作诸葛亮&#xff…

并行超算云计算使用步骤完整流程详情

本文目录 一、将项目传入并运云。二、创建项目的虚拟环境三、编辑run.sh脚本四、提交作业五、查看作业输出六、查看提交的作业号七、结束作业 一、将项目传入并运云。 二、创建项目的虚拟环境 打开终端 使用conda创建&#xff1a;conda create -n 环境名 python3.8查看conda下…

消息队列MQ的介绍和docker安装MQ

一、什么是mq? MQ全称 Message Queue&#xff08;消息队列&#xff09;&#xff0c;是在消息的传输过程中保存消息的容器。多用于分布式系统之间进行通信&#xff0c;解耦。 二、常见的mq产品 RabbitMQ、RocketMQ、ActiveMQ、Kafka、ZeroMQ、MetaMq RabbitMQ: One broker …

LINUX 下IPTABLES配置详解

-t<表>&#xff1a;指定要操纵的表&#xff1b; -A&#xff1a;向规则链中添加条目&#xff1b; -D&#xff1a;从规则链中删除条目&#xff1b; -i&#xff1a;向规则链中插入条目&#xff1b; -R&#xff1a;替换规则链中的条目&#xff1b; -L&#xff1a;显示规则链中…

【算法详解】二分查找

1. 二分查找算法介绍 「二分查找算法&#xff08;Binary Search Algorithm&#xff09;」&#xff1a;也叫做 「折半查找算法」、「对数查找算法」。是一种在有序数组中查找某一特定元素的搜索算法。 基本算法思想&#xff1a;先确定待查找元素所在的区间范围&#xff0c;在逐步…

界面组件DevExpress WinForms v23.2 - 功能区、富文本编辑器功能升级

DevExpress WinForms拥有180组件和UI库&#xff0c;能为Windows Forms平台创建具有影响力的业务解决方案。DevExpress WinForms能完美构建流畅、美观且易于使用的应用程序&#xff0c;无论是Office风格的界面&#xff0c;还是分析处理大批量的业务数据&#xff0c;它都能轻松胜…

软考 系统架构设计师系列知识点之云原生架构设计理论与实践(16)

接前一篇文章&#xff1a;软考 系统架构设计师系列知识点之云原生架构设计理论与实践&#xff08;15&#xff09; 所属章节&#xff1a; 第14章. 云原生架构设计理论与实践 第3节 云原生架构相关技术 14.3.3 无服务器技术 1. 技术特点 2. 技术关注点 &#xff08;1&#xff…

四川一体化污水处理设备厂家如何挑选

如果您想寻找一家可靠的四川地区的污水处理设备厂家&#xff0c;以下是一些挑选的关键要素可以考虑&#xff1a; 1. 信誉和口碑&#xff1a;了解该厂家在业界的声誉和客户的评价&#xff0c;可以通过查阅相关的评论和建议&#xff0c;或者咨询其他业内人士来了解。 2. 技术实力…

数据生成 | Matlab实现基于SNN浅层神经网络的数据生成

数据生成 | Matlab实现基于SNN浅层神经网络的数据生成 目录 数据生成 | Matlab实现基于SNN浅层神经网络的数据生成生成效果基本描述模型描述程序设计参考资料 生成效果 基本描述 1.Matlab实现基于SNN浅层神经网络的数据生成&#xff0c;运行环境Matlab2021b及以上&#xff1b; …

【接口自动化】参数化替换

在做接口测试时&#xff0c;除了测单个接口&#xff0c;还需要进行业务链路间的接口测试 比如[注册-登陆]需要token鉴权的业务流 当我们用使用postman/jmeter等工具时&#xff0c;将注册接口的一些响应信息提取出来&#xff0c;放到登陆接口的请求中&#xff0c;来完成某个业务…

紫叶写作能用吗 #微信#知识分享

紫叶写作是一款非常好用、靠谱的论文写作工具&#xff0c;它旨在帮助用户快速高效地完成论文写作任务&#xff0c;并提供查重降重的功能。它不仅操作简单方便&#xff0c;而且功能强大&#xff0c;能够有效提高论文写作的效率和质量。 首先&#xff0c;紫叶写作提供了丰富的模板…