一、业务背景
营销自动化平台支持多种不同类型运营活动策略(比如:短信推送策略、微信图文推送策略、App Push 推送策略),每种活动类型都有各自不同的执行流程和活动状态。比如短信活动的活动执行流程如下:
(图 1-1:短信活动状态转移)
整个短信活动经历了 未开始 → 数据准备中 → 数据已就绪 → 活动推送中→ 活动结束 多个状态变更流程。不仅如此, 我们发现在活动业务逻辑处理过程中,都有以下类似的特点:
针对系统状态的流转管理,计算机领域有一套标准的理论方案模型——有限状态机。
二、理解状态机
2.1 状态机定义
有限状态机(Finite-State Machine , 缩写:FSM),简称状态机。是表示有限个状态以及这些状态之间的转移和触发动作的模型。
简而言之,状态机是由事件、状态、动作三大部分组成。三者的关系是:事件触发状态的转移,状态的转移触发后续动作的执行。其中动作不是必须的,也可以只进行状态转移,不进行任何操作。
(图 2-1:状态机组成)
所以将上述【图 1-1:短信活动状态转移 】使用状态机模型来描述就是:
(图 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 的更多内容可以了解:Martin Fowler《Domain Specific Languages》)。
2.2.3.2 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 本质上就是将状态转移过程用其他外部语言进行描述,比如使用 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@EnableStateMachinepublic 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 事件触发,推动状态机的自动流转。
优势
缺陷
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 是一款轻量级的状态机库,设计目标是为企业使用提供轻量级、高度灵活、可扩展、易于使用、类型安全和可编程的状态机实现。
优势
缺陷
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); } /** * 短信推送活动状态机初始化 */@Componentpublic 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 小结
三种开源状态机框架对比如下:
希望直接利用开源状态机能力的系统,可以根据自身业务的需求和流程复杂度,进行合适的选型。
四、营销自动化业务案例实践
4.1 设计选型
vivo 营销自动化的业务特点是:
针对以上业务特点,在实际项目开发中,我们是基于开源状态的实现方案——基于内部 DSL 的方式进行开发。同时汲取了以上开源框架的特点,选用了无状态高性能、功能简洁、支持动作异步执行的轻量设计。
4.2 核心流程
(图 4-1:核心流程设计)
4.3 实践思考
1)状态机配置可视化,结合外部 DSL 的方式(比如 JSON 的方式,存储到数据库中),支持更快速的配置。
2)目前只支持状态的简单流转,在流转过程加入流转接口扩展点,应对未来可能出现的复杂场景。
五、总结
状态机是由事件、状态、动作三大部分组成。三者的关系是:事件触发状态的转移,状态的转移触发后续动作的执行。利用状态机进行系统状态管理,可以提升业务扩展性和内聚性。状态机可以使用条件分支判断、状态模式和基于 DSL 来实现,其中更具表达性的 DSL 也是很多开源状态机的实现方式。可以基于开源状态机的特点和自身项目需求进行合适的选型,也可以基于前面的方案自定义状态机组件。
本篇是《营销自动化技术解密》系列专题文章的第三篇,系列文章回顾:
《营销自动化技术解密|开篇》
《设计模式如何提升 vivo 营销自动化业务拓展性|引擎篇01》
后面我们将继续带来系列专题文章的其他内容,每一篇文章都会对里面的技术实践进行详尽解析,敬请期待。
作者:vivo 互联网服务器团队-Chen Wangrong
评论