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

news2024/11/24 8:01:46

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

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

文章目录

  • 定义
  • 图纸
  • 一个例子:如何模拟一个转笔刀
    • 自动转笔刀
          • 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 状态类显然是可以复用的

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


状态对象

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




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

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

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

相关文章

雪亮工程视频联网综合管理/视频智能分析系统建设方案(一)

一、行业背景 雪亮工程主要是针对农村地区治安防控的监控项目&#xff0c;在乡村的主干道、路口、人群聚集地部署高清摄像头&#xff0c;通过三级综治中心和指挥平台&#xff0c;将视频图像信息系统纵向下延至县、乡、村&#xff0c;同时利用系统拓展在安防、社会治理、智慧交…

细胞世界:4.细胞分化(划区域)与细胞衰老(设施磨损)

(1)细胞凋亡 1. 概念&#xff1a;细胞凋亡可以比作城市的规划者主动拆除某些建筑来更新城市或防止危险建筑对市民的潜在伤害。这是一个有序的过程&#xff0c;由城市&#xff08;细胞内部&#xff09;的特定规划&#xff08;基因&#xff09;所决定。 2. 特征&#xff1a;细…

LeetCode-1143. 最长公共子序列【字符串 动态规划】

LeetCode-1143. 最长公共子序列【字符串 动态规划】 题目描述&#xff1a;解题思路一&#xff1a;动规五部曲解题思路二&#xff1a;1维DP解题思路三&#xff1a;0 题目描述&#xff1a; 给定两个字符串 text1 和 text2&#xff0c;返回这两个字符串的最长 公共子序列 的长度。…

【攻防世界】Confusion1

php的标志是大象&#xff0c;Python的标志是蛇 。Python 的 Flask 框架( Flask 使用 Jinja2 作为模板引擎 ) 点进register.php 输入{{3*4}} 输入 {{config}} 也有回显&#xff0c;ssti 判断是否存在ssti注入&#xff1a; 1. {{8*8}} 2. {{config}} 过滤了关键字&#xff0…

详解Qt添加外部库

在Qt项目中添加外部库是一项常见任务&#xff0c;无论是静态库还是动态库都需要正确的配置才能让项目顺利编译链接。以下是详细步骤和不同场景下的配置方法&#xff1a; 方法一&#xff1a;手动编辑.pro文件 添加头文件路径&#xff1a; 在Qt项目中的.pro文件中使用INCLUDEPAT…

在线视频教育平台|基于Springboot的在线视频教育平台系统设计与实现(源码+数据库+文档)

在线视频教育平台目录 基于Springboot的在线视频教育平台系统设计与实现 一、前言 二、系统设计 三、系统功能设计 1、前台&#xff1a; 2、后台 用户功能模块 教师功能模块 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&a…

Kingbase(人大金仓数据库)(总结全网精华,虚拟机:从安装到操作数据库一条龙)

前言&#xff1a; 前一阵子一直在捣鼓人大金仓数据库&#xff0c;虽然国产化的数据库很值得夸赞&#xff0c;但是网上的资料确实少的可怜。特此记录一下我在学习这个数据库的心酸历程。 安装就看这个大哥的&#xff0c;我之前安装就是看的他的&#xff0c;非常靠谱。 linux安装…

海外代理IP如何助力YouTube广告投放?

一、海外代理的角色与优势 拓展地理访问&#xff1a; 海外代理允许您从其他国家或地区的IP地址进行网络访问。通过使用海外代理&#xff0c;您可以绕过部分限制&#xff0c;实现访问YouTube和其他平台的目的。扩展受众&#xff1a; 利用海外代理&#xff0c;您可以将广告投放面…

每日一题---OJ题: 相交链表

片头 嗨! 小伙伴们,大家好! 今天我们来一起学习这道OJ题---相交链表,准备好了吗? Ready Go! ! ! emmm,看这道题好像不怎么难,我们一起画图分析分析 上图中,A链表有5个结点,分别为 a1,a2,c1,c2,c3 ; B链表有6个结点,分别为 b1,b2,b3,c1,c2,c3 ; A链表和B链表在c1结点相交 …

【Python】数学函数和特殊内置函数大揭秘:Python中隐藏的秘密武器

欢迎来CILMY23的博客 本篇主题为 数学函数和特殊内置函数大揭秘&#xff1a;Python中隐藏的秘密武器 个人主页&#xff1a;CILMY23-CSDN博客 个人专栏系列&#xff1a; Python | C语言 | 数据结构与算法 | C 感谢观看&#xff0c;支持的可以给个一键三连&#xff0c;点赞关…

Partisia Blockchain:被严重低估的隐私区块链生态

在今年 3 月&#xff0c;隐私公链 Partisia Blockchain 迎来了重要的进展&#xff0c;该生态通证 $MPC 上线了交易所&#xff0c;目前 $MPC 通证可以在 Kucoin、Gate、BitMart、Bitfinex、Bitture 等平台交易&#xff0c;并将在不久后上线 MEXC 平台。 在上个月上线市场至今&am…

【Linux】进程间通信——system V版本 共享内存

目录 共享内存 原理 实践 shmget() 创建共享内存 shmctl() 删除共享内存 shmat() 挂接进程和共享内存 shmt() 进程和共享内存去关联 共享内存的特性 优势 劣势 用共享内存实现进程间通信 共享内存 原理 两个进程的PCB各自维护着一个进程地址空间。当两个进…

VsCode 安装Jupyter Notebook

VsCode 安装Jupyter Notebook 安装 1、打开 VSCode 编辑器&#xff0c;点击界面左端的【扩展】栏&#xff1b; 2、在【搜索框】中输入python&#xff0c;点击第一个Python&#xff0c;检查是否已经安装 python 插件&#xff0c;没安装的点击安装&#xff1b;已安装的继续第3步…

32单片机入门持续更新中

配套资料为野火霸道V2 初识 STM32 4.1 什么是 STM32 STM32&#xff0c;从字面上来理解&#xff0c;ST 是意法半导体&#xff0c;M 是 Microelectronics 的缩写&#xff0c;32 表示 32 位&#xff0c;合起 来理解&#xff0c;STM32 就是指 ST 公司开发的 32 位微控制器。在如今…

C++数据结构与算法——动态规划子序列系列

C第二阶段——数据结构和算法&#xff0c;之前学过一点点数据结构&#xff0c;当时是基于Python来学习的&#xff0c;现在基于C查漏补缺&#xff0c;尤其是树的部分。这一部分计划一个月 (2024.1.30-2024.4.10已完结&#xff09;&#xff0c;任务量确实有点大&#xff0c;包括春…

Mysql主从同步原理

master每次提交事务时都会将数据变更记录到二进制文件BINLOG中&#xff0c;binlog文件主要记录除查询以外的DDL和DML&#xff0c;slave的IO线程读取master的BINLOG文件&#xff0c;并写入中继日志Relay log文件中&#xff0c;然后slave的SQL线程读取relay log文件并重做事务映射…

核心api实操-Activiti7从入门到专家(5)

背景 上一节已经搭建了&#xff0c;具体的开发环境&#xff0c;数据库&#xff0c;并且找了一个可以用bpmnjs流程设计器&#xff0c;这一些&#xff0c;我们对核心api做个基础的实操&#xff0c;有个感性的认知&#xff0c;另外对数据库和基本数据流动有个理解。 部署 模板部…

Deblurring 3D Gaussian Splatting去模糊3D高斯溅射

Abstract 摘要 Recent studies in Radiance Fields have paved the robust way for novel view synthesis with their photorealistic rendering quality. Nevertheless, they usually employ neural networks and volumetric rendering, which are costly to train and impede…

Web中使用Weblogic用户

WebLogic用户&#xff0c;组设置 1. 登录weblogic console, domain结构中选择Security Realms&#xff0c;显示安装时默认创建的Realm &#xff1a; myrealm 2. 点击myrealm, 选择 users and Group&#xff0c; 追加用户和组 选择既存的权限组追加到新规的组中&#xff0c;赋予…

【jumpserver-02】离线安装jumpserver

https://docs.jumpserver.org/zh/v3/installation/setup_linux_standalone/offline_install/#1 环境准备 mysql数据库 create database jumpserver default character set utf8mb4;create user jumpserver% identified by jumpserver;grant all on jumpserver.* to jumpserver…