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

目录
  • 一、背景
  • 二、推荐方式
    • 2.1 自定义的枚举
    • 2.2 外部枚举
  • 三、总结

一、背景

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

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

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

有些同学会选择将状态定义为常量,使用 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,实际编码时,可自行优化。

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

三、总结

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

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

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

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

到此这篇关于Java实现有限状态机的推荐方案的文章就介绍到这了,更多相关Java实现有限状态机内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java如何通过枚举实现有限状态机

    首先在枚举类中 定义state 和定义的抽象方法. public enum JavaPlatformState { // 定义state OPEN{ @Override void exit(JavaPlatformMachine pm){super.exit(pm);} @Override void valid(JavaPlatformMachine pm){ this.exit(pm); if(pm.data.getValid_()){ pm.state =STEP1; }else{ NotF

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

    目录 一.背景 二.推荐方式 2.1 自定义的枚举 2.2 外部枚举 三.总结 一.背景 平时工作开发过程中,难免会用到状态机(状态的流转). 如奖学金审批流程.请假审批流程.竞标流程等,都需要根据不同行为转到不同的状态. 下面是一个简单的模拟状态机: 有些同学会选择将状态定义为常量,使用 if else 来流转状态,不太优雅. 有些同学会考虑将状态定义为枚举. 但是定义为枚举之后,大多数同学会选择使用 switch 来流转状态: import lombok.Getter; public enu

  • Java对象转换的方案分享

    目录 前言 为什么模型要分这么多层? 模型之间的转换 建议不要用的方式 常用的方式 使用方式 定义对象 BeanCopier 最简单的使用方式 创建可复用的BeanCopier工具类 MapStruct 引入mapstruct 简单Demo 常见用法 性能测试 测试代码 测试结果 最后 前言 系统变的复杂,系统的层次划分越来越细,边界也越来越明确. 然后每一层之间一般都有自己要处理的领域对象,统称为pojo一般在model或者domain包下(类的后缀不能为pojo). 常见的一些模型类型: P

  • Android加载长图的多种方案分享

    背景介绍 在某些特定场景下,我们需要考虑加载长图的需求,比如加载一幅<清明上河图>,这个好像有点过分了,那就加载1/2的<清明上河图>吧... 那TMD还不是一样道理. 言归正传说一下我这边遇到的情况,之前有图片或大图的模块是划分为H5来实现的,现在需求变更划分为原生开发,那么问题就来了. 图片尺寸为 图片大小为 这一刻我是懵逼的,哪个端图片上传的时候没限制尺寸和压缩?mdzz, 吐槽归吐槽,还是要撸起袖子解决加载长图大图的问题. 先提供几个技术方案来对比一下: 方案1:WebVi

  • 老生常谈Java异常处理和设计(推荐)

    在程序设计中,进行异常处理是非常关键和重要的一部分.一个程序的异常处理框架的好坏直接影响到整个项目的代码质量以及后期维护成本和难度.试想一下,如果一个项目从头到尾没有考虑过异常处理,当程序出错从哪里寻找出错的根源?但是如果一个项目异常处理设计地过多,又会严重影响到代码质量以及程序的性能.因此,如何高效简洁地设计异常处理是一门艺术,本文下面先讲述Java异常机制最基础的知识,然后给出在进行Java异常处理设计时的几个建议. 若有不正之处,请多多谅解和指正,不胜感激. 以下是本文的目录大纲: 一.什

  • java反射深入剖析(推荐)

    本篇文章依旧采用小例子来说明,因为我始终觉的,案例驱动是最好的,要不然只看理论的话,看了也不懂,不过建议大家在看完文章之后,在回过头去看看理论,会有更好的理解. 下面开始正文. [案例1]通过一个对象获得完整的包名和类名 package Reflect; /** * 通过一个对象获得完整的包名和类名 * */ class Demo{ //other codes... } class hello{ public static void main(String[] args) { Demo demo

  • Core Java 简单谈谈HashSet(推荐)

    同学们在看这个问题的时候,我先提出者两个问题,然后大家带着问题看这个文章会理解的更好. 1.HashSet为什么添加元素时不能添加重复元素? 2.HashSet是否添加null元素? 打开源码, 我们看到如下代码,我们看到HashSet也有一个HashMap做为属性,HashSet()的构造方法就是将这个map实例化.如果大家对HashMap还不了解话,可以看我的这篇博文.还要注意有一个静态final的对象PRESENT,这个是干什么用的,咱们继续往下看. private transient H

  • java编程常用技术(推荐)

    一:将String字符串放在最前面 防止发生NullPointerException异常,我们通常把String字符串放在equals方法的左边来比较,这样可以有效的避免 空指针异常的发生. 第一种情况,如果variable为Null,则会发生空指针异常情况:第二种情况即使variable为Null,也不会发生空指针异常,而且不会丢失任何数据.所以建议 大家编程时把直接量放在左边. 二:不要相信早期的JDK API 在早期编程中,JDK API还不是很成熟,例如下面代码块: File file=

  • 70行Java代码实现深度神经网络算法分享

    对于现在流行的深度学习,保持学习精神是必要的--程序员尤其是架构师永远都要对核心技术和关键算法保持关注和敏感,必要时要动手写一写掌握下来,先不用关心什么时候用到--用不用是政治问题,会不会写是技术问题,就像军人不关心打不打的问题,而要关心如何打赢的问题. 程序员如何学习机器学习 对程序员来说,机器学习是有一定门槛的(这个门槛也是其核心竞争力),相信很多人在学习机器学习时都会为满是数学公式的英文论文而头疼,甚至可能知难而退.但实际上机器学习算法落地程序并不难写,下面是70行代码实现的反向多层(BP

  • Java实现接口限流方案

    本文实例为大家分享了Java实现接口限流方案的具体代码,供大家参考,具体内容如下 RateLimiter Google开源工具包Guava提供了限流工具类RateLimiter,基于令牌桶算法实现. 1.maven依赖: <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>27.1-jre</versio

  • jar包加密方案分享

    目录 前言 基础环境准备 开始加密 运行加密后的jar包 后记 其他的加密方案参考: 前言 jar包相信大家都很熟悉,是通过打包java工程而获得的产物,但是jar包是有一个致命的缺点的,那就是很容易被反编译,只需要使用jd-gui就可以很容易的获取到java源码. 如果你想要防止别人反编译做逆向工程,那么对jar包进行一次加密就是一项很重要的工作了. 开源jar加密分为两种一种代码混淆,一种字节码转换. 字节码混淆,主要思路就是的对类名,变量名和方法名,局部参数名进行替换,让其命名变得无意义,

随机推荐