状态模式,一种不太常用的设计模式,但是在涉及状态流转的场景下非常好用,一般用来实现状态机。它的侧重点也在解耦,且具备良好的可扩展性。状态机也称有限状态机,一般包含三要素:事件(Event)、状态(State)、行为(Action)。其中,事件触发状态转移,状态转移后可能会做一些行为,也可能只转移状态,什么事情也不做,所以事件和状态是必须的,但是行为不是必须的。
1、背景:
公司主营港口无人驾驶,无人车需要与港口自动化设备交互,进行引导对位,从而进行自动化抓落箱。整个对位交互过程可以划分为如下几个状态:初始化->就绪->开始对位->对位中->单次对位完成->对位完成->锁车->解锁->对位结束。
2、状态模式使用:
定义一个状态机类AlignStateMachine
,用来管理所有的事件与状态;定义一个抽象的事件AbstractAlignEvent
,具体的事件继承该类,用来触发状态转移;定义一个抽象状态类AbstractAlignState
,具体的状态继承该类,实现自己关心的方法;定义一个抽象的行为AbstractAlignAction
,具体的行为继承该类,表示状态转移后要做的事情。以下,将结合Spring的事件实现对位状态机。
2.1、AlignStateMachine
:
状态机,其中包含所有的事件以及管理着每个无人车的状态。
java
@Service
@Slf4j
public class AlignStateMachine {
/**
* key->无人车ID
* value->无人车的状态
*/
private final Map<String, AbstractAlignState> stateMachineMap = new ConcurrentHashMap<>();
@Resource
private AbstractAlignState alignInit;
public AbstractAlignState getCurrentAlignState(String vehicleId) {
return stateMachineMap.get(vehicleId);
}
public void goToTheNextAlignState(String vehicleId, AbstractAlignState alignState) {
stateMachineMap.computeIfPresent(vehicleId, (k, v) -> {
if (Objects.nonNull(stateMachineMap.get(vehicleId))) {
return alignState;
} else {
return null;
}
});
}
public void removeAlignState(String vehicleId) {
stateMachineMap.remove(vehicleId);
}
@EventListener
public void initAlign(AlignInitEvent alignInitEvent) {
log.info("无人车{}收到AlignInitEvent", alignInitEvent.getVehicleId());
removeAlignState(alignInitEvent.getVehicleId());
stateMachineMap.putIfAbsent(alignInitEvent.getVehicleId(), alignInit);
stateMachineMap.get(alignInitEvent.getVehicleId()).initAlign(this, alignInitEvent);
log.info("无人车{}处于初始化状态", alignInitEvent.getVehicleId());
}
@EventListener
public void readyAlign(AlignReadyEvent alignReadyEvent) {
log.info("无人车{}收到AlignReadyEvent", alignReadyEvent.getVehicleId());
stateMachineMap.get(alignReadyEvent.getVehicleId()).readyAlign(this, alignReadyEvent);
}
// 省略其它事件...
}
2.2、AbstractAlignEvent
:
继承Spring的Event,方便使用Spring的Event进行管理。此外,将一些公共字段提取到此类中,例如无人车车号
java
@Getter
public abstract class AbstractAlignEvent extends ApplicationEvent {
/**
* 车号
*/
private final String vehicleId;
public AbstractAlignEvent(Object source, String vehicleId) {
super(source);
this.vehicleId = vehicleId;
}
}
AlignInitEvent->初始化事件;
java
public class AlignInitEvent extends AbstractAlignEvent {
public AlignInitEvent(Object source, String vehicleId) {
super(source, vehicleId);
}
}
省略其它事件...
2.3、AbstractAlignState
:
包含所有事件以及默认实现,默认实现只打印日志,具体的实现由对应的子类实现感兴趣的事件。有了默认实现,方便进行调试,例如发布了某个事件,但是却没有达到预期的效果,通过日志可以看到一般是状态没有达到预期的状态。
java
@Service
@Slf4j
public abstract class AbstractAlignState {
@Resource
private ApplicationContext applicationContext;
public <T extends AbstractAlignState> T getAlignStateInstance(Class<T> beanType) {
return applicationContext.getBean(beanType);
}
protected void initAlign(AlignStateMachine alignStateMachine, AlignInitEvent alignInitEvent) {
log.info("无人车{}当前处于{}状态, 忽略{}事件",
alignInitEvent.getVehicleId(),
alignStateMachine.getCurrentAlignState(alignInitEvent.getVehicleId()).getClass().getSimpleName(),
alignInitEvent.getClass().getSimpleName()
);
}
protected void readyAlign(AlignStateMachine alignStateMachine, AlignReadyEvent alignReadyEvent) {
log.info("无人车{}当前处于{}状态, 忽略{}事件",
alignReadyEvent.getVehicleId(),
alignStateMachine.getCurrentAlignState(alignReadyEvent.getVehicleId()).getClass().getSimpleName(),
alignReadyEvent.getClass().getSimpleName()
);
}
// 省略其它事件...
}
具体的状态类子类实现:
java
@Service
@Slf4j
public class AlignInit extends AbstractAlignState {
@Resource
private ReportIntoCraneAction reportIntoCraneAction;
@Resource
private SetVehicleArriveAction setVehicleArriveAction;
@Resource
private AlignResetAction alignResetAction;
@Override
protected void initAlign(AlignStateMachine alignStateMachine, AlignInitEvent alignInitEvent) {
log.info("无人车{}收到AlignInitEvent", alignInitEvent.getVehicleId());
alignResetAction.act(alignStateMachine.getCurrentAlignState(alignInitEvent.getVehicleId()), alignInitEvent);
reportIntoCraneAction.act((AlignInit) alignStateMachine.getCurrentAlignState(alignInitEvent.getVehicleId()), alignInitEvent);
}
@Override
protected void readyAlign(AlignStateMachine alignStateMachine, AlignReadyEvent alignReadyEvent) {
AlignReady alignReady = getAlignStateInstance(AlignReady.class);
alignStateMachine.goToTheNextAlignState(alignReadyEvent.getVehicleId(), alignReady);
setVehicleArriveAction.act(alignReady, alignReadyEvent);
log.info("无人车{}处于就绪状态", alignReadyEvent.getVehicleId());
}
}
其中,AlignInit状态只关心initAlign和readyAlign事件,其它事件不关心,走默认实现,只打印日志。监听到对应事件后,将状态转移至下一状态,不过initAlign事件比较特殊,因为遇到该事件时先进入初始化状态,然后执行对应的行为,在状态机中提前初始化好了。其它都是在事件中发生状态转移,然后执行对应的行为。后面的状态如AlignReady、AlignBegin、Aligning、SingleAlignFinish、AlignFinish、AlignLocked、 AlignUnlocked、AlignEnd,只实现自己关心的事件,做自己该做的行为。
2.4、AbstractAlignAction
:
定义一个行为方法,具体的行为由子类去实现。
java
public abstract class AbstractAlignAction<T extends AbstractAlignState, S extends AbstractAlignEvent> {
public abstract void act(T alignState, S alignEvent);
}
具体的行为实现:
java
@Service
public class ReportIntoCraneAction extends AbstractAlignAction<AlignInit, AlignInitEvent> {
@Override
public void act(AlignInit alignState, AlignInitEvent alignInitEvent) {
// 省略具体实现
}
}
省略其它行为实现...
2.5、发布对应事件,触发状态机流转:
java
@Service
public class PncCommandService {
@Resource
private ApplicationEventPublisher applicationEventPublisher;
private void handleXXX(String vehicleId) {
applicationEventPublisher.publishEvent(new AlignInitEvent(this, vehicleId));
}
// 省略其它事件发布
}
3、总结:
当发现程序在设计上符合状态机时,采用状态模式可以使程序更加解耦,每个状态转移要做什么事情都很清晰,未来新增状态也很利于扩展。此外,合理的代码包结构例如状态都放在state包下,事件都放在event包下,行为都放在action包下,代码层次一目了然,利于维护。结合Spring的事件,在Java中如此实现状态机更加优雅一些。