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

简介: 平时工作开发过程中,难免会用到状态机(状态的流转)。如奖学金审批状态流转、请假审批状态流转、竞标状态流转等,都需要根据不同行为转到不同的状态。有什么好的编码方式吗?

一、背景

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

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

注: 本文主要讲的是状态机,即状态从一个状态转变为另外一个状态。如果设置 3个人审批才可以从 A状态 转到 B 状态,那么3个人审批是状态转换的条件。如果想实现流程编排,建议使用工作流引擎。

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

在这里插入图片描述

有些同学会选择将状态定义为常量,使用 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();
}
注:这里的抽象方法可以定义参数,枚举中实现方法可以根据参数决定是否执行状态流转。 比如参数可以传入当前已经同意的人数,枚举中可以判断当审批同意的人数大于3时,返回下一个状态。

测试代码

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;
}

这样构造好链表以后,还需在工具类中要构造 StateStateNode 的映射(因为对于外部来说,只应该感知 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),每一个映射定义为 fromto

为了避免只有 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,实际编码时,可自行优化。

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

三、总结

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

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

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

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

创作不易,如果本文对你有帮助,欢迎点赞、收藏加关注,你的支持和鼓励,是我创作的最大动力。
在这里插入图片描述
相关文章
|
19天前
|
监控 Java
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
82 17
|
30天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
15天前
|
缓存 安全 算法
Java 多线程 面试题
Java 多线程 相关基础面试题
|
1月前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
1月前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
1月前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
60 3
|
1月前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
173 2
|
1月前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
54 6
|
2月前
|
设计模式 Java 开发者
Java多线程编程的陷阱与解决方案####
本文深入探讨了Java多线程编程中常见的问题及其解决策略。通过分析竞态条件、死锁、活锁等典型场景,并结合代码示例和实用技巧,帮助开发者有效避免这些陷阱,提升并发程序的稳定性和性能。 ####
|
1月前
|
存储 监控 小程序
Java中的线程池优化实践####
本文深入探讨了Java中线程池的工作原理,分析了常见的线程池类型及其适用场景,并通过实际案例展示了如何根据应用需求进行线程池的优化配置。文章首先介绍了线程池的基本概念和核心参数,随后详细阐述了几种常见的线程池实现(如FixedThreadPool、CachedThreadPool、ScheduledThreadPool等)的特点及使用场景。接着,通过一个电商系统订单处理的实际案例,分析了线程池参数设置不当导致的性能问题,并提出了相应的优化策略。最终,总结了线程池优化的最佳实践,旨在帮助开发者更好地利用Java线程池提升应用性能和稳定性。 ####