一、事务布景

营销主动化平台支撑多种不同类型运营活动战略(比方:短信推送战略、微信图文推送战略、App Push推送战略),每种活动类型都有各自不同的履行流程和活动情况。比方短信活动的活动履行流程如下:

状态机引擎在vivo营销自动化中的深度实践 | 引擎篇02

(图1-1:短信活动情况搬运)

整个短信活动经历了 未开端 → 数据预备中 → 数据已安排妥当 → 活动推送中→ 活动完毕 多个情况改变流程。不仅如此, 咱们发现在活动事务逻辑处理进程中,都有以下类似的特色:

  • 每增加一种新的活动事务类型,就要新增相应的活动情况以及处理情况改变的逻辑;

  • 当一个类型的活动事务流程有修改时,可能需求对原先的情况搬运进程进行改变;

  • 当每个事务都各自编写自己的情况搬运的事务代码时,中心事务逻辑和操控逻辑耦合性会非常强,扩展性差,本钱也很高。

针对体系情况的流通管理,核算机范畴有一套规范的理论计划模型——有限情况机。

二、理解情况机

2.1 情况机界说

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

  • 情况是描绘体系方针在某个时刻所处的情况。

  • 搬运指示情况改变,一般是经过外部事情为条件触发情况的搬运。

  • 动作是对给定情况下要进行的操作。

简而言之,情况机是由事情、情况、动作三大部分组成。三者的联系是:事情触发情况的搬运,情况的搬运触发后续动作的履行。其间动作不是必须的,也能够只进行情况搬运,不进行任何操作。

状态机引擎在vivo营销自动化中的深度实践 | 引擎篇02

(图2-1:情况机组成)

所以将上述【图1-1:短信活动情况搬运 】运用情况机模型来描绘便是:

状态机引擎在vivo营销自动化中的深度实践 | 引擎篇02

(图2-2:短信活动情况机)

情况机本质上是对体系的一种数学建模,将问题解决计划体系化表达出来。下面咱们来看下在实际开发中有哪些完结情况机的办法 。

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条件分支法,当事务情况流程新增或修改时,影响粒度更小,范围可控,扩展性更强。

缺点

同样难以应对事务流程情况搬运杂乱的场景,此场景下运用情况形式会引入非常多的情况类和办法,当情况逻辑有改变时,代码也会变得难以保护。

能够看到,尽管以上两种办法都能够完结情况机的触发、搬运、动作流程,可是复用性都很低。假如想要构建一个能够满意绝大部分事务场景的笼统情况机组件,是无法满意的。

2.2.3 依据DSL的完结

2.2.3.1 DSL 介绍

DSL 全称是 Domain-Specific Languages,指的是针对某一特定范畴,具有受限表达性的一种核算机程序规划言语。不同于通用的编程言语,DSL只用在某些特定的范畴,聚集于解决该范畴体系的某块问题。DSL通常分为 内部 DSL ( Internal DSLs ),外部 DSL ( external DSLs ) 。

  • 内部DSL :依据体系的宿主言语,由宿主言语进行编写和处理的 DSL,比方:依据 Java 的 内部 DSL 、依据 C++ 的内部 DSL 、依据 Javascript 的 内部 DSL 。

  • 外部DSL :不同于体系宿主言语,由自界说言语或许其他编程言语编写并处理的 DSL,有独立的解析器。比方:正则表达式、XML、SQL、HTML 等。

(有关DSL的更多内容能够了解:Martin Fowler《Domain Specific Languages》)。

2.2.3.2 DSL 的选型和情况机完结

运用DSL作为开发工具,能够用更加明晰和更具表达性的形式来描绘体系的行为。DSL 也是现在完结情况机比较推荐的办法,能够依据本身的需求选用内部 DSL 或许外部DSL 来完结。

  • 内部 DSL :事务体系假如只期望经过代码直接进行情况机的装备,那么能够挑选运用内部 DSL,特色是简略直接,不需求依靠额定的解析器和组件。

Java 内部 DSL 一般是使用 Builder Pattern 和 Fluent Interface 办法(Builder 形式和流式接口),完结示例:

StateMachineBuilder builder = new StateMachineBuilder();
                     builder.sourceState(States.STATE1)
                            .targetState(States.STATE2)
                            .event(Events.EVENT1)
                            .action(action1());
  • 外部 DSL :能够使用外部存储和通用脚本言语的解析能力,完结运行时动态装备、支撑可视化装备和跨言语应用场景。

外部 DSL 本质上便是将情况搬运进程用其他外部言语进行描绘,比方运用 XML 的办法:

<state id= "STATE1">
  <transition event="EVENT1"  target="STATE2">
    <action method="action1()"/>
  </transition>
</state>
<state id= "STATE2">
</state>

外部 DSL 一般放在装备文件或许数据库等外部存储中,经过对应的文本解析器,就能够将外部 DSL 的装备解析成类似内部 DSL 的模型,进行流程处理;一起由于外部存储的独立性和持久性,能够很便利地支撑运行时动态改变和可视化装备。

Java开源的情况机结构根本上都是依据DSL的完结办法。

三、开源情况机结构

咱们别离运用三种开源情况机结构来完结短信活动情况流经进程。

3.1 Spring Statemachine

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 是 Spring 官方的产品,具有强壮生态社区。

  • 功用非常完备,除了支撑根本的情况机装备外,还具备可嵌套的子情况机、依据zk的分布式情况机和外部存储持久化等丰厚的功用特性。

缺点

  • Spring Statemachine 在每个 statemachine 实例内部保存了当时情况机上下文相关的属性,也便是说是有情况的(这一点从触发情况机流通只需事情作为参数也能够看出来),所以运用单例形式的情况机实例不是线程安全的。要确保线程安全性只能每次经过工厂形式创立一个新的情况机实例,这种办法在高并发场景下,会影响体系全体功用。

  • 代码层次结构稍显杂乱,二次开发改造本钱大,一般场景下也并不需求运用如此多的功用,运用时观感上显得比较沉重。

3.2 Squirrel Foundation

public class SmsStatemachineSample {
    // 1. 情况界说
     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;
        }
    }
    // 2. 事情界说
    enum ActEvent {
        ACT_BEGIN, FINISH_DATA_CAL,FINISH_DATA_PREPARE,FINISH_DATA_PUSHING
    }
    // 3. 情况机上下文
    class StatemachineContext {
    }
    @StateMachineParameters(stateType=ActivityState.class, eventType=ActEvent.class, contextType=StatemachineContext.class)
    static class SmsStatemachine extends AbstractUntypedStateMachine {
        protected void notice(ActivityState from, ActivityState to, ActEvent event, StatemachineContext context) {
            System.out.println("【改变前情况】:"+from+";【改变后情况】:"+to);
        }
    }
    public static void main(String[] args) {
        // 4. 构建情况搬运
        UntypedStateMachineBuilder builder = StateMachineBuilderFactory.create(SmsStatemachine.class);
        builder.externalTransition().from(ActivityState.NOT_START).to(ActivityState.DATA_PREPARING).on(ActEvent.ACT_BEGIN).callMethod("notice");
        builder.externalTransition().from(ActivityState.DATA_PREPARING).to(ActivityState.DATA_PREPARED).on(ActEvent.FINISH_DATA_CAL).callMethod("notice");
        builder.externalTransition().from(ActivityState.DATA_PREPARED).to(ActivityState.DATA_PUSHING).on(ActEvent.FINISH_DATA_PREPARE).callMethod("notice");
        builder.externalTransition().from(ActivityState.DATA_PUSHING).to(ActivityState.FINISHED).on(ActEvent.FINISH_DATA_PUSHING).callMethod("notice");
        // 5. 触发情况机流通
        UntypedStateMachine fsm = builder.newStateMachine(ActivityState.NOT_START);
        fsm.fire(ActEvent.ACT_BEGIN,  null);
        fsm.fire(ActEvent.FINISH_DATA_CAL, null);
        fsm.fire(ActEvent.FINISH_DATA_PREPARE, null);
        fsm.fire(ActEvent.FINISH_DATA_PUSHING, null);
     }
}

squirrel-foundation 是一款轻量级的情况机库,规划方针是为企业运用提供轻量级、高度灵敏、可扩展、易于运用、类型安全和可编程的情况机完结。

优势

  • 和方针理念一致,与 Spring Statemachine 相比,不依靠于spring结构,规划完结方面更加轻量,尽管也是有情况的规划,可是创立情况机实例开销较小,功用上也更加简练,相比照较适合二次开发。

  • 对应的文档和测验用例也比较丰厚,开发者上手简略。

缺点

  • 过于强调“约好优于装备”的理念,不少默许性的处理,比方情况搬运后动作是经过办法名来调用,不利于操作管理。

  • 社区活跃度不高。

3.3 Cola Statemachine

/**
 * 情况机工厂类
 */
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开源结构里边的一款情况机结构,和前面两者最大的不同便是:无情况的规划——触发情况机流通时需求把当时情况作为入参,情况机实例中不需求保存当时情况上下文消息,只要一个情况机实例,也就直接确保了线程安全性和高功用。

优势

  • 轻量级无情况,安全,功用高。

  • 规划简练,便利扩展。

  • 社区活跃度较高。

缺点

  • 不支撑嵌套、并行等高档功用。

3.4 小结

三种开源情况机结构比照方下:

状态机引擎在vivo营销自动化中的深度实践 | 引擎篇02

期望直接使用开源情况机能力的体系,能够依据本身事务的需求和流程杂乱度,进行适宜的选型。

四、营销主动化事务事例实践

4.1 规划选型

vivo营销主动化的事务特色是:

  • 运营活动类型多,事务流量大,流程相对简略,功用要求高。

  • 流程改变频频,常常需求新增事务情况,需求支撑快速新增装备和改变。

  • 在情况触发后会有多种不同的事务操作,比方情况改变后的消息提醒,情况完结后的事务处理等,需求支撑异步操作和便利扩展。

针对以上事务特色,在实际项目开发中,咱们是依据开源情况的完结计划——依据内部DSL的办法进行开发。一起汲取了以上开源结构的特色,选用了无情况高功用、功用简练、支撑动作异步履行的轻量规划。

状态机引擎在vivo营销自动化中的深度实践 | 引擎篇02

  • 无情况高功用:确保高功用,采用无情况的情况机规划,只需求一个情况机实例就能够进行运转。

  • 功用简练:最小规划准则,只保存中心的规划,比方事情触发,情况的根本流通,后续的操作和上下文参数处理。

  • 动作异步履行:针对异步事务流程,采用线程池或许消息队列的办法进行异步解耦。

4.2 中心流程

  • 沿用开源情况机的内部DSL流式接口规划,在应用启动时扫描情况机界说;

  • 创立异步处理线程池支撑事务的后置动作;

  • 解析情况机的DSL装备,初始化情况机实例;

  • 构建履行上下文,存放各个情况机的实例和其他履行进程信息;

  • 情况机触发时,依据触发条件和当时情况,主动匹配搬运进程,推动情况机流通;

  • 履行后置同步/异步处理操作。

状态机引擎在vivo营销自动化中的深度实践 | 引擎篇02

(图4-1:中心流程规划)

4.3 实践思考

1)情况机装备可视化,结合外部DSL的办法(比方JSON的办法,存储到数据库中),支撑更快速的装备。

2)现在只支撑情况的简略流通,在流经进程参加流通接口扩展点,应对未来可能出现的杂乱场景。

五、总结

情况机是由事情、情况、动作三大部分组成。三者的联系是:事情触发情况的搬运,情况的搬运触发后续动作的履行。使用情况机进行体系情况管理,能够提升事务扩展性和内聚性。情况机能够运用条件分支判别、情况形式和依据DSL来完结,其间更具表达性的DSL也是很多开源情况机的完结办法。能够依据开源情况机的特色和本身项目需求进行适宜的选型,也能够依据前面的计划自界说情况机组件。

本篇是《营销主动化技能解密》系列专题文章的第三篇,系列文章回顾:

《营销主动化技能解密|开篇》

《规划形式怎么提升 vivo 营销主动化事务拓展性|引擎篇01》

后边咱们将继续带来系列专题文章的其他内容,每一篇文章都会对里边的技能实践进行翔实解析,敬请期待。

作者:vivo互联网服务器团队-Chen Wangrong