【Alibaba Cola 状态机】重点解析以及实践案例

【Alibaba Cola 状态机】重点解析以及实践案例

1. 状态模式

状态模式是一种行为型设计模式,允许对象在内部状态改变时改变其行为,简单地讲就是,一个拥有状态的context对象,在不同状态下,其行为会发生改变。看起来是改变了对象各个接口方法的实现一样。

模式中包含角色:

  1. 上下文
  2. 抽象状态
  3. 具体状态

优点包括解耦客户端和状态对象,可扩展性强,避免大量条件语句。

缺点是可能增加系统类的数量和复杂性。

适用场景如自动售货机的状态转换、线程状态管理等。代码示例展示了自动售卖机如何利用状态模式实现不同状态的切换。

推荐文章:23种设计模式之状态模式(State Pattern)_状态模式哪种状态切换好-CSDN博客

2. 状态机

状态机(Finite State Machine,简称FSM)是一个数学模型,用于描述对象在其生命周期中可能的状态以及状态之间的转换。

它由一组状态、一组事件、一组转换规则和一组动作组成,能够清晰地表示和管理对象的行为和状态变化。

在状态机中,"有限"指的是状态和事件都是有限的,即存在一个有限的状态集和事件集。这使得状态机具有明确的、可控的行为模式,便于分析和实现。

状态机的组成元素:状态、事件、转换、动作

状态(State): 表示对象的当前情况或条件。状态机中的状态是有限的,例如:待机、运行、暂停、停止等。

事件(Event): 触发状态转换的输入或信号。事件可以是外部输入、内部触发或其他系统生成的信号。

转换(Transition): 定义了状态之间的切换规则。当特定的事件发生时,状态机会根据转换规则从一个状态转换到另一个状态。

动作(Action): 在状态转换过程中执行的操作或任务。动作可以是状态进入前执行的预处理、状态转换时执行的中间操作或状态退出后执行的清理工作。

状态机的优缺点

优点:

  1. 逻辑清晰:状态机将复杂的系统行为分解为简单的状态和状态之间的转换,使系统逻辑更加清晰和易于理解。
  2. 可维护性高:由于状态机的逻辑是模块化的,任何状态的修改或扩展只需在该状态的实现部分进行,不会影响其他部分,增强了代码的可维护性。
  3. 可扩展性强:新的状态和转换可以方便地添加到现有的状态机中,使系统具有良好的扩展性。

缺点:状态爆炸,对于复杂系统,状态和转换的数量可能非常庞大,导致状态机图变得复杂难以管理,这被称为"状态爆炸"问题。

3. Alibaba Cola 状态机

实现一个状态机引擎,教你看清DSL的本质_状态机 dsl-CSDN博客

相比Spring statemachine状态机等的复杂,功能多;

但是我们 实际业务员 需要常用的功能,简单使用,所以这类就显得不简洁;再看cola-statemachine相比就是小巧、无状态、简单、轻量、性能极高的状态机DSL实现,解决业务中的状态流转问题。

如果是实现业务的话,阿里状态机是不二之选,更加适合我们日常开发!KISS(Keep It Simple and Stupid)

而如果是比较复杂的业务或者组件开发,这个状态机可能也能 妙用进行应对 ,面对不了可能就得用 Spring statemachine 有状态的状态机,或者对阿里状态机进行 二次开发

开源者的初心就是,让开发更舒适,结合实际需求抽离出必要部分,而不是严格按照传统状态模式,传统状态机

无状态状态机--cola stateMachine-CSDN博客

状态机本身没有状态,而是提供一个状态机单例,通过输入参数 "起始状态"、"事件"、"上下文",这些参数足以确定一次状态轮转,期间通过 condition 和 action 后,返回"最终状态",若轮转失败,则返回原状态;

由于没有状态,所以调用者使用这个状态机都是互不干扰的,通过输入决定输出(一次轮转),做到一个实例服务多个调用者

  • 这里的上下文,就是过程中的通行数据罢了
  • 你也可以在上下文里面去设置状态,像状态模式那样,当其实在使用状态机的时候,状态机就相当于状态模式的上下文,这样看,发挥状态机的优势,就没必要设置了。

采用了无状态设计之后,我们就可以使用一个状态机 Instance 来响应所有的请求了,性能会大大的提升

原本的状态机是有状态的,状态机当前的状态决定了其行为,这样导致每次请求,都是申请一个新的状态机去维护状态的轮转,轮转的过程,状态机的状态也在时刻发生变化;

当阿里状态机理念是状态机每次进行一次轮转就行了,哪怕需要连续的过程性的轮转,多次请求状态机即可(一个轮转的 action 嵌套执行外部轮转的事件,返回的最终状态可能与实际不符,最好嵌套执行内部轮转而不是外部轮转;或者等轮转结束,再按照这次请求的响应进行下次轮转)
有状态的状态机就相当于可以本地运行的应用,不同客户之间是隔离的,状态在应用内部轮转

无状态的状态机就相当于 web 应用,所有客户访问同一个 web,请求一次,执行一次特定的轮转,响应结果

4. Alibaba Cola 状态机使用

源码其实不难,大部分还是容易看懂的,如果出现问题按照思路可以调试去查查,这里就是我总结的用法。

java 复制代码
/**
 * Alibaba Cola 状态机关键词
 * State:状态
 * Event:事件,状态由事件触发,引起变化
 * Transition:流转,表示从一个状态到另一个状态
 * External Transition:外部流转,两个不同状态之间的流转
 * Internal Transition:内部流转,同一个状态之间的流转
 * Condition:条件,表示是否允许到达某个状态
 * Action:动作,到达某个状态之后,可以做什么
 * StateMachine:状态机
 */

4.1 StateMachineUtil(核心方法封装)

java 复制代码
@Slf4j
public class StateMachineUtil {

    public static <S, E, C> StateMachine<S, E, C> getMachine(String machineId) {
        return StateMachineFactory.get(machineId);
    }

    public static void showMachine(String machineId) {
        getMachine(machineId).showStateMachine();
    }

    public static String generatePlantUML(String machineId) {
        return getMachine(machineId).generatePlantUML();
    }

    public static <S, E, C> void printMachine(String machineId) {
        StateMachine<S, E, C> stateMachine = getMachine(machineId);
        stateMachine.showStateMachine();
        System.out.println(stateMachine.generatePlantUML());
    }

    public static <S, E, C> S fireEvent(String machineId, S state, E event, C context) {
        return (S) getMachine(machineId).fireEvent(state, event, context);
    }
    
}
  1. 通过 machineId 获得状态机实例:StateMachineFactory.get(machineId)
  2. 打印状态机(注意重写 toString 方法到想要的效果)
  3. 执行状态机(machineId,起始状态,事件,上下文)(返回最终状态)

4.2 状态机准备信息

根据状态机轮转所需的信息,也就是准备信息,在状态机构建过程中需要用到;

我将其抽象成接口,有两个:外部流转助手、内部流转助手

  • 状态的事件会让状态变成别的状态,还是不变就是这里外和内
java 复制代码
public interface StateExternalTransitionHelper<S, E, C> {

    List<S> getFromState();

    S getToState(S from) throws GlobalServiceException;

    E getOnEvent();

    Condition<C> getWhenCondition();

    Action<S, E, C> getPerformAction();

}

from 为 list 是因为有时候同一事件,多个状态都可以执行,其中 getToState 通过 from 得知 to,并允许抛出异常;

java 复制代码
public interface StateInternalTransitionHelper<S, E, C> {

    List<S> getWithinList();

    E getOnEvent();

    Condition<C> getWhenCondition();

    Action<S, E, C> getPerformAction();
}

4.3 通过准备信息构造状态机

外部流转助手、内部流转助手 的实现类,就蕴含了准备信息,将这些在状态机构造过程中应用:

java 复制代码
private static <S, E, C> void builderAssign(StateMachineBuilder<S, E, C> builder,
                                            StateExternalTransitionHelper<S, E, C> helper) {
    List<S> fromStateList = helper.getFromState();
    if(!CollectionUtil.isEmpty(fromStateList)) {
        E onEvent = helper.getOnEvent();
        Condition<C> whenCondition = helper.getWhenCondition();
        Action<S, E, C> performAction = helper.getPerformAction();
        fromStateList.forEach(fromState -> {
            try {
                S toState = helper.getToState(fromState); // 若抛异常就忽略这一个,构造下一个状态轮转
                // 不保证每个 toState 相等的情况下,不用 externalTransitions 与 fromAmong
                builder.externalTransition()
                    .from(fromState)
                    .to(toState)
                    .on(onEvent)
                    .when(whenCondition)
                    .perform(performAction);
            } catch (GlobalServiceException e) {
                log.warn(e.getMessage());
            }
        });
    }
}

private static <S, E, C> void builderAssign(StateMachineBuilder<S, E, C> builder,
                                            StateInternalTransitionHelper<S, E, C> helper) {
    List<S> withinList = helper.getWithinList();
    if(!CollectionUtil.isEmpty(withinList)) {
        E onEvent = helper.getOnEvent();
        Condition<C> whenCondition = helper.getWhenCondition();
        Action<S, E, C> performAction = helper.getPerformAction();
        withinList.forEach(within -> {
            builder.internalTransition()
                .within(within)
                .on(onEvent)
                .when(whenCondition)
                .perform(performAction);
        });
    }
}

public static <S, E, C> void buildMachine(String machineId,
                                          List<? extends StateExternalTransitionHelper<S, E, C>> externalHelpers,
                                          List<? extends StateInternalTransitionHelper<S, E, C>> internalHelpers) {
    // 创建一个 builder
    StateMachineBuilder<S, E, C> builder = StateMachineBuilderFactory.create();
    // 添加轮转
    externalHelpers.forEach(helper -> {
        builderAssign(builder, helper);
    });
    internalHelpers.forEach(helper -> {
        builderAssign(builder, helper);
    });
    // 创建状态机
    builder.build(machineId);
}

4.4 示例(简历状态轮转)

不要直接实现那两个接口,要先用模块内部的接口继承,这样注入 Spring 容器中的 bean,获取的时候指定我们自己的接口类型,防止获得别的模块的 helper bean

java 复制代码
public interface ResumeStateExternalTransitionHelper extends StateExternalTransitionHelper<ResumeStatus, ResumeEvent, ResumeContext> {

}
java 复制代码
public interface ResumeStateInternalTransitionHelper extends StateInternalTransitionHelper<ResumeStatus, ResumeEvent, ResumeContext> {

}

上下文:

java 复制代码
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Slf4j
public class ResumeContext {

    private Long managerId;

    private StuResume resume;

    ResumeExecuteDTO executeDTO;

    public void log(ResumeStatus from, ResumeStatus to, ResumeEvent event) {
        log.info("resume state from {} to {} run {} currentResume {} managerId {} executeDTO {}",
                from, to, event, resume.getId(), managerId, executeDTO);
    }

}

简历状态:

java 复制代码
@Getter
public enum ResumeStatus {
    DRAFT("草稿", 0),

    PENDING_SELECTION("待筛选", 1),
    REJECTED("筛选不通过", 2),

    SCHEDULE_INITIAL_INTERVIEW("待安排初试", 3),
    PENDING_INITIAL_INTERVIEW("待初试", 4),
    INITIAL_INTERVIEW_PASSED("初试通过", 5), // 仅当初试为最后一个流程时显示
    INITIAL_INTERVIEW_FAILED("初试不通过", 6), // 仅当初试为最后一个流程时显示

    SCHEDULE_SECOND_INTERVIEW("待安排复试", 7),
    PENDING_SECOND_INTERVIEW("待复试", 8),
    SECOND_INTERVIEW_PASSED("复试通过", 9), // 仅当复试为最后一个流程时显示
    SECOND_INTERVIEW_FAILED("复试不通过", 10), // 仅当复试为最后一个流程时显示

    SCHEDULE_FINAL_INTERVIEW("待安排终试", 11),
    PENDING_FINAL_INTERVIEW("待终试", 12),
    FINAL_INTERVIEW_PASSED("终试通过", 13), // 仅当复试为最后一个流程时显示
    FINAL_INTERVIEW_FAILED("终试不通过", 14), // 仅当复试为最后一个流程时显示

    PENDING_HANDLING("待处理", 15),
    SUSPENDED("挂起", 16),

    ;

    ResumeStatus(String message, Integer code) {
        this.message = message;
        this.code = code;
    }

    @Override
    public String toString() {
        return message;
    }

    private final String message;

    @EnumValue
    @JsonValue
    private final Integer code;

    public static ResumeStatus get(Integer code) {
        for (ResumeStatus resumeStatus : ResumeStatus.values()) {
            if(resumeStatus.getCode().equals(code)) {
                return resumeStatus;
            }
        }
        throw new GlobalServiceException(GlobalServiceStatusCode.USER_RESUME_STATUS_EXCEPTION);
    }
}

简历事件:

java 复制代码
@Getter
public enum ResumeEvent {

    NEXT(1, "推进"),

    APPROVE(2, "通过"),

    ELIMINATE(3, "淘汰"),

    RESET(4, "重置"),

    PENDING(5, "待处理"),

    SUSPEND(6, "挂起"),

    CONFIRM(7, "转正"),

    ;

    @Override
    public String toString() {
        return description;
    }

    private final Integer event;

    private final String description;

    ResumeEvent(Integer event, String description) {
        this.event = event;
        this.description = description;
    }

    public static ResumeEvent get(Integer event) {
        for (ResumeEvent resumeEvent : ResumeEvent.values()) {
            if(resumeEvent.getEvent().equals(event)) {
                return resumeEvent;
            }
        }
        throw new GlobalServiceException(GlobalServiceStatusCode.USER_RESUME_STATUS_TRANS_EVENT_ERROR);
    }

}

你也可以不是枚举类,但是我们要提供有限的固定的实例,并且代表状态和事件,枚举太适用了!

常量类:

java 复制代码
public interface ResumeStateMachineConstants {

    String RESUME_STATE_MACHINE_ID = "resumeStateMachineId";

}

配置类:

java 复制代码
@Configuration
@RequiredArgsConstructor
public class ResumeStateMachineBuildConfig {

    private final List<ResumeStateExternalTransitionHelper> externalHelpers;

    private final List<ResumeStateInternalTransitionHelper> internalHelpers;

    @PostConstruct
    public void buildInterviewMachine() {
        StateMachineUtil.buildMachine(
                ResumeStateMachineConstants.RESUME_STATE_MACHINE_ID,
                externalHelpers,
                internalHelpers
        );
        StateMachineUtil.printMachine(ResumeStateMachineConstants.RESUME_STATE_MACHINE_ID);
    }

}

一个 helper 实现展示:

java 复制代码
@Component
@RequiredArgsConstructor
public class ResumeNextStateHelper implements ResumeStateExternalTransitionHelper {

    private final Condition<ResumeContext> defaultResumeCondition;

    private final Action<ResumeStatus, ResumeEvent, ResumeContext> defaultResumeAction;
    
    @Override
    public List<ResumeStatus> getFromState() {
        return List.of(
                DRAFT,
                PENDING_SELECTION,
                SCHEDULE_INITIAL_INTERVIEW,
                PENDING_INITIAL_INTERVIEW,
                SCHEDULE_SECOND_INTERVIEW,
                PENDING_SECOND_INTERVIEW,
                SCHEDULE_FINAL_INTERVIEW
        );
    }

    @Override
    public ResumeStatus getToState(ResumeStatus from) throws GlobalServiceException {
        return switch (from) {
            case DRAFT -> PENDING_SELECTION;
            case PENDING_SELECTION -> SCHEDULE_INITIAL_INTERVIEW;
            case SCHEDULE_INITIAL_INTERVIEW -> PENDING_INITIAL_INTERVIEW;
            case PENDING_INITIAL_INTERVIEW -> SCHEDULE_SECOND_INTERVIEW;
            case SCHEDULE_SECOND_INTERVIEW -> PENDING_SECOND_INTERVIEW;
            case PENDING_SECOND_INTERVIEW -> SCHEDULE_FINAL_INTERVIEW;
            case SCHEDULE_FINAL_INTERVIEW -> PENDING_FINAL_INTERVIEW;
            default -> throw new GlobalServiceException(GlobalServiceStatusCode.USER_RESUME_STATUS_EXCEPTION);
        };
    }

    @Override
    public ResumeEvent getOnEvent() {
        return ResumeEvent.NEXT;
    }

    @Override
    public Condition<ResumeContext> getWhenCondition() {
        return defaultResumeCondition;
    }

    @Override
    public Action<ResumeStatus, ResumeEvent, ResumeContext> getPerformAction() {
        return defaultResumeAction;
    }
}

这里这两个是我写的默认值,并注入了容器,这样的写法,需要注意 bean 的名称不要冲突了

通过以上代码,在项目启动的时候即可构造出状态机:

执行状态机(代码片段):

我个人觉得没必要在状态机内部将状态落库,根据请求状态机的响应,进行落库

5. 小思考 · 状态机设计的合理性

状态机并不是通过 from 和 to 确定 event,而是 from 和 event 确定轮转

通过 from 到 to 的变化,是确定不了事件的,实现通过 from 到 to 的变化触发某一事件,事件可能不止一个,而且并不保证触发我们想要的那一个

比如一次请求,对象的当前属性就是 from,请求是以 event 作为参数,还是以 to 作为参数

① 若是 event 作为参数,那么根据 fromevent 即可进行状态轮转,并返回最终的状态

② 若是 to 作为参数,fromto 的这个状态变化会触发什么特定行为

状态机更注重的是状态的轮转,也就是 ①,而 ② 是状态变化的后置行为

以"一场面试"为例,一场面试的状态可以是: 未开始进行中已结束

如果是 ①,传入的参数可以是 "开始面试" 这一具体事件,当前面试状态是 "未开始",就可以轮转成"进行中",也可以是 "通知用户" 这一事件,状态内部流转的过程中对用户进行面试通知;

如果是 ②,传入的参数可以是 "进行中",面试状态将直接更新成"进行中",状态变化触发对应的事件,但做不了内部轮转

① 这种方式靠的是状态机,让请求行为更加具体,如进行"开始面试","通知用户"这种具体的行为

而 ② 更加狭隘,如果我们设计一份代码来管理 fromto 触发的事件,如果要符合我们的接口预期 ,这往往可能 并不能做到 ① 的可读性高、灵活性高、通用性高、可扩展性高

用 ② 来进行状态轮转不太合适,但是也不是完全没用,更适合用其管理一些状态变化固定的"副作用",作为状态轮转的后置行为,还是不错的!可以结合责任链模式去实现

但是又说回来,fromto 的转变的副作用,在编写代码的时候应该也就是规定对应事件的副作用吧,比如 "未开始" 到 "进行中",就是"面试开始" 这一具体行为的副作用,那其实在状态机就能实现啊;

也就是说 ② 这种方式适合,有多个事件可以进行 fromto 的轮转,并且这些事件都有共同的副作用,这个副作用就可以用 ② 来管理,出现这种情况也很极端了~

在比较简单的系统,fromto 能够 可以确定唯一的事件,没有内部轮转的需求 ,用这个也无所谓

如果非要 ② 这种请求方式,不想指定 event,出现特殊的状态变化,就得自动触发对应事件,这种就属于特殊需求了, fromto 触发的事件的映射关系,也只能在 to 已知的情况下可以复用,甚至只用于这一特殊请求,代码写得可能比较死,不太优雅🙁,太为难了(;′⌒`),反正我不是很喜欢,我个人比较喜欢优雅直观的设计;

我个人认为要是有个接口可以任意改变状态,那么应该不需要触发什么特定事件了,毕竟都允许可以任意更改了,或者说有一个统一的事件

可以定义一个"任意更改状态"的事件,上下文携带 toState,所有状态都可以触发,在状态机的内部/外部轮转(状态的行为)即可

但这不符合状态机明确的设计理念

综上所述,

用 ① 更加合适,可以让请求行为更加优雅具体直观和灵活,开发低耦合更具有扩展性,能实现的功能更多;② 的这种请求方式适合那种状态变化没有任何事件触发的场景,或触发的都是同一事件的场景,等不具有状态轮转的过程概念的场景。

BUT,

规矩是死的,视具体需求而论,切勿只纸上谈兵!

以上也只是我的想法,不同人有不同的想法和理念!

状态机还可以结合很多其他的设计模式,更多妙用等你开发!

状态机你爱咋用咋用,只要合理都 OK 啦!

开发是灵活的过程,开发者的理念为准,没有固定的强限定,但是我们要满足其基本的优秀写法!

相关推荐
奋进的芋圆1 小时前
Java 延时任务实现方案详解(适用于 Spring Boot 3)
java·spring boot·redis·rabbitmq
sxlishaobin1 小时前
设计模式之桥接模式
java·设计模式·桥接模式
model20051 小时前
alibaba linux3 系统盘网站迁移数据盘
java·服务器·前端
荒诞硬汉2 小时前
JavaBean相关补充
java·开发语言
提笔忘字的帝国2 小时前
【教程】macOS 如何完全卸载 Java 开发环境
java·开发语言·macos
2501_941882482 小时前
从灰度发布到流量切分的互联网工程语法控制与多语言实现实践思路随笔分享
java·开发语言
華勳全栈3 小时前
两天开发完成智能体平台
java·spring·go
alonewolf_993 小时前
Spring MVC重点功能底层源码深度解析
java·spring·mvc
沛沛老爹3 小时前
Java泛型擦除:原理、实践与应对策略
java·开发语言·人工智能·企业开发·发展趋势·技术原理
专注_每天进步一点点3 小时前
【java开发】写接口文档的札记
java·开发语言