【设计模式】状态模式以及开源状态机框架的实践

一、业务背景

营销自动化平台支持多种不同类型运营活动策略(比如:短信推送策略、微信图文推送策略、App Push推送策略),每种活动类型都有各自不同的执行流程和活动状态。比如短信活动的活动执行流程如下:
在这里插入图片描述

整个短信活动经历了 未开始 → 数据准备中 → 数据已就绪 → 活动推送中→ 活动结束 多个状态变更流程。不仅如此, 我们发现在活动业务逻辑处理过程中,都有以下类似的特点:

每增加一种新的活动业务类型,就要新增相应的活动状态以及处理状态变更的逻辑;

当一个类型的活动业务流程有修改时,可能需要对原先的状态转移过程进行变更;

当每个业务都各自编写自己的状态转移的业务代码时,核心业务逻辑和控制逻辑耦合性会非常强,扩展性差,成本也很高。

针对系统状态的流转管理,计算机领域有一套标准的理论方案模型——有限状态机。

二、理解状态机

2.1 状态机定义

有限状态机(Finite-State Machine , 缩写:FSM),简称状态机。是表示有限个状态以及这些状态之间的转移和触发动作的模型。

状态是描述系统对象在某个时刻所处的状况。

转移指示状态变更,一般是通过外部事件为条件触发状态的转移。

动作是对给定状态下要进行的操作。

简而言之,状态机是由事件、状态、动作三大部分组成。三者的关系是:事件触发状态的转移,状态的转移触发后续动作的执行。其中动作不是必须的,也可以只进行状态转移,不进行任何操作。

在这里插入图片描述

2.2 状态机的实现方式

2.2.1 基于条件判断的实现

这是最直接的一种实现方式,所谓条件判断就是通过使用 if-else 或 switch-case 分支判断进行硬编码实现。对于前面短信活动,基于条件判断方式的代码实例如下:

/**
  * 短信活动状态枚举
  */
public enum ActivityState {
    NOT_START(0), //活动未开始
    DATA_PREPARING(1), //数据准备中
    DATA_PREPARED(2), //数据已就绪
    DATA_PUSHING(3), //活动推送中
    FINISHED(4); //活动结束
}
 
/**
  * 短信活动状态机
  */
public class ActivityStateMachine {
    //活动状态
    private ActivityState currentState;
    public ActivityStateMachine() {
        this.currentState = ActivityState.NOT_START;
    }
    /**
     * 活动时间开始
     */
    public void begin() {
        if (currentState.equals(ActivityState.NOT_START)) {
            this.currentState = ActivityState.DATA_PREPARING;
            //发送通知给运营人员
            notice();
        }
        // do nothing or throw exception ...
    }
 
    /**
     * 数据计算完成
     */
    public void finishCalData() {
        if (currentState.equals(ActivityState.DATA_PREPARING)) {
            this.currentState = ActivityState.DATA_PREPARED;
            //发送通知给运营人员
            notice();
        }
        // do nothing or throw exception ...
    }
 
     /**
     * 活动推送开始
     */
    public void beginPushData() {
        //省略
    }
     /**
     * 数据推送完成
     */
    public void finishPushData() {
        //省略
    }
}

通过条件分支判断来控制状态的转移和动作的触发,上述的 if 判断条件也可以换成 switch 语句,以当前状态为分支来控制该状态下可以执行的操作。

适用场景
适用于业务状态个数少或者状态间跳转逻辑比较简单的场景。

缺陷
当触发事件和业务状态之间对应关系不是简单的一对一时,就需要嵌套多个条件分支判断,分支逻辑会变得异常复杂;当状态流程有变更时,也需要改动分支逻辑,不符合开闭原则,代码可读性和扩展性非常差。

2.2.2 基于状态模式的实现

了解设计模式的童鞋,很容易就可以把状态机和状态模式这两个概念联系起来,状态模式其实可以作为状态机的一种实现方式。主要实现思路是通过状态模式将不同状态的行为进行分离,根据状态变量的变化,来调用不同状态下对应的不同方法。代码示例如下:

/**
   * 活动状态接口
   */
interface IActivityState {
    ActivityState getName();
    //触发事件
    void begin();
    void finishCalData();
    void beginPushData();
    void finishPushData();
}
 
 /**
   * 具体状态类—活动未开始状态
   */
public class ActivityNotStartState implements IActivityState {
    private ActivityStateMachine stateMachine;
    public ActivityNotStartState(ActivityStateMachine stateMachine) {
        this.stateMachine = stateMachine;
    }
 
    @Override
    public ActivityState getName() {
        return ActivityState.NOT_START;
    }
 
    @Override
    public void begin() {
        stateMachine.setCurrentState(new ActivityDataPreparingState(stateMachine));
        //发送通知
        notice();
    }
 
    @Override
    public void finishCalData() {
        // do nothing or throw exception ...
    }
    @Override
    public void beginPushData() {
        // do nothing or throw exception ...
    }
    @Override
    public void finishPushData() {
        // do nothing or throw exception ...
    }
}
 
 /**
   * 具体状态类—数据准备中状态
   */
public class ActivityDataPreparingState implements IActivityState {
    private ActivityStateMachine stateMachine;
    public ActivityNotStartState(ActivityStateMachine stateMachine) {
        this.stateMachine = stateMachine;
    }
 
    @Override
    public ActivityState getName() {
        return ActivityState.DATA_PREPARING;
    }
    @Override
    public void begin() {
        // do nothing or throw exception ...
    }
    public void finishCalData() {
        stateMachine.setCurrentState(new ActivityDataPreparedState(stateMachine));
        //TODO:发送通知
    }
   @Override
    public void beginPushData() {
        // do nothing or throw exception ...
    }
    @Override
    public void finishPushData() {
        // do nothing or throw exception ...
    }
}
    ...(篇幅原因,省略其他具体活动类)
 
 
 /**
   * 状态机
   */
public class ActivityStateMachine {
    private IActivityState currentState;
    public ActivityStateMachine(IActivityState currentState) {
        this.currentState = new ActivityNotStartState(this);
    }
    public void setCurrentState(IActivityState currentState) {
        this.currentState = currentState;
    }
    public void begin() {
        currentState.begin();
    }
    public void finishCalData() {
        currentState.finishCalData();
    }
    public void beginPushData() {
        currentState.beginPushData();
    }
    public void finishPushData() {
        currentState.finishCalData();
    }
}

状态模式定义了状态-行为的对应关系, 并将各自状态的行为封装在对应的状态类中。我们只需要扩展或者修改具体状态类就可以实现对应流程状态的需求。

适用场景

适用于业务状态不多且状态转移简单的场景,相比于前面的if/switch条件分支法,当业务状态流程新增或修改时,影响粒度更小,范围可控,扩展性更强。

三、开源状态机框架

3.1 Spring State Machine

enum ActivityState {
    NOT_START(0),
    DATA_PREPARING(1),
    DATA_PREPARED(2),
    DATA_PUSHING(3),
    FINISHED(4);
 
    private int state;
    private ActivityState(int state) {
        this.state = state;
    }
}
 
enum ActEvent {
    ACT_BEGIN, FINISH_DATA_CAL,FINISH_DATA_PREPARE,FINISH_DATA_PUSHING
}
 
@Configuration
@EnableStateMachine
public class StatemachineConfigurer extends EnumStateMachineConfigurerAdapter<ActivityState, ActEvent> {
    @Override
    public void configure(StateMachineStateConfigurer<ActivityState, ActEvent> states)
            throws Exception {
                states
                .withStates()
                .initial(ActivityState.NOT_START)
                .states(EnumSet.allOf(ActivityState.class));
    }
    @Override
    public void configure(StateMachineTransitionConfigurer<ActivityState, ActEvent> transitions)
            throws Exception {
                transitions
                .withExternal()
                .source(ActivityState.NOT_START).target(ActivityState.DATA_PREPARING)
                .event(ActEvent.ACT_BEGIN).action(notice())
                .and()
                .withExternal()
                .source(ActivityState.DATA_PREPARING).target(ActivityState.DATA_PREPARED)
                .event(ActEvent.FINISH_DATA_CAL).action(notice())
                .and()
                .withExternal()
                .source(ActivityState.DATA_PREPARED).target(ActivityState.DATA_PUSHING)
                .event(ActEvent.FINISH_DATA_PREPARE).action(notice())
                .and()
                .withExternal()
                .source(ActivityState.DATA_PUSHING).target(ActivityState.FINISHED)
                .event(ActEvent.FINISH_DATA_PUSHING).action(notice())
                .and() ;
    }
    @Override
    public void configure(StateMachineConfigurationConfigurer<ActivityState, ActEvent> config)
            throws Exception {
        config.withConfiguration()
                .machineId("ActivityStateMachine");
    }
    public Action<ActivityState, ActEvent> notice() {
        return context -> System.out.println("【变更前状态】:"+context.getSource().getId()+";【变更后状态】:"+context.getTarget().getId());
    }
 
   //测试类
   class DemoApplicationTests {
    @Autowired
    private StateMachine<ActivityState, ActEvent> stateMachine;
 
    @Test
    void contextLoads() {
        stateMachine.start();
        stateMachine.sendEvent(ActEvent.ACT_BEGIN);
        stateMachine.sendEvent(ActEvent.FINISH_DATA_CAL);
        stateMachine.sendEvent(ActEvent.FINISH_DATA_PREPARE);
        stateMachine.sendEvent(ActEvent.FINISH_DATA_PUSHING);
        stateMachine.stop();
    }
}

通过重写配置模板类的三个configure方法,通过流式Api形式完成状态初始化、状态转移的流程以及状态机的声明,实现Java内部DSL的状态机。外部使用状态机通过sendEvent事件触发,推动状态机的自动流转。

缺陷
Spring Statemachine 在每个 statemachine 实例内部保存了当前状态机上下文相关的属性,也就是说是有状态的(这一点从触发状态机流转只需事件作为参数也可以看出来),所以使用单例模式的状态机实例不是线程安全的。要保证线程安全性只能每次通过工厂模式创建一个新的状态机实例,这种方式在高并发场景下,会影响系统整体性能。

3.2 Cola State Machine

/**
 * 状态机工厂类
 */
public class StatusMachineEngine {
    private StatusMachineEngine() {
    }
    private static final Map<OrderTypeEnum, String> STATUS_MACHINE_MAP = new HashMap();
 
    static {
        //短信推送状态
        STATUS_MACHINE_MAP.put(ChannelTypeEnum.SMS, "smsStateMachine");
        //PUSH推送状态
        STATUS_MACHINE_MAP.put(ChannelTypeEnum.PUSH, "pushStateMachine");
        //......
    }
 
    public static String getMachineEngine(ChannelTypeEnum channelTypeEnum) {
        return STATUS_MACHINE_MAP.get(channelTypeEnum);
    }
 
   /**
     * 触发状态转移
     * @param channelTypeEnum
     * @param status 当前状态
     * @param eventType 触发事件
     * @param context 上下文参数
     */
    public static void fire(ChannelTypeEnum channelTypeEnum, String status, EventType eventType, Context context) {
        StateMachine orderStateMachine = StateMachineFactory.get(STATUS_MACHINE_MAP.get(channelTypeEnum));
        //推动状态机进行流转,具体介绍本期先省略
        orderStateMachine.fireEvent(status, eventType, context);
    }
 
/**
 * 短信推送活动状态机初始化
 */
@Component
public class SmsStateMachine implements ApplicationListener<ContextRefreshedEvent> {
 
    @Autowired
    private  StatusAction smsStatusAction;
    @Autowired
    private  StatusCondition smsStatusCondition;
 
    //基于DSL构建状态配置,触发事件转移和后续的动作
    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
        StateMachineBuilder<String, EventType, Context> builder = StateMachineBuilderFactory.create();
        builder.externalTransition()
                .from(INIT)
                .to(NOT_START)
                .on(EventType.TIME_BEGIN)
                .when(smsStatusAction.checkNotifyCondition())
                .perform(smsStatusAction.doNotifyAction());
        builder.externalTransition()
                .from(NOT_START)
                .to(DATA_PREPARING)
                .on(EventType.CAL_DATA)
                .when(smsStatusCondition.doNotifyAction())
                .perform(smsStatusAction.doNotifyAction());
        builder.externalTransition()
                .from(DATA_PREPARING)
                .to(DATA_PREPARED)
                .on(EventType.PREPARED_DATA)
                .when(smsStatusCondition.doNotifyAction())
                .perform(smsStatusAction.doNotifyAction());
        ...(省略其他状态)
        builder.build(StatusMachineEngine.getMachineEngine(ChannelTypeEnum.SMS));
    }
 
   //调用端
   public class Client {
     public static void main(String[] args){
          //构建活动上下文
          Context context = new Context(...);
         // 触发状态流转
          StatusMachineEngine.fire(ChannelTypeEnum.SMS, INIT, EventType.SUBMIT, context);
      }
   }
}

Cola Statemachine 是阿里COLA开源框架里面的一款状态机框架,和前面两者最大的不同就是:无状态的设计——触发状态机流转时需要把当前状态作为入参,状态机实例中不需要保留当前状态上下文消息,只有一个状态机实例,也就直接保证了线程安全性和高性能。
在这里插入图片描述

四、项目地址

https://gitee.com/charles_ruan/state-machine

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值