✅统一状态机设计
状态机用于描述一个系统在不同状态之间的转换和行为,是状态模式的一种具体应用。状态机是一种抽象的计算模型,它包含有限个状态和转换规则,用于描述系统在不同状态下如何响应输入以及在不同输入下如何进行状态转换。
在我们日常开发中,我们提到的状态机基本都是有限状态机,用于解决状态相关的问题。有限状态机可以通过状态转换和事件触发来描述程序的行为和状态迁移
一个状态机通常包含以下几个要素
- 状态(States):代表系统可能处于的各种状态,例如 "已下单"、"已支付"、"已发货" 等。
- 事件(Events):触发状态转换的事件,例如 "下单"、"支付"、"发货" 等。
- 转换(Transitions):定义状态之间的转换规则,即在某个事件发生时,系统从一个状态转换到另一个状态的规则。
- 动作(Actions):在状态转换发生时执行的操作或行为。
一般我们在工作中,我们的主要的业务单据都会有一个状态的设计,我们会通过状态图画出他的状态流转情况,而控制这些状态如何流转,就是状态机干的事儿。
状态机的实现,有很多种方式,可以用一些状态机的框架,如Spring StateMachine,也可以用状态模式,也可以自己封装一个工具类都行。
如果没有一个严格的状态机控制的话,我们是可以随便修改订单的状态的,我们可以在已下单
状态直接推进到已发货
状态,这显然是不对的。
而状态机就是来控制这个状态的流转的他的目的都是把状态、事件、转换以及动作封装在一起的,他把这些东西内聚在一起了。有了它,一个已下单
状态的订单,只能通过支付
事件来驱动,并且还会有一些其他的约束,比如支付金额>0
(转移条件)等,然后他的下一个状态只能是已支付
这样的。
企业级状态机实战案例
在我们的项目中,针对订单的状态流转做了状态的严格控制,并且自定义了一个简单的状态机设计。
首先我们定义了一个接口,StateMachine,其中提供了一个状态转换的方法:transition。
/**
* @author Aska
*/
public interface StateMachine<STATE, EVENT> {
/**
* 状态机转移
*
* @param state
* @param event
* @return
*/
public STATE transition(STATE state, EVENT event);
}
入参分别是STATE和EVENT。
接下来定义了一个通用的状态机:
/**
* @author Aska
*/
public class BaseStateMachine<STATE, EVENT> implements StateMachine<STATE, EVENT> {
private Map<String, STATE> stateTransitions = Maps.newHashMap();
protected void putTransition(STATE origin, EVENT event, STATE target) {
stateTransitions.put(Joiner.on("_").join(origin, event), target);
}
@Override
public STATE transition(STATE state, EVENT event) {
STATE target = stateTransitions.get(Joiner.on("_").join(state, event));
if (target == null) {
throw new BizException("state = " + state + " , event = " + event, STATE_MACHINE_TRANSITION_FAILED);
}
return target;
}
}
这里实现了transition接口,进行状态的流转判断。这个方法主要做的事情,就是去stateTransitions中查看是否有状态&事件对,如果有的话认为可以转换,没有则认为不能转换。
接下我们看下OrderStateMachine的实现,这里只干了一件事,就是去初始化前面用到的stateTransitions这个 map。
其实逻辑也很剪的,就是把能转换的原状态、事件和目标状态放进去。
/**
* @author Aska
*/
public class OrderStateMachine extends BaseStateMachine<TradeOrderState, TradeOrderEvent> {
public static final OrderStateMachine INSTANCE = new OrderStateMachine();
{
putTransition(TradeOrderState.CREATE, TradeOrderEvent.CONFIRM, TradeOrderState.CONFIRM);
putTransition(TradeOrderState.CONFIRM, TradeOrderEvent.PAY, TradeOrderState.PAID);
//库存预扣减成功,但是未真正扣减成功,也能支付/取消,不能因为延迟导致用户无法支付/取消。
putTransition(TradeOrderState.CREATE, TradeOrderEvent.PAY, TradeOrderState.PAID);
putTransition(TradeOrderState.CREATE, TradeOrderEvent.CANCEL, TradeOrderState.CLOSED);
putTransition(TradeOrderState.CREATE, TradeOrderEvent.TIME_OUT, TradeOrderState.CLOSED);
//订单创建过程中失败,推进到废弃态,这种状态用户看不到订单
putTransition(TradeOrderState.CREATE, TradeOrderEvent.DISCARD, TradeOrderState.DISCARD);
putTransition(TradeOrderState.CONFIRM, TradeOrderEvent.DISCARD, TradeOrderState.DISCARD);
//已支付后,再确认,状态不变
putTransition(TradeOrderState.PAID, TradeOrderEvent.CONFIRM, TradeOrderState.PAID);
putTransition(TradeOrderState.CONFIRM, TradeOrderEvent.CANCEL, TradeOrderState.CLOSED);
putTransition(TradeOrderState.CONFIRM, TradeOrderEvent.TIME_OUT, TradeOrderState.CLOSED);
putTransition(TradeOrderState.PAID, TradeOrderEvent.FINISH, TradeOrderState.FINISH);
}
}
比如,订单状态可以从 CREATE流转到 CONFIRM 状态,并且需要经过 CONFIRM 事件才能 。那么就:
putTransition(TradeOrderState.CREATE, TradeOrderEvent.CONFIRM, TradeOrderState.CONFIRM);
就 OK 了。这样我们在需要状态流转的时候,只需要告诉状态机,我当前的状态,和我本次事件,就可以通过状态机来控制是否可以转换,以及得到转换后的状态是什么了。
如以下调用方式:
public TradeOrder confirm(OrderConfirmRequest request) {
this.setOrderConfirmedTime(request.getOperateTime());
TradeOrderState orderState = OrderStateMachine.INSTANCE.transition(this.getOrderState(), request.getOrderEvent());
this.setOrderState(orderState);
return this;
}
在 confirm 方法中。通过OrderStateMachine.INSTANCE.transition(this.getOrderState(), request.getOrderEvent());
来获取到目标状态,如果不报错,将返回一个目标状态,如果报错,则说明当前事件是不合法的。
通过这样的设计,我们可以严格的控制状态流转。并且业务操作时不需要关注具体的目标状态,只需要知道当前状态和事件就行了。