有限状态机的4种实现对比
在日常工作过程中,我们经常会遇到状态的变化场景,例如订单状态发生变化,商品状态的变化。这些状态的变化,我们称为有限状态机,缩写为FSM( F State Machine).。之所以称其为有限,是因为这些场景中的状态往往是可以枚举出来的有限个的,所以称其为有限状态机。下面我们来看一个具体的场景例子。
简单场景:
地铁进站闸口的状态有两个:已经关闭、已经开启两个状态。刷卡后闸口从已关闭变为已开启,人通过后闸口状态从已开启变为已关闭。
01 遇到这类问题,在编码时我们应该如何处理呢?
- 基于Switch
- 基于状态集合
- 基于State模式
- 基于枚举的实现
下面我们针对每一种实现方式进行分析。场景分解后会有一下2种状态4种情况出现:
Index | State | Event | NextState | Action |
---|---|---|---|---|
1 | 闸机口 LOCKED | 投币 | 闸机口 UN_LOCKED | 闸机口打开闸门 |
2 | 闸机口 LOCKED | 通过 | 闸机口 LOCKED | 闸机口警告 |
3 | 闸机口 UN_LOCKED | 投币 | 闸机口 UN _LOCKED | 闸机口退币 |
4 | 闸机口 UN_LOCKED | 通过 | 闸机口 LOCKED | 闸机口关闭闸门 |
针对以上4种请求,共拆分了5个Test Case
T01
Given:一个Locked的进站闸口
When: 投入硬币
Then:打开闸口
T02
Given:一个Locked的进站闸口
When: 通过闸口
Then:警告提示
T03
Given:一个Unocked的进站闸口
When: 通过闸口
Then:闸口关闭
T04
Given:一个Unlocked的进站闸口
When: 投入硬币
Then:退还硬币
T05
Given:一个闸机口
When: 非法操作
Then:操作失败
代码地址:https://gitlab.com/tengbai/fsm-java
项目中共有4中状态机的实现方式。
基于Switch语句实现的有限状态机,代码在master分支
基于State模式实现的有限状态机。代码在state-pattern分支
基于状态集合实现的有限状态机。代码在collection-state分支
基于枚举实现的状态机。代码在enum-state分支
01.01 使用Switch来实现有限状态机
这种方式只需要懂得Java语法及可以实现出来。先看代码,然后我们在讨论这种实现方式是否好。
EntranceMachineTest.java
package com.page.java.fsm;
import com.page.java.fsm.exception.InvalidActionException;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.BDDAssertions.then;
class EntranceMachineTest {
@Test
void should_be_unlocked_when_insert_coin_given_a_entrance_machine_with_locked_state() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED);
String result = entranceMachine.execute(Action.INSERT_COIN);
then(result).isEqualTo("opened");
then(entranceMachine.getState()).isEqualTo(EntranceMachineState.UNLOCKED);
}
@Test
void should_be_locked_and_alarm_when_pass_given_a_entrance_machine_with_locked_state() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED);
String result = entranceMachine.execute(Action.PASS);
then(result).isEqualTo("alarm");
then(entranceMachine.getState()).isEqualTo(EntranceMachineState.LOCKED);
}
@Test
void should_fail_when_execute_invalid_action_given_a_entrance_with_locked_state() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED);
assertThatThrownBy(() -> entranceMachine.execute(null))
.isInstanceOf(InvalidActionException.class);
}
@Test
void should_locked_when_pass_given_a_entrance_machine_with_unlocked_state() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.UNLOCKED);
String result = entranceMachine.execute(Action.PASS);
then(result).isEqualTo("closed");
then(entranceMachine.getState()).isEqualTo(EntranceMachineState.LOCKED);
}
@Test
void should_refund_and_unlocked_when_insert_coin_given_a_entrance_machine_with_unlocked_state() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.UNLOCKED);
String result = entranceMachine.execute(Action.INSERT_COIN);
then(result).isEqualTo("refund");
then(entranceMachine.getState()).isEqualTo(EntranceMachineState.UNLOCKED);
}
}
Action.java
public enum Action {
INSERT_COIN,
PASS
}
EntranceMachineState.java
public enum EntranceMachineState {
UNLOCKED,
LOCKED
}
InvalidActionException.java
package com.page.java.fsm.exception;
public class InvalidActionException extends RuntimeException {
}
EntranceMachine.java
package com.page.java.fsm;
import com.page.java.fsm.exception.InvalidActionException;
import lombok.Data;
import java.util.Objects;
@Data
public class EntranceMachine {
private EntranceMachineState state;
public EntranceMachine(EntranceMachineState state) {
this.state = state;
}
public String execute(Action action) {
if (Objects.isNull(action)) {
throw new InvalidActionException();
}
if (EntranceMachineState.LOCKED.equals(state)) {
switch (action) {
case INSERT_COIN:
setState(EntranceMachineState.UNLOCKED);
return open();
case PASS:
return alarm();
}
}
if (EntranceMachineState.UNLOCKED.equals(state)) {
switch (action) {
case PASS:
setState(EntranceMachineState.LOCKED);
return close();
case INSERT_COIN:
return refund();
}
}
return null;
}
private String refund() {