状态机 java_Java有限状态机的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

T01Given:一个Locked的进站闸口

When: 投入硬币

Then:打开闸口

T02Given:一个Locked的进站闸口

When: 通过闸口

Then:警告提示

T03Given:一个Unocked的进站闸口

When: 通过闸口

Then:闸口关闭

T04Given:一个Unlocked的进站闸口

When: 投入硬币

Then:退还硬币

T05Given:一个闸机口

When: 非法操作

Then:操作失败项目中共有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() {

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时,会让系统变得晦涩难懂,脆弱,不易修改。

上面的代码虽然出现了多层嵌套但是还算是结构简单,不过想通过并不能很清楚闸机口的逻辑还是化点时间。如果闸机口的状态等多一些,那就阅读、理解起来也就更加困难。

所以在日常工作,我遵循“事不过三,三则重构”的原则:事不过三:

当只有一两个状态(或者重复)时,那么先用最简单的实现实现。

一旦出现三种以及以上的状态(或者重复),立即重构。

01.02 State模式

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的堆积问题。

01.03 使用状态集合

状态集合是将一组描述状态变化的事务元素组成的集合。

集合中的每一个元素包含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 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 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需要我们每次在脑子中组织一次状态的顺序和规则,而集合能够很直观的表达出这个规则。

01.04 使用Enum的来实现状态机

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方法。

02 总结

通过上面4中对FSM的实现,我们看到每一种是实现都有优点和它的不足。那么在日常工作中,如何选择呢,我个人认为可以遵循一下两个建议:遵循Simple Design。如果没有一个外部参考,那么用哪一种都不为过。所以引入一个原则作为参考,可以更好的帮助我们做决定。这里日常工作中我们经常使用Simple Design:通过测试、揭示意图、消除重复、最少元素。并在实现过程中不断重构,代码是重构出来的,而不是一次性的设计出来的。

在状态机的实现上多做尝试。例子只是一个简单的场景,所以只能看到简单场景下的实现效果,实际业务线上的状态会非常丰富,而且每种状态中可真行的动作也是不同的。所以针对特定场景遇到的问题,多尝试练习思考,练习思考后的经验才是最重要的。

参考《敏捷开发实践》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值