设计模式之状态模式:如何通过有限状态机监控功能的“状态变化”?

状态模式的应用场景非常广泛,比如,线上购物订单、手机支付、音乐播放器、游戏、工作流引擎等场景。状态模式设计的初衷是应对同一个对象里不同状态变化时的不同行为的变化。那么,当我们遇见类似的场景时,该如何来使用状态模式呢?

话不多说,让我们开始今天的学习吧。

模式原理分析

状态模式的原始定义是:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了自己的类一样。

这个定义确实有点抽象,简单来说,状态模式就是让一个对象通过定义一系列状态的变化来控制行为的变化。比如,给购买的物品定义几个包裹运送状态,已下单、运送中、已签收等,当“已下单”状态变为“运送中”状态时,物流货车会把包装好的包裹运送到指定地址,也就是说,当包裹的状态发生改变时,就会触发相应的外部操作。

我们先来看看状态模式的标准 UML 图:

Drawing 1.png

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

  • 上下文信息类(Context):实际上就是存储当前状态的类,对外提供更新状态的操作。

  • 抽象状态类(State):可以是一个接口或抽象类,用于定义声明状态更新的操作方法有哪些。

  • 具体状态类(StateA 等):实现抽象状态类定义的方法,根据具体的场景来指定对应状态改变后的代码实现逻辑。

下面我们直接来看看该 UML 对应的代码实现:

public interface State {

    void handle(Context context);

}

public class StateA implements State{

    private static StateA instance = new StateA();

    public StateA() {

    }

    public static StateA instance(){

        return instance;

    }

    @Override

    public void handle(Context context) {

        System.out.println("=== 进入状态A");

        context.setCurrentState(StateB.instance());

    }

}

public class StateB implements State {

    private static StateB instance = new StateB();

    public StateB() {

    }

    public static StateB instance(){

        return instance;

    }

    @Override

    public void handle(Context context) {

        System.out.println("=== 进入状态B");

        context.setCurrentState(this);

    }

}

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);

    }

}

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

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

使用场景分析

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

  • 对象根据自身状态的变化来进行不同行为的操作时, 比如,购物订单状态。

  • 对象需要根据自身变量的当前值改变行为,不期望使用大量 if-else 语句时, 比如,商品库存状态。

  • 对于某些确定的状态和行为,不想使用重复代码时, 比如,某一个会员当天的购物浏览记录。

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

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

为了帮助你更好地理解状态模式的适用场景,下面我们还是通过一个简单的例子来演示一下。在线上购物的过程中,当我们选定好了商品并提交订单后,装有商品的包裹就会开始进行运送。这里我们定义 6 种简单的包裹运送状态:已下单、仓库处理中、运输中、派送中、待取件和已签收。如下图所示:

Drawing 3.png

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

public interface PackageState {

    

    /**

     * 定义了6种状态

     * 1 - 已下单

     * 2 - 仓库处理中

     * 3 - 运输中

     * 4 - 派送中

     * 5 - 待取件

     * 6 - 已签收

     * @param ctx

     */

    void updateState(PackageContext ctx);

}

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

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 = Acknowledged.instance();

        }

    }

    public PackageState getCurrentState() {

        return currentState;

    }

    public void setCurrentState(PackageState currentState) {

        this.currentState = currentState;

    }

    public String getPackageId() {

        return packageId;

    }

    public void setPackageId(String packageId) {

        this.packageId = packageId;

    }

    public void update() {

        currentState.updateState(this);

    }

}

接下来,我们依次定义具体的状态类:已下单(Acknowledged)、仓库处理中(WarehouseProcessing)、运输中(InTransition)、派送中(Delivering)、待取件(WaitForPickUp)、已签收(Received)。每一个类都会实现 updateState() 方法,同时使用单例模式模拟状态的唯一性。

1 - 已下单

public class Acknowledged implements PackageState {

    //Singleton

    private static Acknowledged instance = new Acknowledged();

    private Acknowledged() {}

    public static Acknowledged instance() {

        return instance;

    }



    @Override

    public void updateState(PackageContext ctx) {

        System.out.println("=== state start...");

        System.out.println("1 - Package is acknowledged !!");

        ctx.setCurrentState(WarehouseProcessing.instance());

    }

}

public class WarehouseProcessing implements PackageState  {

    //Singleton

    private static WarehouseProcessing instance = new WarehouseProcessing();

    private WarehouseProcessing() {}

    public static WarehouseProcessing instance() {

        return instance;

    }



    @Override

    public void updateState(PackageContext ctx) {

        System.out.println("2 - Package is WarehouseProcessing");

        ctx.setCurrentState(InTransition.instance());

    }

}

public class InTransition implements PackageState {

    //Singleton

    private static InTransition instance = new InTransition();

 

    private InTransition() {}

 

    public static InTransition instance() {

        return instance;

    }

     

    //Business logic and state transition

    @Override

    public void updateState(PackageContext ctx) {

        System.out.println("3 - Package is in transition !!");

        ctx.setCurrentState(Delivering.instance());

    }

}

public class Delivering implements PackageState {

    //Singleton

    private static Delivering instance = new Delivering();

    private Delivering() {

    }

    public static Delivering instance() {

        return instance;

    }

    //Business logic

    @Override

    public void updateState(PackageContext ctx) {

        System.out.println("4 - Package is Delivering !!");

        ctx.setCurrentState(WaitForPickUp.instance());

    }

}

public class WaitForPickUp implements PackageState {

    //Singleton

    private static WaitForPickUp instance = new WaitForPickUp();

    private WaitForPickUp() {}

    public static WaitForPickUp instance() {

        return instance;

    }

    //Business logic and state transition

    @Override

    public void updateState(PackageContext ctx) {

        System.out.println("5 - Package is waiting for pick up !!");

        ctx.setCurrentState(Received.instance());

    }

}

public class Received implements PackageState {

    //Singleton

    private static Received instance = new Received();

    private Received() {}

    public static Received instance() {

        return instance;

    }

    //Business logic and state transition

    @Override

    public void updateState(PackageContext ctx) {

        System.out.println("6 - Package is Received !!");

        System.out.println("=== state end ");

    }

}

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

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();

    }

}

//输出结果

=== state start...

1 - Package is acknowledged !!

2 - Package is WarehouseProcessing

3 - Package is in transition !!

4 - Package is Delivering !!

5 - Package is waiting for pick up !!

6 - Package is Received !!

=== state end

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

为什么使用状态模式?

分析完状态模式的原理和使用场景后,我们再来说说使用状态模式的原因,主要有以下两个。

第一个,当要设计的业务具有复杂的状态变迁时,我们期望通过状态变化来快速进行变更操作,并降低代码耦合性。 在上面使用场景的例子中,我们大致看到了一个包裹的状态变化流程,实际上的购物订单的状态变化远比这个要复杂。对于状态变化引起行为变化的情况,使用状态模式就能够很好地解决。一方面因为状态是提前进行分析整理的,这样能减少代码实现的难度。另一方面是因为状态与状态之间做了天然的隔离,能够将相关的行为聚合到一起,提高类的内聚度,降低耦合性。

第二个,避免增加代码的复杂性。在通常的编程设计中,每一次新增状态时,就需要添加大量的条件判断语句。最典型的就是 if-else 的不断嵌套处理,这样的代码发展到后期,逻辑会变得异常复杂,进而导致代码的可维护性和灵活性变差。而使用状态模式则能够很好地从状态的维度来进行逻辑的关联,状态与状态之间只有切换的动作,至于状态本身如何进行复杂的处理,对于另一个状态来说,其实并不关心,这样就能很好地避免对象间的调用关系变得复杂。

收益什么?损失什么?

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

  • 提前定好可能的状态,降低代码实现复杂度。 状态模式通常需要提前设计好状态的转移,这样就需要提前设计好状态之间的转移关系,在实现代码时就变得容易很多。

  • 快速理解状态和行为之间的关系。 由于将操作和状态相关联,那么所有与某个状态相关的对象都会被聚合在一起,这样可以很方便地增加和删除操作,而不会影响其他状态的功能。

  • 避免写大量的 if-else 条件语句。 比如,要判断订单商品到达配送站的状态,需要判断商品是在运送中还是已送达,到了以后还要再判断发往哪里,等等,这样的 if-else 条件语句会随着场景的增多而不断增加。如果建立起状态之间的转移关系,订单商品到达配送站会触发状态变换,然后进行对应状态下的对应操作,这样就能够有效减少直接的条件语句判断。

  • 可以让多个环境对象共享一个状态对象,从而减少重复代码。 状态模式通常用于整体的流程控制和状态变更,这样对于多环境的应用程序来说,只需要共享一整个状态,而不需要每个环境都各自实现自己的状态对象。

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

  • 造成很多零散类。 状态模式因为需要对每一个状态定义一个具体状态类,所以势必会增加系统类和对象的个数。

  • 状态切换关系越复杂,代码实现难度越高。 随着状态的不断扩展,状态的结构与实现就会变得复杂,比如,A 状态切换到 B,B 切换到 C,C 异常回退 A,A 再走 D 异常状态,等等。如果使用不当,就会造成维护代码的人需要花费大量时间来梳理状态转移关系。

  • 不满足开闭原则。 状态模式虽然降低了状态与状态之间的耦合性,但是新增和修改状态都会涉及前一个状态和后一个状态的代码修改,增大了引入代码问题的概率。

总结

状态模式描述了对象状态的变化以及对象如何在每一种状态下表现出不同的行为。其适合场景是:对象本身具备很多状态变化,同时不同变化需要不同的行为来处理。

状态模式虽然可以让我们的代码条理清楚,容易阅读,但是实际上对开闭原则的支持并不友好,新增状态可能会影响原有的状态,在使用时要注意。

要想用好状态模式,关键点在于寻找好的状态以及状态与状态之间的关系,而不是急着去实现状态模式。状态确定好以后,状态模式本身的代码实现其实是非常容易的。

课后思考

在以往的开发经历中,你有没有自定义过不同的状态并用状态模式来进行代码实现?欢迎你在留言区与我分享。

在下一讲,我会接着与你分享“观察者模式与发送消息变化的通知”的相关内容,记得按时来听课!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 动环监控系统(Environmental Monitoring System,简称EMS)是一种对机房、数据中心、生产线等环境参数进行监控和管理的系统。根据EMS的应用场景和要求,选择合适的设计模式可以提高系统的性能、可维护性和可扩展性。 以下是几种常用的设计模式: 1. 观察者模式(Observer Pattern) 观察者模式是一种常用的设计模式,它可以用于EMS中实现监控数据的实时更新。观察者模式将主题(Subject)和观察者(Observer)分离开来,当主题发生变化时,会自动通知观察者进行更新。在EMS中,主题可以是环境参数采集器,观察者可以是显示屏、报警器等。 2. 状态模式(State Pattern) 状态模式可以用于EMS中实现状态机(State Machine),例如对温度、湿度等环境参数进行监控和管理。状态模式将每个状态都封装为一个类,这些状态类之间可以进行转换。在EMS中,状态模式可以用于实现温度和湿度的状态切换,例如当温度超过一定阈值时,系统可以切换为降温模式。 3. 命令模式(Command Pattern) 命令模式可以用于EMS中实现对设备的控制,例如对UPS、空调、灯光等设备进行控制。命令模式将命令封装为对象,这些对象可以被传递、存储和执行。在EMS中,命令模式可以用于实现对设备的控制,例如当温度超过一定阈值时,系统可以发送控制命令给空调。 4. 工厂模式(Factory Pattern) 工厂模式可以用于EMS中实现采集器、显示屏、报警器等对象的创建。工厂模式将对象的创建过程封装起来,客户端只需要通过工厂类来创建对象。在EMS中,工厂模式可以用于实现各种对象的创建,例如根据不同的环境参数采集方式,创建不同类型的采集器对象。 5. 单例模式(Singleton Pattern) 单例模式可以用于EMS中实现全局的环境参数存储和管理。单例模式确保一个类只有一个实例,并提供一个全局的访问点。在EMS中,单例模式可以用于实现全局的环境参数存储和管理,例如对历史环境数据的存储和查询。 以上是一些常用的设计模式,根据EMS的具体要求和场景,可以选择合适的设计模式进行实现。 ### 回答2: 动环监控系统设计模式的选择是一个重要决策,它直接影响到系统的可扩展性、可维护性和可靠性。根据系统的需求和特点,我们可以选择不同的设计模式。 首先,观察者模式是一个适合动环监控系统的设计模式。该模式将系统分为主题和观察者两部分,主题负责维护观察者的列表,并在状态发生改变时通知观察者。在动环监控系统中,主题可以是监测数据的源头,而观察者可以是需要接收监测数据的设备或用户。通过观察者模式,可以实现实时的数据传输和更新,确保监控数据的准确性和及时性。 其次,策略模式也是一个可用的设计模式。动环监控系统可能需要根据不同的监测目标和环境进行不同的监控策略。策略模式可以将不同的策略封装成独立的类,并在运行时根据需要动态地选择合适的策略。例如,针对不同的设备类型和监测指标,可以有不同的算法和处理方式。通过策略模式,系统可以灵活地适应变化的监测需求,同时保持代码的可维护性和可扩展性。 此外,桥接模式也可以考虑用于动环监控系统的设计。动环监控系统可能包括不同的功能模块,如数据采集、数据处理、数据展示等。桥接模式可以将这些功能模块分离,使它们能够独立变化。通过定义抽象接口和实现类之间的桥梁,可以实现各个模块之间的解耦和扩展。桥接模式能够提高系统的灵活性和可维护性,方便对功能模块进行替换和升级。 综上所述,动环监控系统的设计模式选择需要根据具体的应用场景来确定。观察者模式、策略模式和桥接模式都是常用的设计模式,它们能够提供良好的解决方案,满足系统的需求。通过合理地选择设计模式,可以提高动环监控系统的性能和可维护性,提升用户体验和系统的稳定性。 ### 回答3: 动环监控系统是用于实时监测和管理数据中心的温度、湿度、电压等环境参数的系统。在设计动环监控系统时,选择合适的设计模式是非常重要的。 首先,我们可以考虑使用观察者模式。该模式可以将需要被监控的对象(观察目标)与观察者进行解耦,当观察目标的状态发生变化时,观察者会收到相应的通知并做出相应的处理。在动环监控系统中,观察目标可以是各种环境传感器,而观察者可以是处理传感器数据的模块。通过观察者模式,可以实现对环境参数的实时监测,以及根据情况作出相应的报警或调整。 其次,我们可以考虑使用责任链模式。该模式可以将请求的发送者和接收者解耦,并按照一定的顺序将请求依次传递给不同的处理者。在动环监控系统中,可以将从传感器收集到的数据作为请求,通过责任链模式依次传递给不同的处理模块,比如温度、湿度以及电压处理模块。每个处理模块可以负责不同的数据处理逻辑,从而实现对环境参数的监测和管理。 最后,我们可以考虑使用单例模式。该模式可以保证一个类只有一个实例,并提供全局访问点。在动环监控系统中,可以使用单例模式来管理整个系统的状态和数据。比如,可以设计一个单例类来管理所有传感器的状态和数据,同时提供一个全局访问点,方便其他模块对传感器数据进行监测和管理。 综上所述,当设计动环监控系统时,观察者模式、责任链模式以及单例模式都是值得考虑的设计模式,它们可以提供良好的解耦性、灵活性和可扩展性,有助于实现高效的动环监控系统。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值