美文网首页
有限状态机的4种Java实现对比

有限状态机的4种Java实现对比

作者: Java柱柱 | 来源:发表于2020-09-04 17:48 被阅读0次

    写在前面:2020年面试必备的Java后端进阶面试题总结了一份复习指南在Github上,内容详细,图文并茂,有需要学习的朋友可以Star一下!
    GitHub地址:https://github.com/abel-max/Java-Study-Note/tree/master

    在日常工作过程中,我们经常会遇到状态的变化场景,例如订单状态发生变化,商品状态的变化。这些状态的变化,我们称为有限状态机,缩写为FSM( F State Machine).。之所以称其为有限,是因为这些场景中的状态往往是可以枚举出来的有限个的,所以称其为有限状态机。下面我们来看一个具体的场景例子。

    简单场景:

    地铁进站闸口的状态有两个:已经关闭、已经开启两个状态。刷卡后闸口从已关闭变为已开启,人通过后闸口状态从已开启变为已关闭。

    01 遇到这类问题,在编码时我们应该如何处理呢?

    • 基于Switch
    • 基于状态集合
    • 基于State模式
    • 基于枚举的实现

    下面我们针对每一种实现方式进行分析。场景分解后会有一下2种状态4种情况出现:

    image.png

    针对以上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:操作失败
    

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

    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的实现,我们看到每一种是实现都有优点和它的不足。那么在日常工作中,如何选择呢,我个人认为可以遵循一下两个建议:

    1. 遵循Simple Design。如果没有一个外部参考,那么用哪一种都不为过。所以引入一个原则作为参考,可以更好的帮助我们做决定。这里日常工作中我们经常使用Simple Design:通过测试、揭示意图、消除重复、最少元素。并在实现过程中不断重构,代码是重构出来的,而不是一次性的设计出来的。

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

    来源: https://juejin.im/post/5dff7595f265da33d645bc63

    相关文章

      网友评论

          本文标题:有限状态机的4种Java实现对比

          本文链接:https://www.haomeiwen.com/subject/axctektx.html