其实我们在编程时实现相关业务逻辑时经常需要处理各种事件和状态切换,写各种switch/case 和if/else ,所以我们其实可能一直都在跟有限状态机打交道,只是可能没有意识到。在处理一些业务逻辑比较复杂的需求时,可以先看看是否适合用一个有限状态机来描述,如果可以把业务模型抽象成一个有限状态机,那么代码就会逻辑特别清晰,结构特别规整。
下面我们就来聊聊所谓的状态机,以及它如何在代码中实现。
1、状态机的要素
状态(State):表示对象的某种形态,在当前形态下可能会拥有不同的行为和属性。
转移(Transition):表示状态变更,并且必须满足确使转移发生的条件来执行。
动作(Action):表示在给定时刻要进行的活动。
事件(Event):事件通常会引起状态的变迁,促使状态机从一种状态切换到另一种状态。
所有的状态转换都可以概括为:F(S, E) -> (A, S’),即如果当前状态为S,接收到一个事件E,则执行动作A,同时状态转换为下个状态S’。
- F(S) -> (A, S’) 型状态机:下一状态只由当前状态决定
- F(S, E) -> (A, S’) 型状态机:下一状态不但与当前状态有关,还与当前输入值有关
2、状态表的要素
现态:是指当前所处的状态。
条件:又称为“事件”。当一个条件被满足,将会触发一个动作,或者执行一次状态的迁移。
动作:条件满足后执行的动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。动作不是必需的,当条件满足后,也可以不执行任何动作,直接迁移到新状态。
次态:条件满足后要迁往的新状态。“次态”是相对于“现态”而言的,“次态”一旦被激活,就转变成新的“现态”了。
我们以视频转码过程(基于业务模型的猜想)用状态表表示整个过程,如下图所示。
现态|次态 | 文件上传 | 转码模板 | 视频转码 | 保存视频 |
文件上传 |
N/A |
N/A |
N/A |
N/A |
转码模板 |
触发条件(event):文件上传完成 执行动作(action):添加转码模板 |
触发条件(event):重新添加转码模板 执行动作(action):添加转码模板 |
触发条件(event):重新转码,上传转码模板 执行动作(action):添加转码模板 |
触发条件(event):重新转码,添加模板 执行动作(action):添加转码模板 |
视频转码 |
触发条件(event):不需要转码模板 执行动作(action):开始视频转码 |
触发条件(event):添加转码模板完成 执行动作(action):开始转码 |
触发条件(event):转码失败或重新转码 执行动作(action):开始转码 |
触发条件(event):重新转码 执行动作(action):开始转码 |
保存视频 |
触发条件(event):不需要转码 执行动作(action):保存视频文件 | N/A |
触发条件(event):转码完成 执行动作(action):保存视频 | N/A |
2、简单状态机实现
代码结构:
Action
public interface Action {
boolean action(Context context);
}
Action实现类
public class AddCodeTemplateAction implements Action {
@Override
public boolean action(Context context) {
//TODO
return false;
}
}
public class SaveFileAction implements Action {
@Override
public boolean action(Context context) {
//TODO
return false;
}
}
public class TranslateCodeAction implements Action {
@Override
public boolean action(Context context) {
//TODO
return false;
}
}
public class UploadFileAction implements Action {
@Override
public boolean action(Context context) {
//TODO
return false;
}
}
Event:
public enum Event {
//上传文件
UPLOAD_FILE,
//添加模板
ADD_TEMPLATE,
//转码
TRANS_CODE,
//保存文件
SAVE_FILE;
}
State:
public enum State {
//文件上传
FILE_UPLOAD_COMPLETED,
//转码模板
CODE_TEMPLATE_COMPLETED,
//转码
TRANS_CODE_COMPLETED,
//保存文件
SAVE_FILE_COMPLETED;
}
ActionMapping:
package org.example.fsm;
import org.example.fsm.action.Action;
import org.example.fsm.event.Event;
import org.example.fsm.state.State;
public class ActionMapping {
private Event event;
private State currState;
private State nextState;
private Action action;
public ActionMapping(Event event, State currState, State nextState, Action action) {
this.event = event;
this.currState = currState;
this.nextState = nextState;
this.action = action;
}
//根据事件和当前状态级下一个状态获取动作
public Action getActionByEventAndState(Event event, State currState, State nextState){
if(event == null || currState == null || nextState == null){
return null;
}
else {
if(event.equals(getEvent()) && currState.equals(getCurrState()) && nextState.equals(getNextState())){
return getAction();
}
return null;
}
}
//根据事件、当前状态、下一状态、动作获取ActionMapping
public ActionMapping getActionMappingByEventAndState(Event event, State currState, State nextState,Action action){
if(getAction().equals(action) && getEvent().equals(event) && getCurrState().equals(currState) && getNextState().equals(nextState)){
return this;
}
if(getAction().equals(action) && getCurrState().equals(currState) && getNextState().equals(nextState)){
return this;
}
return null;
}
public Event getEvent() {
return event;
}
public State getCurrState() {
return currState;
}
public State getNextState() {
return nextState;
}
public Action getAction() {
return action;
}
}
Context:
package org.example.fsm;
public class Context {
private Object params;
public Object getParams() {
return params;
}
public void setParams(Object params) {
this.params = params;
}
}
FSM:
package org.example.fsm;
import org.example.fsm.action.impl.AddCodeTemplateAction;
import org.example.fsm.action.impl.SaveFileAction;
import org.example.fsm.action.impl.TranslateCodeAction;
import org.example.fsm.action.impl.UploadFileAction;
import org.example.fsm.event.Event;
import org.example.fsm.state.State;
import java.util.ArrayList;
import java.util.List;
public class FSM {
private static List<ActionMapping> actionMappingList = new ArrayList<>();
static {
actionMappingList.add(new ActionMapping(Event.UPLOAD_FILE, State.FILE_UPLOAD_COMPLETED,State.CODE_TEMPLATE_COMPLETED,new UploadFileAction()));
actionMappingList.add(new ActionMapping(Event.ADD_TEMPLATE,State.CODE_TEMPLATE_COMPLETED,State.TRANS_CODE_COMPLETED,new AddCodeTemplateAction()));
actionMappingList.add(new ActionMapping(Event.TRANS_CODE,State.TRANS_CODE_COMPLETED,State.SAVE_FILE_COMPLETED,new TranslateCodeAction()));
actionMappingList.add(new ActionMapping(Event.SAVE_FILE,State.SAVE_FILE_COMPLETED,null,new SaveFileAction()));
}
public static void transform(Event event,State currState,State nextState,Context context){
ActionMapping actionMapping = getActionMapping(event, currState, nextState);
actionMapping.getAction().action(context);
}
private static ActionMapping getActionMapping(Event event,State currState,State nextState){
for(ActionMapping actionMapping : actionMappingList){
return actionMapping.getActionMappingByEventAndState(event, currState, nextState);
}
return null;
}
}
思考当前代码结构:
1、当需要添加状态、动作、事件时需要更改三个包下的类
2、ActionMapping用来组织状态、动作、事件的关系
3、FSM中使用了策略模式,list封装了所有的策略。
当前代码结构中存在的问题:
1、违反了开闭原则
当需要添加状态和事件时,需要修改枚举类
需要增加策略时,需要修改FSM类
解决方案:
1、采用状态模式(state-pattern)
状态模式:封装基于状态的行为,并将行为委托到当前的状态。
2、设置事件顶层接口
每一个事件基于接口定义该事件的特点
将事件内聚到State中,每次一扩展时,只需要新增一整套状态、事件、动作的类
3、利用反射加注解来封装策略
3、自定义实现状态机(有机会实现)
- 状态机的定义:通过定义State,Event,Action,Transition来实现状态机的流转,摒弃标准状态机中那些较复杂的概念(通过其他方式来实现所谓的较复杂的操作)
- 状态持久化:数据持久化到数据库,实现状态机的中断重启
- 上下文保存与传递:提供一套流程流转过程中参数的传递机制
- 并发控制:提供不同状态机隔离,同一状态机单实例运行机制
- 功能增强:接口或注解的形式实现自触发,重试,定时执行