软件编程
位置:首页>> 软件编程>> java编程>> Java实现有限状态机的推荐方案分享

Java实现有限状态机的推荐方案分享

作者:明明如月学长  发布时间:2022-01-07 15:54:27 

标签:java,有限,状态机
目录
  • 一、背景

  • 二、推荐方式

    • 2.1 自定义的枚举

    • 2.2 外部枚举

  • 三、总结

    一、背景

    平时工作开发过程中,难免会用到状态机(状态的流转)。

    如奖学金审批流程、请假审批流程、竞标流程等,都需要根据不同行为转到不同的状态。

    下面是一个简单的模拟状态机:

    Java实现有限状态机的推荐方案分享

    有些同学会选择将状态定义为常量,使用 if else 来流转状态,不太优雅。

    有些同学会考虑将状态定义为枚举。

    但是定义为枚举之后,大多数同学会选择使用 switch 来流转状态:


    import lombok.Getter;

    public enum State {

    STATE_A("A"),
       STATE_B("B"),
       STATE_C("C"),
       STATE_D("D");

    @Getter
       private final String value;

    State(String value) {
           this.value = value;
       }

    public static State getByValue(String value) {
           for (State state : State.values()) {
               if (state.getValue().equals(value)) {
                   return state;
               }
           }
           return null;
       }

    /**
        * 批准后的状态
        */
       public static State getApprovedState(State currentState) {
           switch (currentState) {
               case STATE_A:
                   return STATE_B;
               case STATE_B:
                   return STATE_C;
               case STATE_C:
                   return STATE_D;
               case STATE_D:
               default:
                  throw new IllegalStateException("当前已终态");
           }

    }

    /**
        * 拒绝后的状态
        */
       public static State getRejectedState(State currentState) {
           switch (currentState) {
               case STATE_A:
                   throw new IllegalStateException("当前状态不支持拒绝");
               case STATE_B:
               case STATE_C:
               case STATE_D:
               default:
                   return STATE_A;
           }
       }
    }

    上面这种写法有几个弊端:

    (1) getByValue 每次获取枚举值都要循环一次当前枚举的所有常量,时间复杂度是
    O(N),虽然耗时非常小,但总有些别扭,作为有追求的程序员,应该尽量想办法优化掉。

    (2) 总感觉使用 switch-case 实现状态流转,更多的是面向过程的产物。虽然可以实现功能,但没那么“面向对象”,既然 State 枚举就是用来表示状态,如果同意和拒绝可以通过 State 对象的方法获取就会更直观一些。

    二、推荐方式

    2.1 自定义的枚举

    通常状态流转有两种方向,一种是赞同,一种是拒绝,分别流向不同的状态。

    由于本文讨论的是有限状态,我们可以将状态定义为枚举比较契合,除非初态和终态,否则赞同和拒绝都会返回一个状态。

    下面只是一个DEMO, 实际编码时可以自由发挥。

    该 Demo 的好处是:

    1 使用 CACHE缓存,避免每次通过 value 获取 State都循环 State 枚举数组

    2 定义【同意】和【拒绝】抽象方法,每个 State 通过实现该方法来流转状态。

    3 状态的定义和转换都收拢在一个枚举中,更容易维护

    虽然代码看似更多一些,但是更“面向对象”一些。


    package basic;

    import lombok.Getter;

    import java.util.Arrays;
    import java.util.Map;
    import java.util.function.Function;
    import java.util.stream.Collectors;

    public enum State {
       /**
        * 定义状态,并实现同意和拒绝的流转
        */
       STATE_A("A") {
           @Override
           State getApprovedState() {
               return STATE_B;
           }

    @Override
           State getRejectedState() {
               throw new IllegalStateException("STATE_A 不支持拒绝");
           }
       },
       STATE_B("B") {
           @Override
           State getApprovedState() {
               return STATE_C;
           }

    @Override
           State getRejectedState() {
               return STATE_A;
           }
       },
       STATE_C("C") {
           @Override
           State getApprovedState() {
               return STATE_D;
           }

    @Override
           State getRejectedState() {
               return STATE_A;
           }
       },
       STATE_D("D") {
           @Override
           State getApprovedState() {
                throw new IllegalStateException("当前已终态");
           }

    @Override
           State getRejectedState() {
               return STATE_A;
           }
       };

    @Getter
       private final String value;

    State(String value) {
           this.value = value;
       }

    private static final Map<String, State> CACHE;

    static {
           CACHE = Arrays.stream(State.values()).collect(Collectors.toMap(State::getValue, Function.identity()));
       }

    public static State getByValue(String value) {
           return CACHE.get(value);
       }

    /**
        * 批准后的状态
        */
       abstract State getApprovedState();

    /**
        * 拒绝后的状态
        */
       abstract State getRejectedState();
    }

    测试代码


    package basic;

    import static basic.State.STATE_B;

    public class StateDemo {
       public static void main(String[] args) {
           State state = State.STATE_A;

    // 一直赞同
           State approvedState;
           do {
               approvedState = state.getApprovedState();
               System.out.println(state + "-> approved:" + approvedState);
               state = approvedState;
           } while (state != State.STATE_D);

    // 获取某个状态的赞同和拒绝后的状态
           System.out.println("STATE_B approved ->" + STATE_B.getApprovedState());
           System.out.println("STATE_C reject ->" + State.getByValue("C").getRejectedState());
           System.out.println("STATE_D reject ->" + State.getByValue("D").getRejectedState());
       }
    }

    输出结果:

    STATE_A-> approved:STATE_B
    STATE_B-> approved:STATE_C
    STATE_C-> approved:STATE_D
    -----
    STATE_B approved ->STATE_C
    STATE_C reject ->STATE_A
    STATE_D reject ->STATE_A

    本质上通过不同的方法调用实现自身的流转,而且赞同和拒绝定义为抽象类,可以“强迫”让状态的定义方明确自己的状态流转。

    整体逻辑比较内聚,状态的定义和流转都在 State 类中完成。

    2.2 外部枚举

    假如该枚举是外部提供,只提供枚举常量的定义,不提供状态流转,怎么办?

    我们依然可以采用 switch 的方式实现状态流转:


    import static basic.State.*;

    public class StateUtils {
       /**
        * 批准后的状态
        */
       public static State getApprovedState(State currentState) {
           switch (currentState) {
               case STATE_A:
                   return STATE_B;
               case STATE_B:
                   return STATE_C;
               case STATE_C:
                   return STATE_D;
               case STATE_D:
               default:
               throw new IllegalStateException("当前已经是终态");
           }

    }

    /**
        * 拒绝后的状态
        */
       public static State getRejectedState(State currentState) {
           switch (currentState) {
               case STATE_A:
                   throw new IllegalStateException("当前状态不支持拒绝");
               case STATE_B:
               case STATE_C:
               case STATE_D:
               default:
                   return STATE_A;
           }
       }
    }

    还有更通用、更容易理解的编程方式呢(不用 switch)?

    状态机的每次转换是一个 State 到另外一个 State 的映射,每个状态都应该维护赞同和拒绝后的下一个状态。

    因此,我们很容易会联想到使用【链表】来存储这种关系 。

    由于这里是外部枚举,无法将状态流转在枚举内部完成(定义),就意味着我们还需要自定义状态节点来表示流转,如:


    import lombok.Data;

    @Data
    public class StateNode<T> {

    private T state;

    private StateNode<T> approveNode;

    private StateNode<T> rejectNode;
    }

    这样构造好链表以后,还需在工具类中要构造 State 到 StateNode 的映射(因为对于外部来说,只应该感知 State 类,不应该再去理解 StateNode ) , 提供赞同和拒绝方法,内部通过拿到赞同和拒绝对应的 StateNode 之后拿到对应的 State 返回即可。

    伪代码如下:


    public class StateUtils{

    // 构造 StateNode 链表,和构造 cache Map 略
    private Map<State, StateNode<State>> cache ;

    public State getApproveState(State current){
    StateNode<State> node = cache.get(current);
    return node == null? null: return node.getApproveNode().getState();
    }

    public State getRejectState(State current){
    StateNode<State> node = cache.get(current);
    return node == null? null: return node.getRejectNode().getState();
    }

    }

    整体比较曲折,不如直接将赞同和拒绝定义在 State 枚举内更直观。

    下面给出一种 “状态链模式” 的解决方案。

    赞同和拒绝底层分别使用两个 Map 存储。

    为了更好地表达每次状态的方向(即 Map 中的 key 和 value),每一个映射定义为 from 和 to 。

    为了避免只有 from 没有 to ,定义一个中间类型 SemiData,只有调用 to 之后才可以继续链式编程下去,最终构造出状态链。

    以下结合 Map 的数据结构,结合升级版的 Builder 设计模式,实现链式编程


    package basic;

    import java.util.HashMap;
    import java.util.Map;

    public class StateChain<T> {

    private final Map<T, T> chain;

    private StateChain(Map<T, T> chain) {
           this.chain = chain;
       }

    public T getNextState(T t) {
           return chain.get(t);
       }

    public static <V> Builder<V> builder() {
           return new Builder<V>();
       }

    static class Builder<T> {

    private final Map<T, T> data = new HashMap<>();

    public SemiData<T> from(T state) {
               return new SemiData<>(this, state);
           }

    public StateChain<T> build() {
               return new StateChain<T>(data);
           }

    public static class SemiData<T> {
               private final T key;
               private final Builder<T> parent;

    private SemiData(Builder<T> builder, T key) {
                   this.parent = builder;
                   this.key = key;
               }

    public Builder<T> to(T value) {
                   parent.data.put(key, value);
                   return parent;
               }
           }
       }

    }

    使用案例:


    package basic;

    import static basic.State.*;

    public class StateUtils {

    private static final StateChain<State> APPROVE;
       private static final StateChain<State> REJECT;

    static {
           APPROVE = StateChain.<State>builder().from(STATE_A).to(STATE_B).from(STATE_B).to(STATE_C).from(STATE_C).to(STATE_D).build();

    REJECT = StateChain.<State>builder().from(STATE_B).to(STATE_A).from(STATE_C).to(STATE_A).from(STATE_D).to(STATE_A).build();
       }

    /**
        * 批准后的状态
        */
       public static State getApprovedState(State currentState) {
            State next = APPROVE.getNextState(currentState);
            if(next == null){
               throw new IllegalStateException("当前已经终态");
            }
            return next;
       }

    /**
        * 拒绝后的状态
        */
       public static State getRejectedState(State currentState) {
           State next =  REJECT.getNextState(currentState);
            if(next == null){
               throw new IllegalStateException("当前状态不支持驳回");
            }
            return next;
       }
    }

    测试方法


    import static basic.State.STATE_B;

    public class StateDemo {

    public static void main(String[] args) {
           State state = State.STATE_A;

    // 一直赞同
           State approvedState;
           do {
               approvedState = StateUtils.getApprovedState(state);
               System.out.println(state + "-> approved:" + approvedState);
               state = approvedState;
           } while (state != State.STATE_D);

    System.out.println("-------");

    // 获取某个状态的赞同和拒绝后的状态
           System.out.println("STATE_B approved ->" + StateUtils.getApprovedState(STATE_B));
           System.out.println("STATE_C reject ->" + StateUtils.getRejectedState(State.getByValue("C")));
           System.out.println("STATE_D reject ->" + StateUtils.getRejectedState(State.getByValue("D")));
       }
    }

    输出结果

    STATE_A-> approved:STATE_B
    STATE_B-> approved:STATE_C
    STATE_C-> approved:STATE_D
    ----
    STATE_B approved ->STATE_C
    STATE_C reject ->STATE_A
    STATE_D reject ->STATE_A

    这种方式更加灵活,可定义多条状态链,实现每个链的状态各自流转。而且性能非常好。

    巧妙地将状态的转换定义和 Map 的定义合二为一,既能够表意(from,to 比较明确),又能获得很好的性能(获取赞同和拒绝后的状态转化为
    通过 key 取 Map 中的 value ),还有不错的编程体验(链式编程)。

    以上只是 DEMO,实际编码时,可自行优化。

    可能还有一些开源的包提供状态机的功能,但核心原理大同小异。

    三、总结

    本文结合自己的理解,给出一种推荐的有限状态机的写法。

    给出了自有状态枚举和外部状态枚举的解决方案,希望对大家有帮助。

    通过本文,大家也可以看出,简单的问题深入思考,也可以得到不同的解法。

    希望大家不要满足现有方案,可以灵活运用所学来解决实践问题。

    来源:https://blog.csdn.net/w605283073/article/details/121345982

    0
    投稿

    猜你喜欢

    手机版 软件编程 asp之家 www.aspxhome.com