写点什么

还有谁不会如此优雅的状态机模式?

作者:架构师之道
  • 2025-01-08
    湖南
  • 本文字数:7043 字

    阅读完需:约 23 分钟

前言

状态模式一般是用在对象内部的状态流转场景中,用来实现状态机。

什么是状态机?

状态机是对状态转移的抽象,由事件、状态、动作组成,事件有时候也被称为转移事件或者转移,当事件触发时,可以将状态由一个状态变更为另一个状态,并执行动作。其中,事件和状态是必须存在的,动作可以不要。

下面是一张状态图,表达的就是一个状态机的模型。

通俗来讲,就是对状态的变更做了一定的限制,不能随意的修改状态,而是只有处于某个特定的状态时,才能变更到另一个特定的状态。

状态模式

状态模式将状态抽象成一个个的状态对象,状态机当前持有某个状态对象,就表示当前的状态机处于什么状态。然后将事件处理为一个个的方法,每个方法中会操作状态机修改状态,有需要的情况下,在修改状态的同时还可以执行某些动作。

把通用部分提取出来后,可以得到这样一个通用类图:

可以看到上面的 StateMachine 和 State 关系是双向的,这是因为状态机需要持有状态对象来表示当前状态,以及通过当前的状态对象中的方法进行状态的流转,而流转的结果需要重新 set 到状态机中,又要求 State 必须持有状态机对象。当然,这里 State 对 StateMachine 的关系也可以通过依赖来表示。

订单状态流转案例

假设现在有一个商品订单的状态流转需求,状态图如下:

这里没有加退款的状态,后续的拓展例子上会加上,用这种方式来体验状态模式的拓展性。

我们拿着这个图的时候,可以简单的在脑海里面过一遍如果通过 if/else 或者 switch 来做,应该要怎么写,后续如果想把退款的状态加入进去又该怎么拓展,这种方式应该大家都会,就不在这里赘述了。

接下来,就一步步的通过状态模式来实现这么一个状态机。

状态枚举定义

定义状态枚举主要是为了统一状态常量,因为订单是需要落库的,我们在持久化到数据库时,不能把状态对象保存进去,所以会涉及到状态常量与状态对象的互相转换。定义的枚举如下:

import lombok.Getter;
@Getterpublic enum OrderStateEnum {    WAIT_PAYMENT(1, "待支付"),    WAIT_DELIVER(2, "待发货"),    WAIT_RECEIVE(3, "待收货"),    RECEIVED(4, "已收货"),    CANCEL(5, "已取消");
    private final int state;    private final String desc;
    OrderStateEnum(int state, String desc) {        this.state = state;        this.desc = desc;    }
    public int getState() {        return state;    }
    public String getDesc() {        return desc;    }}

复制代码

状态接口与实现

先上代码:

public interface OrderState {
    OrderStateEnum orderStateType();        default void pay(OrderStateMachine stateMachine) {        System.out.println("|--当前订单状态不支持支付,已忽略");    }    default void cancel(OrderStateMachine stateMachine) {        System.out.println("|--当前订单状态不支持取消,已忽略");    }    default void deliver(OrderStateMachine stateMachine) {        System.out.println("|--当前订单状态不支持发货,已忽略");    }    default void receive(OrderStateMachine stateMachine) {        System.out.println("|--当前订单状态不支持收货,已忽略");    }}
复制代码

接口中定义的 pay,cancel 等方法就是事件,供子类进行实现,相信大家也发现了,这些事件没有定义成抽象方法,而是通过 default 定义成了一个实例方法。不太清楚为什么的同学,可以先思考一下为什么要这么定义。

其实这么定义的好处是各个状态子类只需要实现自己需要的方法,而不用把所有的方法都实现一遍,这种做法在 Spring 中也比较常见,在 JDK8 之前通常是用 xxxWrapper 来实现的,JDK8 之后就重构为直接使用 default 方法来实现了。

举个例子:后续如果需要加入退款状态,接口中也会新增一个提交退款的事件,在各个子类中,选择需要实现提交退款事件的状态子类进行重写即可,而不需要所有的子类都重写。


有多少个状态,就有多少个实现类,并按照上面的状态图,在对应的状态中实现自己需要的事件。

待支付状态:有支付和取消两种事件(注意看事件里面 setCurrentState 做的状态流转):

public class WaitPaymentState implements OrderState {
    @Override    public OrderStateEnum orderStateType() {        return OrderStateEnum.WAIT_PAYMENT;    }
    @Override    public void pay(OrderStateMachine stateMachine) {        stateMachine.setCurrentState(new WaitDeliverState());    }
    @Override    public void cancel(OrderStateMachine stateMachine) {        stateMachine.setCurrentState(new CancelState());    }}

复制代码

待发货状态:有发货事件

public class WaitDeliverState implements OrderState {
    @Override    public OrderStateEnum orderStateType() {        return OrderStateEnum.WAIT_DELIVER;    }
    @Override    public void deliver(OrderStateMachine stateMachine) {        stateMachine.setCurrentState(new WaitReceiveState());    }}

复制代码

待收货状态:有收货事件

public class WaitReceiveState implements OrderState {
    @Override    public OrderStateEnum orderStateType() {        return OrderStateEnum.WAIT_RECEIVE;    }
    @Override    public void receive(OrderStateMachine stateMachine) {        stateMachine.setCurrentState(new ReceivedState());    }}

复制代码

已收货状态:状态结束点,没有其他事件

public class ReceivedState implements OrderState {
    @Override    public OrderStateEnum orderStateType() {        return OrderStateEnum.RECEIVED;    }
}
复制代码

取消状态:状态结束点,没有其他事件

public class CancelState implements OrderState {
    @Override    public OrderStateEnum orderStateType() {        return OrderStateEnum.CANCEL;    }}
复制代码

状态机

状态机中需要持有当前状态对象,同时需要把状态接口中的事件同步定义到状态机中,以便外部业务对象调用。

除此之外,状态枚举常量与状态对象之间的映射关系也可以直接配置在当前状态机中,功能更加内聚。

public class OrderStateMachine {
    public static final Map<OrderStateEnum, OrderState> ORDER_STATE_MAP = new HashMap<>();
    static {        ORDER_STATE_MAP.put(OrderStateEnum.WAIT_PAYMENT, new WaitPaymentState());        ORDER_STATE_MAP.put(OrderStateEnum.WAIT_DELIVER, new WaitDeliverState());        ORDER_STATE_MAP.put(OrderStateEnum.WAIT_RECEIVE, new WaitReceiveState());        ORDER_STATE_MAP.put(OrderStateEnum.RECEIVED, new ReceivedState());        ORDER_STATE_MAP.put(OrderStateEnum.CANCEL, new CancelState());    }
    private OrderState currentState;
    public OrderStateMachine(OrderStateEnum orderStateEnum) {        this.currentState = ORDER_STATE_MAP.get(orderStateEnum);    }
    public OrderState getCurrentState() {        return currentState;    }
    public void setCurrentState(OrderState currentState) {        this.currentState = currentState;    }
    void pay() {        currentState.pay(this);    }
    void deliver() {        currentState.deliver(this);    }
    void receive() {        currentState.receive(this);    }
    void cancel() {        currentState.cancel(this);    }
}
复制代码

测试

做一下状态机的测试,由于打印的日志重复度很高,这里取了个巧,将函数作为参数封装了一下:

public class OrderService {

public class OrderService {
    public static void main(String[] args) {        OrderStateMachine stateMachine = new OrderStateMachine(OrderStateEnum.WAIT_DELIVER);
        invoke(stateMachine::pay, "用户支付", stateMachine);        invoke(stateMachine::deliver, "商家发货", stateMachine);        invoke(stateMachine::receive, "用户收货", stateMachine);        invoke(stateMachine::cancel, "取消支付", stateMachine);    }
    public static void invoke(Runnable runnable, String desc, OrderStateMachine stateMachine) {        System.out.println(desc + "前订单状态: " + stateMachine.getCurrentState().orderStateType().getDesc());        runnable.run();        System.out.println(desc + "后订单状态: " + stateMachine.getCurrentState().orderStateType().getDesc());        System.out.println("------------------");    }}
复制代码

以待发货作为状态常量创建了一个状态机,状态机当前的状态就是待发货,下面的四个调用中,第 1,4 个是不会改变状态的,第 2,3 个会改变状态,下面以执行结果来验证猜测:

用户支付前订单状态: 待发货|--当前订单状态不支持支付,已忽略用户支付后订单状态: 待发货------------------商家发货前订单状态: 待发货商家发货后订单状态: 待收货------------------用户收货前订单状态: 待收货用户收货后订单状态: 已收货------------------取消支付前订单状态: 已收货|--当前订单状态不支持取消,已忽略取消支付后订单状态: 已收货------------------
复制代码

退款状态的拓展

通过状态模式来实现状态机,看重的就是它带来的拓展性和易维护性,所以在原有的基础上,加上退款的事件和状态,一起看看需要做些什么事。

代码拓展

下面是加入了退款的状态图:

通过状态图可以看到,需要加入:

  • 两个状态:退款中和已退款

  • 两个事件:申请退款和确认退款

  • 原有状态拓展:待发货、待收货、已收货 3 个状态中都需要引入申请退款事件

综上,一步一步的拓展代码:

第一步:拓展枚举常量

public enum OrderStateEnum {    WAIT_PAYMENT(1, "待支付"),    WAIT_DELIVER(2, "待发货"),    WAIT_RECEIVE(3, "待收货"),    RECEIVED(4, "已收货"),    CANCEL(5, "已取消"),    REFUNDING(6, "退款中"),    REFUNDED(7, "已退款"),    ; // 省略后续代码……}
复制代码

第二步:拓展状态接口

public interface OrderState {    // 省略已有代码……
    default void refund(OrderStateMachine stateMachine) {        System.out.println("|--当前订单状态不支持退款,已忽略");    }
    default void confirmRefund(OrderStateMachine stateMachine) {        System.out.println("当前订单状态不支持确认退款,已忽略");    }}
复制代码

第三步:新增两个状态,退款中与已退款

public class RefundingState implements OrderState {
    @Override    public OrderStateEnum name() {        return OrderStateEnum.REFUNDING;    }
    @Override    public void confirmRefund(OrderStateMachine stateMachine) {        stateMachine.setCurrentState(new RefundedState());    }}
复制代码


public class RefundedState implements OrderState {
    @Override    public OrderStateEnum name() {        return OrderStateEnum.REFUNDED;    }}
复制代码

第四步:拓展原有状态,待发货,待收货,已收货

public class WaitDeliverState implements OrderState {    // 省略已有代码……        @Override    public void refund(OrderStateMachine stateMachine) {        stateMachine.setCurrentState(new RefundingState());    }}    
复制代码


public class WaitReceiveState implements OrderState {    // 省略已有代码……        @Override    public void refund(OrderStateMachine stateMachine) {        stateMachine.setCurrentState(new RefundingState());    }
}
复制代码


public class ReceivedState implements OrderState {    // 省略已有代码……
    @Override    public void refund(OrderStateMachine stateMachine) {        stateMachine.setCurrentState(new RefundingState());    }
}
复制代码

第五步:拓展状态机

public class OrderStateMachine {
    public static final Map<OrderStateEnum, OrderState> ORDER_STATE_MAP = new HashMap<>();
    static {          // 省略已有状态……        ORDER_STATE_MAP.put(OrderStateEnum.REFUNDING, new RefundingState());        ORDER_STATE_MAP.put(OrderStateEnum.REFUNDED, new RefundedState());    }
    // 省略已有方法……
    void refund() {        currentState.refund(this);    }
    void confirmRefund() {        currentState.confirmRefund(this);    }}
复制代码

测试

在上面的代码中可以看到,都是在对配置进行追加,而没有对原有的逻辑做任何的修改,然后写一个测试:

public class OrderService {
    public static void main(String[] args) {        OrderStateMachine stateMachine = new OrderStateMachine(OrderStateEnum.WAIT_DELIVER);
        invoke(stateMachine::pay, "用户支付", stateMachine);        invoke(stateMachine::deliver, "商家发货", stateMachine);        invoke(stateMachine::receive, "用户收货", stateMachine);        invoke(stateMachine::cancel, "取消支付", stateMachine);        invoke(stateMachine::refund, "申请退款", stateMachine);        invoke(stateMachine::confirmRefund, "确认退款", stateMachine);    }
    public static void invoke(Runnable runnable, String desc, OrderStateMachine stateMachine) {        System.out.println(desc + "前订单状态: " + stateMachine.getCurrentState().orderStateType().getDesc());        runnable.run();        System.out.println(desc + "后订单状态: " + stateMachine.getCurrentState().orderStateType().getDesc());        System.out.println("------------------");    }
}
复制代码

查看日志,是否触发退款:

用户支付前订单状态: 待发货|--当前订单状态不支持支付,已忽略用户支付后订单状态: 待发货------------------商家发货前订单状态: 待发货商家发货后订单状态: 待收货------------------用户收货前订单状态: 待收货用户收货后订单状态: 已收货------------------取消支付前订单状态: 已收货|--当前订单状态不支持取消,已忽略取消支付后订单状态: 已收货------------------申请退款前订单状态: 已收货申请退款后订单状态: 退款中------------------确认退款前订单状态: 退款中确认退款后订单状态: 已退款------------------
复制代码

小结

从上面的代码可以看到,通过状态模式可以很轻松的对状态进行拓展。

不过上面的例子中没有对状态机中的动作进行实现,其实动作和状态转换的逻辑放在一起就可以了,即通过事件(方法调用) 可以变更状态,同时也能够触发对应的动作。

此外,代码中只是状态机的流程,实际的开发中应该将状态机关联到对应的业务实体中,通过业务实体的实时状态来创建状态机,在完成状态流转之后再将状态更新到业务实体中。

总结

本篇主要讲述了如何通过状态模式来实现一个状态机。状态模式的实现,代码结构清晰(相对于 if/else,switch)拓展性强,同时也起到了良好的封装效果(状态在状态机内部流转,业务流程不需要关心状态到底是怎么流转的)。

当然缺点就是类膨胀问题,类会比较多,如果状态非常复杂的情况下,也可以采取其他办法来实现状态机,例如查表法。

总之,要分析并实现一个业务流程中的状态流转的时候,先画出状态图,以状态图为指导来选择状态机的实现方式即可,在状态相对不那么复杂的情况下,可以优先考虑使用状态模式。

用户头像

还未添加个人签名 2022-04-10 加入

还未添加个人简介

评论

发布
暂无评论
还有谁不会如此优雅的状态机模式?_编程_架构师之道_InfoQ写作社区