状态模式一般用来实现状态机,而状态机常用在游戏、工作流引擎等系统开发中。不过,状态机的实现方式有多种,除了状态模式,比较常用的还有分支逻辑法和查表法。
有限状态机,英文翻译是 Finite State Machine,缩写为 FSM,简称为状态机。状态机有 3 个组成部分:状态(State)、事件(Event)、动作(Action)。其中,事件也称为转移条件(Transition Condition)。事件触发状态的转移及动作的执行。不过,动作不是必须的,也可能只转移状态,不执行任何动作。
以支付收获为例。
三种状态State:未支付、未收货、完成。
两种事件Event:支付、收货。
状态的流转即动作:支付后状态为未收货,收获后状态为完成。
/**
* 支付状态枚举
*/
@Getter
public enum PayStateEnum {
/**
* 待支付
*/
UNPAY(0),
/**
* 待收货
*/
UNRECEIVE(1),
/**
* 完成
*/
FINAL(2),
/**
* 错误
*/
ERROR(3);
/**
* 值
*/
private int value;
PayStateEnum(int value) {
this.value = value;
}
}
/**
* 支付事件枚举
*/
@Getter
public enum PayEventEnum {
/**
* 支付
*/
PAY(0),
/**
* 收货
*/
RECEIVE(1);
/**
* 值
*/
private int value;
PayEventEnum(int value) {
this.value = value;
}
}
使用if-else或者switch-case
进行处理的方式
/**
* 支付状态机:逻辑分支
*/
@Slf4j
public class PayStateMachine {
/**
* 当前状态
*/
private PayStateEnum payStateEnum;
public PayStateMachine() {
payStateEnum = PayStateEnum.UNPAY;
}
public void executeEvent(PayEventEnum payEventEnum) {
switch (payEventEnum) {
//支付行为发生:未支付->待收货
case PAY:
if (this.payStateEnum != PayStateEnum.UNPAY) {
log.info("商铺不是【未支付】状态,请核验!");
this.payStateEnum = PayStateEnum.ERROR;
break;
}
this.payStateEnum = PayStateEnum.UNRECEIVE;
break;
//收货行为发生:待收货->完成
case RECEIVE:
if (this.payStateEnum != PayStateEnum.UNRECEIVE) {
log.info("商铺不是【未收货】状态,请核验!");
this.payStateEnum = PayStateEnum.ERROR;
break;
}
this.payStateEnum = PayStateEnum.FINAL;
break;
default:
log.info("未设置的行为!");
break;
}
}
public PayStateEnum getCurrentState() {
return this.payStateEnum;
}
}
/**
* 测试逻辑分支方式
*/
@Test
public void testLogicBranch() {
PayStateMachine payStateMachine = new PayStateMachine();
log.info("当前状态为:{}", payStateMachine.getCurrentState());
payStateMachine.executeEvent(PayEventEnum.RECEIVE);
log.info("当前状态为:{}", payStateMachine.getCurrentState());
}
将状态和事件形成一个二维矩阵表,将结果态放入其中。
UNPAY | UNRECEIVE | FINAL | |
---|---|---|---|
PAY | 状态转为UNRECEIVE | ERROR | ERROR |
RECEIVE | ERROR | 状态转为FINAL | ERROR |
我们可以将结果态转为一个二维数组进行存储,用的时候使用状态和事件枚举的value
值作为索引,获取结果态。
/**
* 支付状态机:查表法
*/
public class PayStateMachine2 {
/**
* 当前状态
*/
private PayStateEnum currentPayStateEnum;
/**
* 将结果态转为一个二维数组进行存储
*/
private static final PayStateEnum[][] payStateTable= {
{UNRECEIVE, ERROR, ERROR},
{ERROR, FINAL, ERROR}
};
/**
* 初始化
*/
public PayStateMachine2() {
currentPayStateEnum = PayStateEnum.UNPAY;
}
/**
* 用的时候使用状态和事件枚举的value值作为索引,获取结果态。
*/
public void executeEvent(PayEventEnum payEventEnum) {
this.currentPayStateEnum =
payStateTable[this.currentPayStateEnum.getValue()]
[payEventEnum.getValue()];
}
/**
* 获取当前状态
*/
public PayStateEnum getCurrentState() {
return this.currentPayStateEnum;
}
}
/**
* 测试查表法方式
*/
@Test
public void testCheckTable() {
PayStateMachine2 payStateMachine = new PayStateMachine2();
log.info("当前状态为:{}", payStateMachine.getCurrentState());
payStateMachine.executeEvent(PayEventEnum.PAY);
log.info("当前状态为:{}", payStateMachine.getCurrentState());
payStateMachine.executeEvent(PayEventEnum.RECEIVE);
log.info("当前状态为:{}", payStateMachine.getCurrentState());
}
状态模式,定义状态接口,并将行为规定为抽象方法,实现对应三种状态以及行为方法(实现状态流转逻辑)。定义状态机,并依赖状态接口。同时,每个具体的状态又依赖状态机。(即状态机和各个状态类之间是双向依赖关系,因为每个状态需要依靠状态机修改状态)。
这里要注意,具体的状态实现类Unpay和UnReceive
都是固定的表示一种状态,不能修改,所以最好使用单例。
定义状态和实现状态。
/**
* 状态接口
*/
public interface IPay {
/**
* 获取当前状态
*/
PayStateEnum getState();
/**
* 支付行为
*/
void pay(PayStateMachine3 payStateMachine);
/**
* 收货行为
*/
void receive(PayStateMachine3 payStateMachine);
}
/**
* 未支付状态
*/
@Slf4j
public class Unpay implements IPay {
/**
* 单例模式
*/
private static final Unpay INSTANCE = new Unpay();
@Override
public PayStateEnum getState() {
return PayStateEnum.UNPAY;
}
//依赖状态机修改状态
@Override
public void pay(PayStateMachine3 payStateMachine) {
payStateMachine.setIPay(UnReceive.getInstance()); //将未收货状态类注入
}
@Override
public void receive(PayStateMachine3 payStateMachine) {
log.info("未支付,不能收货!");
}
/**
* 单例:提供获取实例方法
*/
public static Unpay getInstance() {
return INSTANCE;
}
}
/**
* 未收货
*/
@Slf4j
public class UnReceive implements IPay {
/**
* 单例模式
*/
private static final UnReceive INSTANCE = new UnReceive();
@Override
public PayStateEnum getState() {
return PayStateEnum.UNRECEIVE;
}
@Override
public void pay(PayStateMachine3 payStateMachine) {
log.info("未收到货,不能支付");
}
@Override
public void receive(PayStateMachine3 payStateMachine) {
payStateMachine.setIPay(Final.getInstance());
}
public static UnReceive getInstance() {
return INSTANCE;
}
}
/**
* 完成
*/
@Slf4j
public class Final implements IPay {
/**
* 单例模式
*/
private static final Final INSTANCE = new Final();
@Override
public PayStateEnum getState() {
return PayStateEnum.FINAL;
}
@Override
public void pay(PayStateMachine3 payStateMachine) {
log.info("订单已完成,不能支付!" );
}
@Override
public void receive(PayStateMachine3 payStateMachine) {
log.info("订单已完成,不能收货!" );
}
public static Final getInstance() {
return INSTANCE;
}
}
定义状态机。
/**
* 状态模式
*/
@Setter
public class PayStateMachine3 {
/**
* 将状态接口注入
*/
private IPay iPay;
/**
* 初始化:未支付
*/
public PayStateMachine3() {
this.iPay = Unpay.getInstance();
}
/**
* 支付
*/
public void pay() {
this.iPay.pay(this);
}
/**
* 收货
*/
public void receive() {
this.iPay.receive(this);
}
/**
* 获取当前状态:通过状态的具体实现来修改掉内部的iPay实现(多态)
*/
public PayStateEnum getCurrentState() {
return this.iPay.getState();
}
}
/**
* 测试状态模式
*/
@Test
public void testStateDesign() {
PayStateMachine3 payStateMachine = new PayStateMachine3();
log.info("当前状态为:{}", payStateMachine.getCurrentState());
payStateMachine.pay();
log.info("当前状态为:{}", payStateMachine.getCurrentState());
payStateMachine.receive();
log.info("当前状态为:{}", payStateMachine.getCurrentState());
}
实际上,像游戏这种比较复杂的状态机,包含的状态比较多,我优先推荐使用查表法,而状态模式会引入非常多的状态类,会导致代码比较难维护。相反,像电商下单、外卖下单这种类型的状态机,它们的状态并不多,状态转移也比较简单,但事件触发执行的动作包含的业务逻辑可能会比较复杂,所以,更加推荐使用状态模式来实现。