Java实现有限状态机的推荐方案分享
作者:明明如月学长 发布时间:2022-01-07 15:54:27
目录
一、背景
二、推荐方式
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,实际编码时,可自行优化。
可能还有一些开源的包提供状态机的功能,但核心原理大同小异。
三、总结
本文结合自己的理解,给出一种推荐的有限状态机的写法。
给出了自有状态枚举和外部状态枚举的解决方案,希望对大家有帮助。
通过本文,大家也可以看出,简单的问题深入思考,也可以得到不同的解法。
希望大家不要满足现有方案,可以灵活运用所学来解决实践问题。
来源:https://blog.csdn.net/w605283073/article/details/121345982


猜你喜欢
- 实现多表联合查询还是在david.mybatis.model包下面新建一个Website类,用来持久化数据之用,重写下相应toString(
- 作为开发人员,掌握开发环境下的调试技巧十分有必要。我们在编写java程序的过程中,经常会遇到各种莫名其妙的问题,为了检测程序是哪里出现问题,
- 本文实例讲述了Android编程实现滑动按钮功能。分享给大家供大家参考,具体如下:首先效果图:然后是分别建立三个文件,第一个是main.cl
- 场景我们团队现在面临着多端数据接口对接的问题,为了解决这个问题我们定义了接口对接的规范,前端(安卓,Ios,web前端)和后端进行了数据的格
- 线程池中ThreadGroup的坑在Java中每一个线程都归属于某个线程组管理的一员,例如在主函数main()主工作流程中产生一个线程,则产
- 这是进行Java Web开发必备的一个过程,仅供新手参考,高手可以忽略!JDK 和 JRE 的区别JRE(Java Runtime Envi
- 1.分发对象-MotionEvent事件类型有:1.ACTION_DOWN-----手指刚接触屏幕2.ACTION_MOVE------手指
- Java调用Linux系统命令有时候,我们在使用Java做一些操作时,可能性能上并不能达到我们满意的效果,就拿最近工作中的遇到的一个场景来说
- Vibrator振动器,是手机自带的振动器哦,不要想成岛国用的那种神秘东西哦~~Vibrator是Android给我们提供的用于机身震动的一
- SpringBoot项目上传图片一般是上传至远程服务器存储,开发过程中可能会上传至当前项目的某个静态目录中,此时就会遇到这个问题,文件在上传
- 一.背景在复习《C++基础与提高》时,自己实现运算符重载(i++)时,几次都报错。其实还是自己对运算符重载这一部分内容理解得不够透彻,于是再
- 当游戏在手机/模拟器上卡死,logcat没有日志输出,也没有卡死堆栈信息或者bugly也没有捕获到异常,你是否很焦急?本文介绍一下我们项目中
- 前言由于android M的popupwindow与之前版本不一致,笔者找不到能够代码监听物理返回键的方式,故另寻方式实现筛选菜单。5.0及
- 1、Idea 设置字体settings --> Editor --> Font2、Idea配置MavenSettings --&
- 概念逃逸分析一种数据分析算法,基于此算法可以有效减少 Java 对象在堆内存中的分配。 Hotspot 虚拟机的编译器能够分析出一个新对象的
- 在android中常用存储数据的基本就三种,sqlite,SharedPreferences,文件存储,其中针对于对象存储,使用sqlite
- 前言链表是一种动态的数据结构,因为在创建链表时,不需要知道链表的长度,只需要对指针进行操作。1. 节点的创建 链表的节点包括两部分,分别是:
- 使用RecyclerView设置间距,需要重写RecyclerView.ItemDecoration这个类。有如下的效果图需要实现,间距只有
- 前言最近遇到个小问题,要为几十个文本框添加相同的失去焦点事件,常规的办法是在VS的事件管理器里面添加,但那样太繁琐了,几十个文本框,要加几十
- 简介项目需要...直接展示效果吧:原理使用UGUI提供的ScrollRect和ScrollBar组件实现基本滑动以及自己控制每次移动一页来达