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

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

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

模式原理分析

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

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

我们先来看看状态模式的标准 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
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值