原文地址:服務端指南 | 狀態機設計
部落格地址:blog.720ui.com/
狀態機中,每個狀態有著相應的行為,隨著行為的觸發來切換狀態。其中一種做法是使用二維陣列實現狀態機機制,其中橫座標表示行為,縱座標表示狀態,具體的數值則表示當前的狀態。
我們以登入場景設計一個狀態機。
![](https://i.iter01.com/images/3f5c6cd6ce105c66ea66d900442eac2747634d1993744d07a15150639fad5709.png)
這時,我們設計一張狀態機表。
![](https://i.iter01.com/images/92882b757a013a9ea83d6f0bf717299f863acb84f2cbe3be50b4f836ac8278b6.png)
那麼,此時它的二維陣列,如下所示。
![](https://i.iter01.com/images/f00faede1616d26c840f56ff5063f492967a4d01812968a245cea2efc2a56e61.png)
此外,我們也可以通過狀態模式實現一個狀態機。狀態模式將每一個狀態封裝成獨立的類,具體行為會隨著內部狀態而改變。狀態模式用類表示狀態,這樣我們就能通過切換類來方便地改變物件的狀態,避免了冗長的條件分支語句,讓系統具有更好的靈活性和可擴充套件性。
現在,我們定義一個狀態列舉,其中包括未連線、已連線、註冊中、已註冊 4 種狀態。
public enum StateEnum {
// 未連線
UNCONNECT(1, "UNCONNECT"),
// 已連線
CONNECT(2, "CONNECT"),
// 註冊中
REGISTING(3, "REGISTING"),
// 已註冊
REGISTED(4, "REGISTED");
private int key;
private String value;
StateEnum(int key, String value) {
this.key = key;
this.value = value;
}
public int getKey() {return key;}
public String getValue() {return value;}
}複製程式碼
定義一個環境類,它是實際上是真正擁有狀態的物件。
public class Context {
private State state;
public void connect(){
state.connect(this);
System.out.println("STATE : " + state.getCurState());
}
public void register(){
state.register(this);
System.out.println("STATE : " + state.getCurState());
}
public void registerSuccess(){
state.registerSuccess(this);
System.out.println("STATE : " + state.getCurState());
}
public void registerFailed(){
state.registerFailed(this);
System.out.println("STATE : " + state.getCurState());
}
public void unRegister(){
state.unRegister(this);
System.out.println("STATE : " + state.getCurState());
}
public State getState() {
return state;
}
public void setState(State state) {
this.state = state;
}
}複製程式碼
狀態模式用類表示狀態,這樣我們就能通過切換類來方便地改變物件的狀態。現在,我們定義幾個狀態類。
public interface State {
void connect(Context c);
void register(Context c);
void registerSuccess(Context c);
void registerFailed(Context c);
void unRegister(Context c);
String getCurState();
}
public class UnconnectState implements State {
@Override
public void connect(Context c) {
c.setState(new ConnectState());
}
@Override
public void register(Context c) {
throw new RuntimeException("INVALID_OPERATE_ERROR");
}
@Override
public void registerSuccess(Context c) {
throw new RuntimeException("INVALID_OPERATE_ERROR");
}
@Override
public void registerFailed(Context c) {
throw new RuntimeException("INVALID_OPERATE_ERROR");
}
@Override
public void unRegister(Context c) {
throw new RuntimeException("INVALID_OPERATE_ERROR");
}
@Override
public String getCurState() {
return StateEnum.UNCONNECT.toString();
}
}
public class ConnectState implements State {
@Override
public void connect(Context c) {
c.setState(new ConnectState());
}
@Override
public void register(Context c) {
c.setState(new RegistingState());
}
@Override
public void registerSuccess(Context c) {
throw new RuntimeException("INVALID_OPERATE_ERROR");
}
@Override
public void registerFailed(Context c) {
throw new RuntimeException("INVALID_OPERATE_ERROR");
}
@Override
public void unRegister(Context c) {
c.setState(new UnconnectState());
}
@Override
public String getCurState() {
return StateEnum.CONNECT.toString();
}
}
public class RegistingState implements State {
@Override
public void connect(Context c) {
throw new RuntimeException("INVALID_OPERATE_ERROR");
}
@Override
public void register(Context c) {
c.setState(new RegistingState());
}
@Override
public void registerSuccess(Context c) {
c.setState(new RegistedState());
}
@Override
public void registerFailed(Context c) {
c.setState(new UnconnectState());
}
@Override
public void unRegister(Context c) {
c.setState(new UnconnectState());
}
@Override
public String getCurState() {
return StateEnum.REGISTING.toString();
}
}
public class RegistedState implements State {
@Override
public void connect(Context c) {
throw new RuntimeException("INVALID_OPERATE_ERROR");
}
@Override
public void register(Context c) {
throw new RuntimeException("INVALID_OPERATE_ERROR");
}
@Override
public void registerSuccess(Context c) {
c.setState(new RegistedState());
}
@Override
public void registerFailed(Context c) {
throw new RuntimeException("INVALID_OPERATE_ERROR");
}
@Override
public void unRegister(Context c) {
c.setState(new UnconnectState());
}
@Override
public String getCurState() {
return StateEnum.REGISTED.toString();
}
}複製程式碼
注意的是,如果某個行為不會觸發狀態的變化,我們可以丟擲一個 RuntimeException 異常。此外,呼叫時,通過環境類控制狀態的切換,如下所示。
public class Client {
public static void main(String[] args) {
Context context = new Context();
context.connect();
context.register();
}
}複製程式碼
Spring StateMachine 讓狀態機結構更加層次化,可以幫助開發者簡化狀態機的開發過程。現在,我們來用 Spring StateMachine 進行改造。修改 pom 檔案,新增 Maven 依賴。
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-core</artifactId>
<version>1.2.0.RELEASE</version>
</dependency>複製程式碼
定義一個狀態列舉,其中包括未連線、已連線、註冊中、已註冊 4 種狀態。
public enum RegStatusEnum {
// 未連線
UNCONNECTED,
// 已連線
CONNECTED,
// 註冊中
REGISTERING,
// 已註冊
REGISTERED;
}複製程式碼
定義一個行為列舉,其中包括連線、註冊、註冊成功、註冊失敗、登出 5 種行為事件。
public enum RegEventEnum {
// 連線
CONNECT,
// 註冊
REGISTER,
// 註冊成功
REGISTER_SUCCESS,
// 註冊失敗
REGISTER_FAILED,
// 登出
UN_REGISTER;
}複製程式碼
接著,我們需要進行狀態機配置,其中 @EnableStateMachine 註解,標識啟用 Spring StateMachine 狀態機功能。
@Configuration
@EnableStateMachine
public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<RegStatusEnum, RegEventEnum> {
}複製程式碼
我們需要初始化狀態機的狀態。其中,initial(RegStatusEnum.UNCONNECTED) 定義了初始狀態是未連線狀態。states(EnumSet.allOf(RegStatusEnum.class)) 定義了狀態機中存在的所有狀態。
@Override
public void configure(StateMachineStateConfigurer<RegStatusEnum, RegEventEnum> states) throws Exception {
states.withStates()
// 定義初始狀態
.initial(RegStatusEnum.UNCONNECTED)
// 定義狀態機狀態
.states(EnumSet.allOf(RegStatusEnum.class));
}複製程式碼
我們需要初始化當前狀態機有哪些狀態事件。其中, source 指定原始狀態,target 指定目標狀態,event 指定觸發事件。
@Override
public void configure(StateMachineTransitionConfigurer<RegStatusEnum, RegEventEnum> transitions)
throws Exception {
// 1.連線事件
// 未連線 -> 已連線
.withExternal()
.source(RegStatusEnum.UNCONNECTED)
.target(RegStatusEnum.CONNECTED)
.event(RegEventEnum.CONNECT)
.and()
.withExternal()
.source(RegStatusEnum.CONNECTED)
.target(RegStatusEnum.CONNECTED)
.event(RegEventEnum.CONNECT)
.and()
// 2.註冊事件
// 已連線 -> 註冊中
.withExternal()
.source(RegStatusEnum.CONNECTED)
.target(RegStatusEnum.REGISTERING)
.event(RegEventEnum.REGISTER)
.and()
.withExternal()
.source(RegStatusEnum.REGISTERING)
.target(RegStatusEnum.REGISTERING)
.event(RegEventEnum.REGISTER)
.and()
// 3.註冊成功事件
// 註冊中 -> 已註冊
.withExternal()
.source(RegStatusEnum.REGISTERING)
.target(RegStatusEnum.REGISTERED)
.event(RegEventEnum.REGISTER_SUCCESS)
.and()
.withExternal()
.source(RegStatusEnum.REGISTERED)
.target(RegStatusEnum.REGISTERED)
.event(RegEventEnum.REGISTER_SUCCESS)
.and()
// 4.註冊失敗事件
// 註冊中 -> 未連線
.withExternal()
.source(RegStatusEnum.REGISTERING)
.target(RegStatusEnum.UNCONNECTED)
.event(RegEventEnum.REGISTER_FAILED)
.and()
// 5.登出事件
// 已連線 -> 未連線
.withExternal()
.source(RegStatusEnum.CONNECTED)
.target(RegStatusEnum.UNCONNECTED)
.event(RegEventEnum.UN_REGISTER)
.and()
// 註冊中 -> 未連線
.withExternal()
.source(RegStatusEnum.REGISTERING)
.target(RegStatusEnum.UNCONNECTED)
.event(RegEventEnum.UN_REGISTER)
.and()
// 已註冊 -> 未連線
.withExternal()
.source(RegStatusEnum.REGISTERED)
.target(RegStatusEnum.UNCONNECTED)
.event(RegEventEnum.UN_REGISTER)
;
}複製程式碼
Spring StateMachine 提供了註解配置實現方式,所有 StateMachineListener 介面中定義的事件都能通過註解的方式來進行配置實現。這裡以連線事件為案例,@OnTransition 中 source 指定原始狀態,target 指定目標狀態,當事件觸發時將會被監聽到從而呼叫 connect() 方法。
@WithStateMachine
public class StateMachineEventConfig {
@OnTransition(source = "UNCONNECTED", target = "CONNECTED")
public void connect() {
System.out.println("///////////////////");
System.out.println("連線事件, 未連線 -> 已連線");
System.out.println("///////////////////");
}
@OnTransition(source = "CONNECTED", target = "REGISTERING")
public void register() {
System.out.println("///////////////////");
System.out.println("註冊事件, 已連線 -> 註冊中");
System.out.println("///////////////////");
}
@OnTransition(source = "REGISTERING", target = "REGISTERED")
public void registerSuccess() {
System.out.println("///////////////////");
System.out.println("註冊成功事件, 註冊中 -> 已註冊");
System.out.println("///////////////////");
}
@OnTransition(source = "REGISTERED", target = "UNCONNECTED")
public void unRegister() {
System.out.println("///////////////////");
System.out.println("登出事件, 已註冊 -> 未連線");
System.out.println("///////////////////");
}
}複製程式碼
Spring StateMachine 讓狀態機結構更加層次化,我們來回顧下幾個核心步驟:第一步,定義狀態列舉。第二步,定義事件列舉。第三步,定義狀態機配置,設定初始狀態,以及狀態與事件之間的關係。第四步,定義狀態監聽器,當狀態變更時,觸發方法。
(完)
更多精彩文章,盡在「服務端思維」微信公眾號!
![](https://i.iter01.com/images/62be312cfb4d9a91edcbeac9d7d2f8d017445d038e05dc45928b3928a92a1b61.png)