平时工作开发过程中,难免会用到状态机,即状态的流转。
下面是一个简单的模拟状态机:
有些同学会定义为常量,使用 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:
return null;
}
}
/**
* 拒绝后的状态
*/
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)没那么“面向对象”,没那么直观,既然 State 枚举就是用来表示状态,如果同意和拒绝可以通过 State 对象的方法获取就会更优雅一些。
通常状态流转有两种方向,一种是赞同,一种是拒绝,分别流向不同的状态。
由于是本文讨论的是有限状态,我们可以将状态定义为枚举比较契合,除非初态和终态,否则赞同和拒绝都会返回一个状态。
下面只是一个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() {
return null;
}
@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 != null);
// 获取某个状态的赞同和拒绝后的状态
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_D-> approved:null STATE_B approved ->STATE_C STATE_C reject ->STATE_A STATE_D reject ->STATE_A
假如该枚举是外部提供,只提供枚举本身,不提供状态流转,怎么办?
我们依然可以采用 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:
return null;
}
}
/**
* 拒绝后的状态
*/
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
的映射,每次获取下一个状态都是传如当前状态。
因此我们可以联想到使用 Map
来存储这种映射 。
可以使用 链表
来维护这种关系(实现略):
import lombok.Data;
@Data
public class StateNode<T> {
private T state;
private StateNode<T> approveState;
private StateNode<T> rejectState;
}
本文使用 Map 来实现,赞同和拒绝分别使用两个 Map
存储。
为了更好地表达状态的转换,每一个映射定义为 from
和 to
。
以下结合 Map 的数据结构,结合链式编程,给出一个解决方案:
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) {
return APPROVE.getNextState(currentState);
}
/**
* 拒绝后的状态
*/
public static State getRejectedState(State currentState) {
return REJECT.getNextState(currentState);
}
}
输出结果
STATE_A-> approved:STATE_B STATE_B-> approved:STATE_C STATE_C-> approved:STATE_D STATE_D-> approved:null STATE_B approved ->STATE_C STATE_C reject ->STATE_A STATE_D reject ->STATE_A
这种方式更加灵活,可定义多条状态链,实现状态的流转。
以上只是 DEMO,实际编码时,可自行优化。
本文结合自己的理解,给出一种推荐的有限状态机的写法。
给出了自有状态枚举和外部状态枚举的解决方案,希望对大家有帮助。
通过本文,大家也可以看出,简单的问题深入思考,也可以得到不同的解法。
希望大家不要满足现有方案,可以灵活运用所学来解决实践问题。