在日常工作过程中,我们经常会遇到状态的变化场景,例如订单状态发生变化,商品状态的变化。这些状态的变化,我们称为有限状态机,缩写为FSM( F State Machine).。之所以称其为有限,是因为这些场景中的状态往往是可以枚举出来的有限个的,所以称其为有限状态机。下面我们来看一个具体的场景例子。
简单场景:
地铁进站闸口的状态有两个:已经关闭、已经开启两个状态。刷卡后闸口从已关闭变为已开启,人通过后闸口状态从已开启变为已关闭。
下面我们针对每一种实现方式进行分析。场景分解后会有一下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 分支
这种方式只需要懂得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() { return "refund"; } private String close() { return "closed"; } private String alarm() { return "alarm"; } private String open() { return "opened"; } } 复制代码
if(), swich语句都是switch语句,但是 Switch是一种Code Bad Smell ,因为它本质上一种重复。当代码中有多处相同的switch时,会让系统变得晦涩难懂,脆弱,不易修改。
上面的代码虽然出现了多层嵌套但是还算是结构简单,不过想通过并不能很清楚闸机口的逻辑还是化点时间。如果闸机口的状态等多一些,那就阅读、理解起来也就更加困难。
所以在日常工作,我遵循**“事不过三,三则重构”**的原则:
事不过三:
当只有一两个状态(或者重复)时,那么先用最简单的实现实现。
一旦出现三种以及以上的状态(或者重复),立即重构。
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(new LockedEntranceMachineState()); String result = entranceMachine.execute(Action.INSERT_COIN); then(result).isEqualTo("opened"); then(entranceMachine.isUnlocked()).isTrue(); } @Test void should_be_locked_and_alarm_when_pass_given_a_entrance_machine_with_locked_state() { EntranceMachine entranceMachine = new EntranceMachine(new LockedEntranceMachineState()); String result = entranceMachine.execute(Action.PASS); then(result).isEqualTo("alarm"); then(entranceMachine.isLocked()).isTrue(); } @Test void should_fail_when_execute_invalid_action_given_a_entrance_with_locked_state() { EntranceMachine entranceMachine = new EntranceMachine(new LockedEntranceMachineState()); assertThatThrownBy(() -> entranceMachine.execute(null)) .isInstanceOf(InvalidActionException.class); } @Test void should_locked_when_pass_given_a_entrance_machine_with_unlocked_state() { EntranceMachine entranceMachine = new EntranceMachine(new UnlockedEntranceMachineState()); String result = entranceMachine.execute(Action.PASS); then(result).isEqualTo("closed"); then(entranceMachine.isLocked()).isTrue(); } @Test void should_refund_and_unlocked_when_insert_coin_given_a_entrance_machine_with_unlocked_state() { EntranceMachine entranceMachine = new EntranceMachine(new UnlockedEntranceMachineState()); String result = entranceMachine.execute(Action.INSERT_COIN); then(result).isEqualTo("refund"); then(entranceMachine.isUnlocked()).isTrue(); } } 复制代码
EntranceMachineState.java
package com.page.java.fsm; public interface EntranceMachineState { String insertCoin(EntranceMachine entranceMachine); String pass(EntranceMachine entranceMachine); } 复制代码
LockedEntranceMachineState.java
package com.page.java.fsm; public class LockedEntranceMachineState implements EntranceMachineState { @Override public String insertCoin(EntranceMachine entranceMachine) { return entranceMachine.open(); } @Override public String pass(EntranceMachine entranceMachine) { return entranceMachine.alarm(); } } 复制代码
UnlockedEntranceMachineState.java
package com.page.java.fsm; public class UnlockedEntranceMachineState implements EntranceMachineState { @Override public String insertCoin(EntranceMachine entranceMachine) { return entranceMachine.refund(); } @Override public String pass(EntranceMachine entranceMachine) { return entranceMachine.close(); } } 复制代码
Action.java
package com.page.java.fsm; public enum Action { PASS, INSERT_COIN } 复制代码
EntranceMachine.java
package com.page.java.fsm; import com.page.java.fsm.exception.InvalidActionException; import java.util.Objects; public class EntranceMachine { private EntranceMachineState locked = new LockedEntranceMachineState(); private EntranceMachineState unlocked = new UnlockedEntranceMachineState(); private EntranceMachineState state; public EntranceMachine(EntranceMachineState state) { this.state = state; } public String execute(Action action) { if (Objects.isNull(action)) { throw new InvalidActionException(); } if (Action.PASS.equals(action)) { return state.pass(this); } return state.insertCoin(this); } public boolean isUnlocked() { return state == unlocked; } public boolean isLocked() { return state == locked; } public String open() { setState(unlocked); return "opened"; } public String alarm() { setState(locked); return "alarm"; } public String refund() { setState(unlocked); return "refund"; } public String close() { setState(locked); return "closed"; } private void setState(EntranceMachineState state) { this.state = state; } } 复制代码
State模式和Proxy模式类似,但是在State模式中EntranceMachineState持有EntranceMachine实例的引用。
我们发现EntranceMachine的execute()方法的逻辑变的简单,但是代码复杂度升高了。因为每个state实例都提供了两个动作实现insertCoin()和pass()。这个地方本人认为并不够表意,因为作出的动作被添加到两个状态上,虽然能够实现业务业务,但是并不利于理解清楚业务意思。
State模式,虽然能够将逻辑进行拆分,但是那些状态的顺序,以及有几种状态,都不是很直观的观察到。
不过在实际业务中,State模式也是一种很好的实现方式,毕竟他避免了switch的堆积问题。
状态集合是将一组描述状态变化的事务元素组成的集合。
集合中的每一个元素包含4个属性:当前的状态,事件,下一个状态,触发的动作。
使用时遍历集合根据动作找到特定的元素,并更具元素上的属性和事件来完成业务逻辑。
具体代码如下:
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_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_machine() { EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED); assertThatThrownBy(() -> entranceMachine.execute(null)) .isInstanceOf(InvalidActionException.class); } @Test void should_closed_when_pass_given_a_entrance_machine_with_unlocked() { 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_when_insert_coin_given_a_entrance_machine_with_unlocked() { 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
package com.page.java.fsm; public enum Action { PASS, INSERT_COIN } 复制代码
EntranceMachineState.java
package com.page.java.fsm; public enum EntranceMachineState { LOCKED, UNLOCKED } 复制代码
EntranceMachine.java
package com.page.java.fsm; import com.page.java.fsm.events.AlarmEvent; import com.page.java.fsm.events.CloseEvent; import com.page.java.fsm.events.OpenEvent; import com.page.java.fsm.events.RefundEvent; import com.page.java.fsm.exception.InvalidActionException; import lombok.Data; import java.util.Arrays; import java.util.List; import java.util.Optional; @Data public class EntranceMachine { List<EntranceMachineTransaction> entranceMachineTransactionList = Arrays.asList( EntranceMachineTransaction.builder() .currentState(EntranceMachineState.LOCKED) .action(Action.INSERT_COIN) .nextState(EntranceMachineState.UNLOCKED) .event(new OpenEvent()) .build(), EntranceMachineTransaction.builder() .currentState(EntranceMachineState.LOCKED) .action(Action.PASS) .nextState(EntranceMachineState.LOCKED) .event(new AlarmEvent()) .build(), EntranceMachineTransaction.builder() .currentState(EntranceMachineState.UNLOCKED) .action(Action.PASS) .nextState(EntranceMachineState.LOCKED) .event(new CloseEvent()) .build(), EntranceMachineTransaction.builder() .currentState(EntranceMachineState.UNLOCKED) .action(Action.INSERT_COIN) .nextState(EntranceMachineState.UNLOCKED) .event(new RefundEvent()) .build() ); private EntranceMachineState state; public EntranceMachine(EntranceMachineState state) { setState(state); } public String execute(Action action) { Optional<EntranceMachineTransaction> transactionOptional = entranceMachineTransactionList .stream() .filter(transaction -> transaction.getAction().equals(action) && transaction.getCurrentState().equals(state)) .findFirst(); if (!transactionOptional.isPresent()) { throw new InvalidActionException(); } EntranceMachineTransaction transaction = transactionOptional.get(); setState(transaction.getNextState()); return transaction.getEvent().execute(); } } 复制代码
EntranceMachineTransaction.java
package com.page.java.fsm; import com.page.java.fsm.events.Event; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Data @Builder @NoArgsConstructor @AllArgsConstructor public class EntranceMachineTransaction { private EntranceMachineState currentState; private Action action; private EntranceMachineState nextState; private Event event; } 复制代码
Event.java
package com.page.java.fsm.events; public interface Event { String execute(); } 复制代码
OpenEvent.java
package com.page.java.fsm.events; public class OpenEvent implements Event { @Override public String execute() { return "opened"; } } 复制代码
AlarmEvent.java
package com.page.java.fsm.events; public class AlarmEvent implements Event { @Override public String execute() { return "alarm"; } } 复制代码
CloseEvent.java
package com.page.java.fsm.events; public class CloseEvent implements Event { @Override public String execute() { return "closed"; } } 复制代码
RefundEvent.java
package com.page.java.fsm.events; public class RefundEvent implements Event { @Override public String execute() { return "refund"; } } 复制代码
InvalidActionException.java
package com.page.java.fsm.exception; public class InvalidActionException extends RuntimeException { } 复制代码
相比于Switch的实现方式,状态集合的实现方式对状态规则的描述更加直观。且扩展性更强,不需求修改实现路基,只需要添加相关的状态描述即可。
我们知道日常工作中读代码和写代码比例在10:1,有些场景下甚至到了20:1。Switch需要我们每次在脑子中组织一次状态的顺序和规则,而集合能够很直观的表达出这个规则。
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_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_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_machine() { EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED); assertThatThrownBy(() -> entranceMachine.execute(null)) .isInstanceOf(InvalidActionException.class); } @Test void should_refund_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); } @Test void should_closed_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); } } 复制代码
EntraceMachine.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) { setState(state); } public String execute(Action action) { if (Objects.isNull(action)) { throw new InvalidActionException(); } return action.execute(this, state); } public String open() { return "opened"; } public String alarm() { return "alarm"; } public String refund() { return "refund"; } public String close() { return "closed"; } } 复制代码
Action.java
package com.page.java.fsm; public enum Action { PASS { @Override public String execute(EntranceMachine entranceMachine, EntranceMachineState state) { return state.pass(entranceMachine); } }, INSERT_COIN { @Override public String execute(EntranceMachine entranceMachine, EntranceMachineState state) { return state.insertCoin(entranceMachine); } }; public abstract String execute(EntranceMachine entranceMachine, EntranceMachineState state); } 复制代码
EntranceMachineState.java
package com.page.java.fsm; public enum EntranceMachineState { LOCKED { @Override public String insertCoin(EntranceMachine entranceMachine) { entranceMachine.setState(UNLOCKED); return entranceMachine.open(); } @Override public String pass(EntranceMachine entranceMachine) { entranceMachine.setState(this); return entranceMachine.alarm(); } }, UNLOCKED { @Override public String insertCoin(EntranceMachine entranceMachine) { entranceMachine.setState(this); return entranceMachine.refund(); } @Override public String pass(EntranceMachine entranceMachine) { entranceMachine.setState(LOCKED); return entranceMachine.close(); } }; public abstract String insertCoin(EntranceMachine entranceMachine); public abstract String pass(EntranceMachine entranceMachine); } 复制代码
InvalidActionException.java
package com.page.java.fsm.exception; public class InvalidActionException extends RuntimeException { } 复制代码
通过上面的代码,可以发现Action、EntranceMachineState两个枚举的复杂度都提升了。不单单是定义了常量那么简单。还提供了相应的逻辑处理。
在EntranceMachineState.java的提交记录中,对进行了一次重构,将具体业务逻辑执行移动到EntranceMachine中,EntranceMachineState内每种状态的方法中只负责调度。这样能够通过EntranceMachineState相对直观的看清楚做了什么,状态变成了什么。
缺陷就是,EntranceMachine 对外提供了public的setState方法,这也就意味着调用者在将来维护是,很有可能滥用setState方法。
通过上面4中对FSM的实现,我们看到每一种是实现都有优点和它的不足。那么在日常工作中,如何选择呢,我个人认为可以遵循一下两个建议:
遵循Simple Design。如果没有一个外部参考,那么用哪一种都不为过。所以引入一个原则作为参考,可以更好的帮助我们做决定。这里日常工作中我们经常使用Simple Design:通过测试、揭示意图、消除重复、最少元素。并在实现过程中不断重构,代码是重构出来的,而不是一次性的设计出来的。
在状态机的实现上多做尝试。例子只是一个简单的场景,所以只能看到简单场景下的实现效果,实际业务线上的状态会非常丰富,而且每种状态中可真行的动作也是不同的。所以针对特定场景遇到的问题,多尝试练习思考,练习思考后的经验才是最重要的。