在JAVA中使用状态机实现状态之间的流转

在JAVA中使用状态机实现状态之间的流转

LM 454 2023-08-17

起因

最近业务上有一个需求,任务存在多个不同的状态,需要实现状态间的流转,但是逻辑会太复杂,直接套用流程引擎又没有必要,所以需要自己实现一个简单的状态流转

最简单的实现

状态分为以下九种

待分配已分配提出方审核待指配已指配协作方审核发起人审核已完成已废弃

状态之间流转的动作分为以下三种

通过驳回废弃

将上面两种类型定义为枚举类:

  • 动作枚举

    @Getter
    public enum TaskAction {
        PASS(1, "通过"),
        REJECT(2, "驳回"),
        ABANDON(3, "废弃");
    
        private Integer id;
        private String description;
    
    
        TaskAction(Integer id, String description) {
            this.id = id;
            this.description = description;
        }
    
        public static TaskAction getById(Integer id) {
            for (TaskAction action : TaskAction.values()) {
                if (action.getId().equals(id)) {
                    return action;
                }
            }
            return null;
        }
    }
    
  • 状态枚举

    @Getter
    public enum TaskStatus {
        WAITING_ASSIGN(1, "待分配"),
        ASSIGNED(2, "已分配"),
        PROPOSER_AUDIT(3, "提出方审核"),
        WAITING_APPOINT(4, "待指配"),
        APPOINTED(5, "已指配"),
        COLLABORATOR_AUDIT(6, "协作方审核"),
        INITIATOR_AUDIT(7, "发起人审核"),
        FINISHED(8, "已完成"),
        ABANDONED(9, "已废弃");
    
        private Integer id;
        private String description;
    
    
        TaskStatus(Integer id, String description) {
            this.id = id;
            this.description = description;
        }
    
        public static TaskStatus getById(Integer id) {
            for (TaskStatus status : TaskStatus.values()) {
                if (status.getId().equals(id)) {
                    return status;
                }
            }
            return null;
        }
    }
    

使用switch…case即可实现最简单的状态流转

    public void handleInstance(TaskFlowInstanceHandleReq req) {
        //TODO 任务鉴权

        //检查处理动作是否合法
        TaskAction action = Optional.ofNullable(TaskAction.getById(req.getActionId()))
                .orElseThrow(() -> new ApiException(ResultCode.PARAMETER_ILLEGAL));
        //检查当前任务是否存在
        TaskFlowInstance instance = Optional.ofNullable(this.getCurrentInstanceByTaskId(req.getTaskId()))
                .orElseThrow(() -> new ApiException(ResultCode.TASK_FLOW_INSTANCE_NOT_EXIST));
        TaskStatus status = instance.getStatus();

        switch (action) {
            case PASS:
                if (status.equals(TaskStatus.WAITING_ASSIGN)) {
                    // do something
                } else if (status.equals(TaskStatus.ASSIGNED)) {
                    // do something
                } else if (status.equals(TaskStatus.PROPOSER_AUDIT)) {
                    // do something
                } else if (status.equals(TaskStatus.WAITING_APPOINT)) {
                    // do something
                } else if (status.equals(TaskStatus.APPOINTED)) {
                    // do something
                } else if (status.equals(TaskStatus.COLLABORATOR_AUDIT)) {
                    // do something
                } else if (status.equals(TaskStatus.INITIATOR_AUDIT)) {
                    // do something
                } else {
                    throw new ApiException(ResultCode.PARAMETER_ILLEGAL);
                }
                break;
            case REJECT:
                if (status.equals(TaskStatus.WAITING_ASSIGN)) {
                    // do something
                } else if (status.equals(TaskStatus.ASSIGNED)) {
                    // do something
                } else if (status.equals(TaskStatus.PROPOSER_AUDIT)) {
                    // do something
                } else if (status.equals(TaskStatus.WAITING_APPOINT)) {
                    // do something
                } else if (status.equals(TaskStatus.APPOINTED)) {
                    // do something
                } else if (status.equals(TaskStatus.COLLABORATOR_AUDIT)) {
                    // do something
                } else if (status.equals(TaskStatus.INITIATOR_AUDIT)) {
                    // do something
                } else {
                    throw new ApiException(ResultCode.PARAMETER_ILLEGAL);
                }
                break;
            case ABANDON:
                if (status.equals(TaskStatus.WAITING_ASSIGN)) {
                    // do something
                } else if (status.equals(TaskStatus.ASSIGNED)) {
                    // do something
                } else if (status.equals(TaskStatus.PROPOSER_AUDIT)) {
                    // do something
                } else if (status.equals(TaskStatus.WAITING_APPOINT)) {
                    // do something
                } else if (status.equals(TaskStatus.APPOINTED)) {
                    // do something
                } else if (status.equals(TaskStatus.COLLABORATOR_AUDIT)) {
                    // do something
                } else if (status.equals(TaskStatus.INITIATOR_AUDIT)) {
                    // do something
                } else {
                    throw new ApiException(ResultCode.PARAMETER_ILLEGAL);
                }
                break;
            default:
                throw new ApiException(ResultCode.PARAMETER_ILLEGAL);
        }
    }

但是这样会显得代码结构不清晰,过于冗杂,且拓展性不强,所以使用了状态机的思想重新设计了一次

状态机

状态机全称有限状态机,因为一般的状态机的状态都是离散而且可枚举的,这就是有限的原因。

状态机表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。

通俗的描述状态机就是定义了一套状态変更的流程:状态机包含一个状态集合,定义当状态机处于某一个状态的时候它所能接收的事件以及可执行的行为,执行完成后,状态机所处的状态变化可以被感知。

一般包含以下几个概念:

  • 状态
  • 事件 状态机的状态变更肯定是通过触发一个事件引起的
  • 行为 触发了之后执行的业务逻辑。比如说订单未支付状态到支付状态的变更需要走的业务,写流水、修改账户余额等
  • 变更 一个状态被一个事件触发执行了某些行为到达了另外一个状态的过程

Snipaste_2023-08-17_11-41-25

设计状态机

重新分析需求,就是一个状态在接收到一个动作时转变为另一个状态,并且需要加上处理逻辑

初始状态 -> 动作 -> 目标状态( -> 处理逻辑)

发现状态机完美满足这种需求,因此可以设计一个嵌套Map的数据结构

Map<TaskStatus,Map<TaskAction,ActionExec>>

自己实现这个数据结构有点麻烦,刚好谷歌的 com.google.common包中提供了HashBasedTable<R, C, V>这种数据结构,直接使用即可

直接上代码

public class TaskStatusMachine {

    private TaskStatusMachine() {
    }

    @Data
    @AllArgsConstructor
    static class ActionExec {
        private TaskStatus nextStatus;
        private IActionHandler handler;
    }

    private static final HashBasedTable<TaskStatus, TaskAction, ActionExec> ACTION_MAP = HashBasedTable.create();

    static {
        ACTION_MAP.put(TaskStatus.WAITING_ASSIGN, TaskAction.PASS, new ActionExec(TaskStatus.ASSIGNED, new WaitingAssignPassActionHandler()));

        ACTION_MAP.put(TaskStatus.ASSIGNED, TaskAction.PASS, new ActionExec(TaskStatus.PROPOSER_AUDIT, new AssignPassActionHandler()));
        ACTION_MAP.put(TaskStatus.ASSIGNED, TaskAction.REJECT, new ActionExec(TaskStatus.WAITING_ASSIGN, new EmptyActionHandler()));
        ACTION_MAP.put(TaskStatus.ASSIGNED, TaskAction.ABANDON, new ActionExec(TaskStatus.ABANDONED, new NormalAbandonActionHandler()));

        ACTION_MAP.put(TaskStatus.PROPOSER_AUDIT, TaskAction.PASS, new ActionExec(TaskStatus.WAITING_APPOINT, new ProposerAuditPassActionHandler()));
        ACTION_MAP.put(TaskStatus.PROPOSER_AUDIT, TaskAction.REJECT, new ActionExec(TaskStatus.ASSIGNED, new EmptyActionHandler()));
        ACTION_MAP.put(TaskStatus.PROPOSER_AUDIT, TaskAction.ABANDON, new ActionExec(TaskStatus.ABANDONED, new NormalAbandonActionHandler()));

        ACTION_MAP.put(TaskStatus.WAITING_APPOINT, TaskAction.PASS, new ActionExec(TaskStatus.APPOINTED, new WaitingAppointPassActionHandler()));
        ACTION_MAP.put(TaskStatus.WAITING_APPOINT, TaskAction.REJECT, new ActionExec(TaskStatus.PROPOSER_AUDIT, new EmptyActionHandler()));
        ACTION_MAP.put(TaskStatus.WAITING_APPOINT, TaskAction.ABANDON, new ActionExec(TaskStatus.ABANDONED, new NormalAbandonActionHandler()));

        ACTION_MAP.put(TaskStatus.APPOINTED, TaskAction.PASS, new ActionExec(TaskStatus.COLLABORATOR_AUDIT, new AppointedPassActionHandler()));
        ACTION_MAP.put(TaskStatus.APPOINTED, TaskAction.REJECT, new ActionExec(TaskStatus.WAITING_APPOINT, new EmptyActionHandler()));
        ACTION_MAP.put(TaskStatus.APPOINTED, TaskAction.ABANDON, new ActionExec(TaskStatus.ABANDONED, new NormalAbandonActionHandler()));

        ACTION_MAP.put(TaskStatus.COLLABORATOR_AUDIT, TaskAction.PASS, new ActionExec(TaskStatus.INITIATOR_AUDIT, new CollaboratorAuditPassActionHandler()));
        ACTION_MAP.put(TaskStatus.COLLABORATOR_AUDIT, TaskAction.REJECT, new ActionExec(TaskStatus.APPOINTED, new EmptyActionHandler()));
        ACTION_MAP.put(TaskStatus.COLLABORATOR_AUDIT, TaskAction.ABANDON, new ActionExec(TaskStatus.ABANDONED, new NormalAbandonActionHandler()));

        ACTION_MAP.put(TaskStatus.INITIATOR_AUDIT, TaskAction.PASS, new ActionExec(TaskStatus.FINISHED, new InitiatorAuditPassActionHandler()));
        ACTION_MAP.put(TaskStatus.INITIATOR_AUDIT, TaskAction.REJECT, new ActionExec(TaskStatus.COLLABORATOR_AUDIT, new EmptyActionHandler()));
        ACTION_MAP.put(TaskStatus.INITIATOR_AUDIT, TaskAction.ABANDON, new ActionExec(TaskStatus.ABANDONED, new NormalAbandonActionHandler()));

    }

    public static TaskStatus getNextStatus(TaskStatus currentStatus, TaskAction action) {
        ActionExec actionExec = ACTION_MAP.get(currentStatus, action);
        if (actionExec == null) {
            throw new ApiException(ResultCode.TASK_STATUS_FLOW_ERROR);
        }
        return actionExec.getNextStatus();
    }

    public static void exec(TaskStatus currentStatus, TaskAction action, TaskActionContext context) {
        ActionExec actionExec = ACTION_MAP.get(currentStatus, action);
        if (actionExec == null) {
            throw new ApiException(ResultCode.TASK_STATUS_FLOW_ERROR);
        }
        actionExec.getHandler().handle(context);
    }

}

其中ActionExec就是指定状态下的具体处理逻辑,handlerIActionHandler的具体实现类,他的入参是TaskActionContext,可以携带处理逻辑所需要的上下文信息

@FunctionalInterface
public interface IActionHandler {
    void handle(TaskActionContext context);
}

使用这种设计方式,就可以将原本service中的代码简化为下面的样子

    public void handleInstance(TaskFlowInstanceHandleReq req) {
        //TODO 任务鉴权

        //检查处理动作是否合法
        TaskAction action = Optional.ofNullable(TaskAction.getById(req.getActionId()))
                .orElseThrow(() -> new ApiException(ResultCode.PARAMETER_ILLEGAL));
        //检查当前任务是否存在
        TaskFlowInstance instance = Optional.ofNullable(this.getCurrentInstanceByTaskId(req.getTaskId()))
                .orElseThrow(() -> new ApiException(ResultCode.TASK_FLOW_INSTANCE_NOT_EXIST));

        TaskActionContext context = new TaskActionContext();
        context.setTaskId(req.getTaskId());
        context.setHandleComment(req.getHandleComment());
        context.setRelatedUserIdList(req.getRelatedUserIds());
        TaskStatusMachine.exec(instance.getStatus(), action, context);
    }

所有不同状态间的切换逻辑都是一个IActionHandler的实现类,这样还可以实现逻辑的复用,例如在任意状态上选择废弃的处理逻辑都是一样的,就可以直接将handler指定为同一个实现类

致谢

  • 星辰@Akimoto —— 感谢星辰的启发

参考