基于枚举实现的状态流转控制

24 阅读4分钟

引言

在现代软件开发中,处理复杂的业务流程时,状态机(State Machine)是一种非常有效的设计模式。它可以帮助我们清晰地定义和管理业务对象的状态及其转换逻辑。本文将通过一个具体的例子——订单处理系统,来展示如何使用状态机接口来控制订单状态的流转,并提供一个基于Java实现的示例。

什么是状态机

状态机是一种行为模型,它描述了系统在不同状态下对外部事件的响应方式以及这些响应如何导致状态的变化。在状态机中,每个状态都有自己的行为,当接收到特定事件时,状态机会根据当前状态和事件执行相应的动作,并可能转换到另一个状态。下面我们通过接口和枚举设计一个简易的订单状态机。

第一步

定义状态机接口,用于统一枚举状态机的行为。

/**
 * 状态机接口
 * @author luguoxiang
 * 开源项目:https://gitee.com/lgx1992/lg-soar 求star!请给我star!请帮我点个star!
 */
public interface StateMachine<S> {

    /**
     * 允许的来源状态(只有这些状态下允许执行事件)
     * @return
     */
    S[] getSource();

    /**
     * 事件执行后的目标状态(事件执行成功后,状态流转到此状态)
     * @return
     */
    S getTarget();

    /**
     * 执行事件(事件允许执行后返回状态值,不被允许执行走拒绝处理,抛异常)
     * @param current 当前状态
     * @return 目标状态
     */
    default S execute(S current) {
        if (allowable(current)) {
            return getTarget();
        }
        return reject(current);
    }

    /**
     * 判断当前状态是否允许执行事件(可用于控制前端操作按钮)
     * @param current 当前状态
     * @return
     */
    default boolean allowable(S current) {
        S[] source = getSource();
        if (source == null || source.length == 0) {
            return true;
        }
        for (S s : source) {
            if (s.equals(current)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 被拒绝后的处理(当前状态不满足来源状态时)
     * @param current 当前状态
     * @return
     */
    default S reject(S current) {
        throw new StateException(current + "无法流转到" + getTarget());
    }

    public static class StateException extends RuntimeException{
        public StateException() {
            super();
        }

        public StateException(String message) {
            super(message);
        }

        public StateException(String message, Throwable cause) {
            super(message, cause);
        }

        public StateException(Throwable cause) {
            super(cause);
        }

        protected StateException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
            super(message, cause, enableSuppression, writableStackTrace);
        }
    }
}

第二步

我们需要定义订单可能经历的各种状态。对于一个典型的电子商务平台,订单可以有以下几种状态。

/**
 * @author luguoxiang
 * @Date: 2024/8/23 17:22
 * 开源项目:https://gitee.com/lgx1992/lg-soar 求star!请给我star!请帮我点个star!
 */
public enum OrderStatus implements IDict<Integer> {

    /**
     * 新订单
     */
    NEW(0, "新"),

    /**
     * 已确认待付款
     */
    CONFIRMED(1, "待付款"),

    /**
     * 已付款待发货
     */
    PAID(2, "待发货"),

    /**
     * 已发货待收货
     */
    SHIPPED(3, "待收货"),

    /**
     * 完成
     */
    COMPLETED(4, "已完成")
    ;


    OrderStatus(int value, String label) {
        this.value = value;
        this.label = label;
    }

    private final int value;
    private final String label;
    @Override
    public Integer getValue() {
        return value;
    }
    @Override
    public String getLabel() {
        return label;
    }
}

第三步

定义订单操作枚举,定义从一种状态转移到另一种状态的操作。例如,支付、发货等操作。

/**
 * @author luguoxiang
 * @Date: 2024/8/23 17:22
 * 开源项目:https://gitee.com/lgx1992/lg-soar 求star!请给我star!请帮我点个star!
 */
public enum OrderEvent implements StateMachine<OrderStatus> {

    /**
     * 确认订单
     * 执行操作后状态流转为:CONFIRMED
     */
    confirm(OrderStatus.CONFIRMED, OrderStatus.NEW),

    /**
     * 支付
     * 执行操作后状态流转为:PAID
     */
    pay(OrderStatus.PAID, OrderStatus.NEW, OrderStatus.CONFIRMED),

    /**
     * 发货
     * 执行操作后状态流转为:SHIPPED
     */
    ship(OrderStatus.SHIPPED, OrderStatus.PAID),

    /**
     * 确认收货
     * 执行操作后状态流转为:COMPLETED
     */
    receiving(OrderStatus.COMPLETED, OrderStatus.SHIPPED),
    ;

    /**
     * @param target 执行事件后的状态
     * @param source 哪些状态下可执行事件,不配置则任意状态都可以执行该事件
     */
    OrderEvent(OrderStatus target, OrderStatus...source) {
        this.target = target;
        this.source = source;
    }

    private final OrderStatus target;
    private final OrderStatus[] source;
    @Override
    public OrderStatus getTarget() {
        return target;
    }
    @Override
    public OrderStatus[] getSource() {
        return source;
    }
}

第四步(使用)

然后我们就可以在各种订单操作中使用我们的状态机枚举来校验状态、控制前端按钮。

发货操作
    /**
     * 发货操作
     */
    public void ship(Long id) throws Exception {
        // 查询订单
        Order order = this.getById(id);
        // 执行订单发货操作,执行成功返回新的订单状态,否则抛异常
        OrderStatus status = OrderOperate.ship.execute(order.getStatus());
        // 更新订单状态
        order.setStatus(status);
        this.updateById(order);
    } 
通过VO字段控制前端操作按钮
    /**
     * 查询订单详情
     */
    public OrderVO detail(Long id) throws Exception {
        // 查询订单
        Order order = this.getById(id);
        OrderVO vo = new OrderVO();
        BeanUtils.copyProperties(order, vo);
        OrderStatus status = order.getStatus();
        // 是否允许发货
        vo.setAllowableShip(OrderOperate.receiving.allowable(status))
        // 是否允许付款
        vo.setAllowablePay(OrderOperate.receiving.allowable(status))
        return vo;
    } 

结论

采用状态机模式不仅能够使代码更加清晰易懂,而且有助于维护和扩展。通过上述示例可以看出,合理利用状态机可以有效简化复杂的业务逻辑处理过程,提高系统的可维护性和灵活性。希望本文能帮助您更好地理解和应用这一强大的设计模式!

尾声

本文提供的示例代码仅用于演示目的,具体实现细节可能会根据实际需求有所不同。希望此文章对您的项目有所帮助!本文代码来源六哥开源前后端开源框架lg-soar,如果您觉得对您有所帮助欢迎访问 gitee.com/lgx1992/lg-… 给lg-soar一个star