首页 > 其他分享 >【设计模式】状态模式State:通过有限状态机监控功能的 "状态变化"

【设计模式】状态模式State:通过有限状态机监控功能的 "状态变化"

时间:2023-09-13 19:01:47浏览次数:36  
标签:状态 ctx private 状态机 instance State 设计模式 public currentState

(目录)


状态模式的应用场景非常广泛,比如:

线上购物订单、手机支付、音乐播放器、游戏、工作流引擎等场景。

状态模式设计的初衷是应对同一个对象里不同状态变化时的不同行为的变化。


模式原理

原始定义是:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了自己的类一样。 这个定义确实有些抽象,简而言之,状态模式就是让一个对象通过定义一系列状态的变化来控制行为的变化。 比如,给购买的物品定义几个包裹运送状态,已下单、运送中、已签收等,当“已下单”状态变为“运送中”状态时,物流货车会把包装好的包裹运送到指定地址,也就是说,当包裹的状态发生改变时,就会触发相应的外部操作

状态模式的标准 UML 图: uTools_1658560150146.png

从这个 UML 图中,我们能看出状态模式包含的关键角色有三个。

  • 上下文信息类(Context):实际上就是存储当前状态的类,对外提供更新状态的操作。
  • 抽象状态类(State):可以是一个接口或抽象类,用于定义声明状态更新的操作方法有哪些。
  • 具体状态类(StateA 等):实现抽象状态类定义的方法,根据具体的场景来指定对应状态改变后的代码实现逻辑。

UML 对应代码实现:

package cn.happymaya.ndp.behavioral.state;

public class Context {

    private State currentState;

    public Context(State currentState) {
        this.currentState = currentState;
        if (null == currentState) {
            this.currentState = StateA.instance();
        }
    }

    public State getCurrentState() {
        return currentState;
    }

    public void setCurrentState(State currentState) {
        this.currentState = currentState;
    }

    public void request() {
        currentState.handle(this);
    }
}

public interface State {
    void handle(Context context);
}

public class StateA implements State{

    private static StateA instance = new StateA();

    private StateA() {}

    public static StateA instance() {
        return instance;
    }

    @Override
    public void handle(Context context) {
        System.out.println("=== 状态 A");
    }
}

public class StateB implements State{

    private static StateB instance = new StateB();

    private StateB() {}

    public static StateB instance() {
        return instance;
    }

    @Override
    public void handle(Context context) {
        System.out.println("=== 状态 B");
    }
}

在该代码实现中:

  • 状态 A 和状态 B 被封装为具体的状态类 StateA 和 StateB,均实现了同一个抽象状态类 State 接口;
  • 上下文信息类 Context 存储一个全局变量 currentState,用以保存当前状态对象,具体状态类通过将 Context 对象作为参数输入,就能获取访问全局的当前状态,以完成状态切换。

因此,状态模式设计的核心点在于找到合适的抽象状态以及状态之间的转移关系,通过改变状态来达到改变行为的目的


使用场景

一般来讲,状态模式常见使用场景有这样几种:

  1. 对象根据自身状态的变化来进行不同行为的操作时, 比如,购物订单状态;
  2. 对象需要根据自身变量的当前值改变行为,不期望使用大量 if-else 语句时, 比如,商品库存状态。
  3. 对于某些确定的状态和行为,不想使用重复代码时, 比如,某一个会员当天的购物浏览记录。

举一个现实中的例子,我们可以通过按电视遥控器上的按钮来改变电视的显示状态。如果电视处于打开状态,我们可以将其设置为关闭、静音或更换频道。但如果电视是关闭状态,当我们按下切换频道的按钮时是不会有任何作用的。因为这时对于关闭状态的电视来说,只有设置为打开状态才有效。

再比如,状态模式在程序中的另一个例子就是 Java 线程状态,一个线程的生命周期里有五种状态,只有在获得当前状态后才能确定下一个状态。

通过一个简单的例子来演示一下

在线上购物的过程中,当我们选定好了商品并提交订单后,装有商品的包裹就会开始进行运送。

这里定义了 6 种简单的包裹运送状态:已下单、仓库处理中、运输中、派送中、待取件和已签收。

如下图所示:

image.png

首先,定义包裹的状态 PackageState,在接口中声明一个更新状态的方法updateState(),该方法接收包裹上下文信息类 PackageContext 作为参数

package cn.happymaya.ndp.behavioral.state;
/**
 * 包裹状态
 */
public interface PackageState {
    /**
     * 定义了 6 中状态
     * 1 - 已下单
     * 2 - 仓库处理中
     * 3 - 运输中
     * 4 - 派送中
     * 5 - 待取件
     * 6 - 已签收
     */
    void updateState(PackageContext ctx);
}

然后,定义详细上下文信息类 PackageContext,其中包含一个当前状态 PackageState 和一个包裹的 id

package cn.happymaya.ndp.behavioral.state;

/**
 * 包裹上下文
 */
public class PackageContext {

    private PackageState currentState;

    private String packageId;

    public PackageContext(PackageState currentState, String packageId) {
        this.currentState = currentState;
        this.packageId = packageId;
        if (currentState == null) {
            this.currentState = Ackonwledged.getInstance();
        }
    }

    public PackageState getCurrentState() {
        return currentState;
    }

    public void setCurrentState(PackageState currentState) {
        this.currentState = currentState;
    }

    public void setPackageId(String packageId) {
        this.packageId = packageId;
    }

    public String getPackageId() {
        return packageId;
    }

    public void update() {
        currentState.updateState(this);
    }
}

接着,依次定义具体状态类:

  • 已下单(Acknowledged)
  • 仓库处理中(WarehouseProcessing)
  • 运输中(InTransition)
  • 派送中(Delivering)
  • **待取件(WaitForPickUp)、已签收(Received)。每一个类都会实现 updateState() 方法,同时使用单例模式模拟状态的唯一性。 **
package cn.happymaya.ndp.behavioral.state;

public class Ackonwledged implements PackageState{

    // Singleton

    private static Ackonwledged instance = new Ackonwledged();

    private Ackonwledged(){}

    public static Ackonwledged getInstance() {
        return instance;
    }

    @Override
    public void updateState(PackageContext ctx) {
        System.out.println("=== state start...");
        System.out.println("1 - Package is acknowledged !!");
        ctx.setCurrentState(WarehouseProcessing.getInstance());
    }
}

public class WarehouseProcessing implements PackageState {

    //Singleton

    private static WarehouseProcessing instance = new WarehouseProcessing();

    private WarehouseProcessing() {}

    public static WarehouseProcessing getInstance() {
        return instance;
    }

    @Override
    public void updateState(PackageContext ctx) {
        System.out.println("2 - Package is WarehouseProcessing");
        ctx.setCurrentState(InTransition.getInstance());
    }

}

public class InTransition implements PackageState {

    //Singleton

    private static InTransition instance = new InTransition();

    private InTransition() {}

    public static InTransition getInstance() {
        return instance;
    }

    @Override
    public void updateState(PackageContext ctx) {
        System.out.println("3 - Package is in transition !!");
        ctx.setCurrentState(Delivering.getInstance());
    }
}

public class Delivering implements PackageState{

    //Singleton

    private static Delivering instance = new Delivering();

    private Delivering() {}

    public static Delivering getInstance() {
        return instance;
    }

    @Override
    public void updateState(PackageContext ctx) {
        System.out.println("4 - Package is Delivering !!");
        ctx.setCurrentState(WaitForPickUp.getInstance());
    }
}

public class Received implements PackageState {

    //Singleton

    private static Received instance = new Received();

    private Received() {}

    public static Received getInstance() {
        return instance;
    }

    @Override
    public void updateState(PackageContext ctx) {
        System.out.println("6 - Package is Received !!");
        System.out.println("=== state end ");
    }
}

最后,运行一个单元测试,通过执行上下文信息类的更新操作了变更状态。

package cn.happymaya.ndp.behavioral.state;

public class Client {
    public static void main(String[] args) {
        PackageContext ctx = new PackageContext(null, "Test123");
        ctx.update();
        ctx.update();
        ctx.update();

        ctx.update();
        ctx.update();
        ctx.update();
    }
}

从单元测试的结果中,能直观地看到:执行一次状态更新,状态会变为下一个状态,直至状态结束。


使用状态模式的原因

使用状态模式的原因,主要有以下两个:

  1. 当要设计的业务具有复杂状态变迁时,期望通过状态变化来快速进行变更操作,并降低代码耦合性。 在上面使用场景的例子中,大致看到了一个包裹的状态变化流程,实际上的购物订单的状态变化远比这个要复杂。对于状态变化引起行为变化的情况,使用状态模式就能够很好地解决。
    1. 一方面因为状态是提前进行分析整理的,这样能减少代码实现的难度;
    2. 另一方面是因为状态与状态之间做了天然的隔离,能够将相关的行为聚合到一起,提高类的内聚度,降低耦合性;
  2. 避免增加代码的复杂性
    1. 在通常的设计中,每次新增状态时,需要添加大量的条件判断语句。最典型的就是 if-else 不断嵌套处理,这样的代码发展到后期,逻辑会变得异常复杂,进而导致代码可维护性和灵活性变差;
    2. 使用状态模式能够很好地从状态的维度来进行逻辑的关联,状态与状态之间只有切换的动作,至于状态本身如何进行复杂处理,对于另一个状态来说,其实并不关心,这样就能很好地避免对象间的调用关系变得复杂。

优缺点

通过上述分析,我们可以得出使用状态模式主要有以下优点。

  • 提前定好可能的状态,降低代码实现复杂度。 状态模式通常需要提前设计好状态的转移,这样就需要提前设计好状态之间的转移关系,在实现代码时就变得容易很多。
  • 快速理解状态和行为之间的关系。 由于将操作和状态相关联,那么所有与某个状态相关的对象都会被聚合在一起,这样可以很方便地增加和删除操作,而不会影响其他状态的功能。
  • 避免写大量的 if-else 条件语句。 比如,要判断订单商品到达配送站的状态,需要判断商品是在运送中还是已送达,到了以后还要再判断发往哪里,等等,这样的 if-else 条件语句会随着场景的增多而不断增加。如果建立起状态之间的转移关系,订单商品到达配送站会触发状态变换,然后进行对应状态下的对应操作,这样就能够有效减少直接的条件语句判断。
  • 可以让多个环境对象共享一个状态对象,从而减少重复代码。 状态模式通常用于整体的流程控制和状态变更,这样对于多环境的应用程序来说,只需要共享一整个状态,而不需要每个环境都各自实现自己的状态对象。

当然,状态模式也有一些缺点。

  • 造成很多零散类。 状态模式因为需要对每一个状态定义一个具体状态类,所以势必会增加系统类和对象的个数。
  • 状态切换关系越复杂,代码实现难度越高。 随着状态的不断扩展,状态的结构与实现就会变得复杂,比如,A 状态切换到 B,B 切换到 C,C 异常回退 A,A 再走 D 异常状态,等等。如果使用不当,就会造成维护代码的人需要花费大量时间来梳理状态转移关系。
  • 不满足开闭原则。 状态模式虽然降低了状态与状态之间的耦合性,但是新增和修改状态都会涉及前一个状态和后一个状态的代码修改,增大了引入代码问题的概率。

总结

  • 状态模式描述了对象状态的变化以及对象如何在每一种状态下表现出不同的行为;
  • 适合场景是**:对象本身具备很多状态变化,同时不同变化需要不同的行为来处理;**
  • 状态模式虽然可以让代码条理清楚,容易阅读,但是实际上对开闭原则的支持并不友好,新增状态可能会影响原有的状态,在使用时要注意!
  • 要想用好状态模式,关键点在于寻找好的状态以及状态与状态之间的关系,而不是急着去实现状态模式。状态确定好以后,状态模式本身代码实现是非常容易的。

标签:状态,ctx,private,状态机,instance,State,设计模式,public,currentState
From: https://blog.51cto.com/panyujie/7463269

相关文章

  • 【设计模式】策略模式Strategy:解决不同活动策略营销推荐场景
    (目录)模板方法模式能帮助我们进行公有方法的抽取,起到快速复用和扩展的作用。另一种快速复用和扩展代码的行为型模式:策略模式。策略模式在实际的开发中很常用,最常见的应用场景是利用它来替换过多的if-else嵌套的逻辑判断。除此之外,还能结合工厂模式给客户端提供非常灵活的使......
  • 设计模式-观察者模式
    设计模式提供了软件开发过程中的一些最佳实践,可以帮助我们解决常见的编程问题,提高软件的可维护性和可复用性,并使我们的代码更加健壮和灵活。设计模式可以带来以下好处:提高代码的可读性和可维护性、提高软件的可复用性、提高开发效率、提高系统的灵活性和可扩展性。今天我们讲一下观......
  • 《精通Python设计模式》 PDF电子书 +源码
    第1章工厂模式第2章建造者模式第3章其他创建型模式第4章适配器模式第5章装饰器模式第6章桥接模式第7章外观模式第8章其他结构型模式第9章职责链模式第10章命令模式第11章观察者模式第12章状态模式第13章其他行为型模式第14......
  • 设计模式-门面模式
    门面模式(文章目录)1、什么是门面模式  门面模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。注意这里针对的是接口。  这个定义很简洁,我再进一步解释一下。  假设有一个系统A,提供了a、b、c、d四个接口。系统B完成某个业务功能,需要调用A系统的......
  • Unity 游戏开发、02 基础篇 | 知识补充、简单使用动画、动画状态机
    前置笔记(由浅入深)Unity游戏开发、01基础篇2场景操作3D场景Q手型工具(鼠标中键):上下左右移动场景ALT+鼠标左键:以视图为中心旋转鼠标右键:以观察者为中心旋转SHIFT+Gizmo方块:Y轴归位物体节点+F:观察者定位至物体窗口布局3D项目一般窗口布局如下3全局光照全......
  • 6. 前端设计模式之观察者模式
    使用观察者模式,我们可以将某些对象(观察者)订阅到另一个对象(可观察对象)。当一个事件发生时,可观察对象会通知它的所有观察者!这个模式出境的概率就比较高了,无论是在前端还是后端,都能见到它的身影,特别跟事件有关的场景。从定义看这个模式涉及到两种对象,一种可观察对象也就是观察的......
  • 设计模式-装饰器模式
    装饰器模式(文章目录)什么是装饰器模式  装饰器(Decorator)模式的定义:指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式。为什么使用装饰器模式使用装饰者模式有如下好处:  1.避免了类爆炸问题,简化了设计  2.易于扩展,可以灵活组......
  • Android开发中常见的设计模式
    Android开发中常见的设计模式对于开发人员来说,设计模式有时候就是一道坎,但是设计模式又非常有用,过了这道坎,它可以让你水平提高一个档次。而在android开发中,必要的了解一些设计模式又是非常有必要的。对于想系统的学习设计模式的同学,这里推荐2本书。一本是HeadFirst系列的HeadH......
  • 软件设计模式系列之三———工厂方法模式
    1模式的定义工厂方法模式是一种常见的设计模式,属于创建型设计模式之一,它在软件工程中用于对象的创建。该模式的主要思想是将对象的创建过程抽象化,将具体对象的实例化延迟到子类中完成,以便在不同情况下可以创建不同类型的对象,而客户端代码不需要知道实际创建的对象类型。2举例......
  • 【23种设计模式】装饰模式(九)
    前言装饰模式,英文名称:DecoratorPattern。我第一次看到这个名称想到的是另外一个词语“装修”,我就说说我对“装修”的理解吧,大家一定要看清楚,是“装修”,不是“装饰”。在房子装修的过程中,各种功能可以相互组合,来增加房子的功用。类似的,如果我们在软件系统中,要给某个类型或者对象......