
初識(shí)
我第一次知道狀態(tài)機(jī),是在大學(xué)學(xué)習(xí)《數(shù)字電子技術(shù)基礎(chǔ)》的時(shí)候。一塊控制芯片有若干輸入數(shù)據(jù)總線Data_in,一個(gè)CLK時(shí)鐘震蕩輸入,還有一定數(shù)量的以高低電平組合來控制狀態(tài)的輸入。不同的狀態(tài),芯片會(huì)對(duì)輸入的數(shù)據(jù)進(jìn)行不同的處理。
再之后是讀研時(shí)跟著導(dǎo)師做課題,用Verilog HDL寫FPGA程序,仿真一些數(shù)字信號(hào)的處理算法,其中也大量使用了狀態(tài)機(jī)編程。

還記得有一次和導(dǎo)師溝通科研時(shí),他提及說狀態(tài)機(jī)的這種編程模型,在軟件行業(yè)也是有所應(yīng)用的。當(dāng)時(shí)我還是個(gè)編程戰(zhàn)五渣,也不知道有設(shè)計(jì)模式這個(gè)東西,只是不以為意得應(yīng)承地點(diǎn)點(diǎn)頭?,F(xiàn)在想想,還是蠻佩服導(dǎo)師的博學(xué)多知的。
再看狀態(tài)機(jī)
狀態(tài)機(jī)的官方定義如下:
The intent of the STATE pattern is to distribute state-specific logic across classes that represent an object’s state.
狀態(tài)模式是為了將與狀態(tài)有關(guān)的邏輯分寫在代表對(duì)象狀態(tài)的類中
我們來通過舉例理解這句話。
想象你要實(shí)現(xiàn)一個(gè)登陸系統(tǒng),用戶將通過以下幾個(gè)步驟與系統(tǒng)交互。
連接進(jìn)登陸界面。
輸入用戶名密碼,點(diǎn)擊登陸
登陸成功則順利進(jìn)入系統(tǒng),登陸失敗則斷開連接。
-
注銷登錄,斷開連接。
登錄流程圖
這些步驟我們抽象成狀態(tài)轉(zhuǎn)移圖來看會(huì)更加清晰
登錄狀態(tài)轉(zhuǎn)移圖
更一般的,我們稍微增加些健壯性的操作。
登錄狀態(tài)轉(zhuǎn)移健壯性增強(qiáng)
這樣簡(jiǎn)單的邏輯,我們可以不假思索得很快的在一份代碼中完成。只要使用switch語(yǔ)法,對(duì)對(duì)象當(dāng)前的狀態(tài)做判斷,然后在給各個(gè)分支中寫上各自的邏輯。但是,如果你需要增加一個(gè)中間狀態(tài),或者修改某一個(gè)分支的邏輯時(shí),你將不得不修改這個(gè)類的代碼,增加case分支,修改邏輯。這違反了軟件設(shè)計(jì)中的“開放封閉原則”。為此,我們將狀態(tài)模式的概念付諸實(shí)施,將與指定狀態(tài)有關(guān)的邏輯操作分別寫在對(duì)應(yīng)的可代表狀態(tài)的類里。
狀態(tài)機(jī)模式

首先定義一個(gè)接口IState,指定所有的動(dòng)作(Action)
/**
* the interface of state, input parameter is target state machine,
* and return the next state
* @author simple
* 2017年11月6日 上午10:29:58
*/
public interface IState {
public IState connect(Context context);
public IState beginToLogin(Context context);
public IState loginFailure(Context context);
public IState loginSuccess(Context context);
public IState logout(Context context);
}
定義一個(gè)抽象類,封裝一些公共方法和實(shí)例成員
public abstract class AbstractState implements IState{
private StateEnum stateEnum;
public AbstractState(StateEnum stateEnum)
{
this.stateEnum = stateEnum;
}
public StateEnum getStateEnum() {
return stateEnum;
}
public void setStateEnum(StateEnum stateEnum) {
this.stateEnum = stateEnum;
}
public String toString()
{
return(stateEnum.toString());
}
}
StateEnum是一個(gè)枚舉類,用來限定狀態(tài)的類型。通過在構(gòu)造器中傳入一個(gè)枚舉,來指明這個(gè)類代表什么狀態(tài)。
public enum StateEnum {
UNCONNECTED(0, "UNCONNECTED"),
CONNECTED(1, "CONNECTED"),
LOGINING(2, "LOGINING"),
LOGIN_INTO_SYSTEM(3, "LOGIN_INTO_SYSTEM");
private int key;
private String stateStr;
StateEnum(int key, String stateStr)
{
this.key = key;
this.stateStr = stateStr;
}
void printState()
{
System.out.println(String.format("current state: %d: %s", this.key, this.stateStr));
}
}
通過繼承AbstractState來定義IState的多個(gè)實(shí)現(xiàn)類,表示不同的狀態(tài)。所有狀態(tài)都需要實(shí)現(xiàn)IState的方法。不同的狀態(tài),對(duì)不同操作有不一樣的實(shí)現(xiàn)。
- 未連接狀態(tài)
public class UnconnectedState extends AbstractState{
public UnconnectedState(StateEnum stateEnum) {
super(stateEnum);
}
@Override
public IState connect(Context context) {
IState nextState = Context.CONNECTED_STATE;
System.out.println(String.format("Switch state from %s to %s", context.getState(), nextState));
return nextState;
}
@Override
public IState beginToLogin(Context context) {
throw new RuntimeException("還沒有連接,不能登錄");
}
@Override
public IState loginFailure(Context context) {
throw new RuntimeException("還沒有連接,不能登錄");
}
@Override
public IState loginSuccess(Context context) {
throw new RuntimeException("還沒有連接,不能登錄");
}
@Override
public IState logout(Context context) {
throw new RuntimeException("還沒有連接,不能登錄");
}
}
- 連接狀態(tài)
public class ConnectedState extends AbstractState {
public ConnectedState(StateEnum stateEnum)
{
super(stateEnum);
}
@Override
public IState connect(Context context) {
IState nextState = Context.CONNECTED_STATE;
System.out.println(String.format("已經(jīng)連接了,Switch state from %s to %s", context.getState(), nextState));
return nextState;
}
@Override
public IState beginToLogin(Context context) {
IState nextState = Context.LOGINING_STATE;
System.out.println(String.format("Switch state from %s to %s", context.getState(), nextState));
return nextState;
}
@Override
public IState loginFailure(Context context) {
throw new RuntimeException("不是正在登錄狀態(tài)");
}
@Override
public IState loginSuccess(Context context) {
throw new RuntimeException("不是正在登錄狀態(tài)");
}
@Override
public IState logout(Context context) {
throw new RuntimeException("不是正在登錄狀態(tài)");
}
}
- 正在登陸狀態(tài)
public class LoginingState extends AbstractState {
public LoginingState(StateEnum stateEnum) {
super(stateEnum);
}
@Override
public IState connect(Context context) {
throw new RuntimeException("處在登錄中");
}
@Override
public IState beginToLogin(Context context) {
IState nextState = Context.LOGINING_STATE;
System.out.println(String.format("已經(jīng)連接并且正在登錄,Switch state from %s to %s", context.getState(), nextState));
return nextState;
}
@Override
public IState loginFailure(Context context) {
IState nextState = Context.UNCONNECTED_STATE;
System.out.println(String.format("Switch state from %s to %s", context.getState(), nextState));
return nextState;
}
@Override
public IState loginSuccess(Context context) {
IState nextState = Context.LOGIN_INTO_SYSTEM_STATE;
System.out.println(String.format("Switch state from %s to %s", context.getState(), nextState));
return nextState;
}
@Override
public IState logout(Context context) {
throw new RuntimeException("處在登錄中");
}
}
- 進(jìn)入系統(tǒng)狀態(tài)
public class LoginIntoSystem extends AbstractState {
public LoginIntoSystem(StateEnum stateEnum) {
super(stateEnum);
}
@Override
public IState connect(Context context) {
throw new RuntimeException("已經(jīng)登錄進(jìn)系統(tǒng)");
}
@Override
public IState beginToLogin(Context context) {
throw new RuntimeException("已經(jīng)登錄進(jìn)系統(tǒng)");
}
@Override
public IState loginFailure(Context context) {
throw new RuntimeException("已經(jīng)登錄進(jìn)系統(tǒng)");
}
@Override
public IState loginSuccess(Context context) {
IState nextState = Context.LOGIN_INTO_SYSTEM_STATE;
System.out.println(String.format("已經(jīng)登錄進(jìn)系統(tǒng)了,Switch state from %s to %s", context.getState(), nextState));
return nextState;
}
@Override
public IState logout(Context context) {
IState nextState = Context.UNCONNECTED_STATE;
System.out.println(String.format("Switch state from %s to %s", context.getState(), nextState));
return nextState;
}
}
幾個(gè)狀態(tài)類中,有些操作的實(shí)現(xiàn)時(shí)沒有意義的,比如在UnconnectedState,進(jìn)行l(wèi)ogout操作是不符合邏輯的,于是直接拋出異常。
最后需要定義個(gè)“環(huán)境”類,用來感知當(dāng)前狀態(tài),你可以理解為就是一個(gè)狀態(tài)機(jī)。
public class Context {
// 將各種狀態(tài)定義成Context的類成員變量,保持單例
public static final IState UNCONNECTED_STATE = new UnconnectedState(StateEnum.UNCONNECTED);
public static final IState CONNECTED_STATE = new ConnectedState(StateEnum.CONNECTED);
public static final IState LOGINING_STATE = new LoginingState(StateEnum.LOGINING);
public static final IState LOGIN_INTO_SYSTEM_STATE = new LoginIntoSystem(StateEnum.LOGIN_INTO_SYSTEM);
private IState state;
public Context(IState initState)
{
initState(initState);
}
public void connect()
{
setState(this.state.connect(this));
}
public void beginToLogin()
{
setState(this.state.beginToLogin(this));
}
public void loginFailure()
{
setState(this.state.loginFailure(this));
}
public void loginSuccess()
{
setState(this.state.loginSuccess(this));
}
public void logout()
{
setState(this.state.logout(this));
}
public void initState(IState state)
{
this.setState(state);
}
public void setState(IState state)
{
this.state = state;
}
public IState getState()
{
return this.state;
}
}
Context類中有與IState接口類似的方法。其內(nèi)部實(shí)現(xiàn)時(shí)交由當(dāng)前狀態(tài)類來實(shí)現(xiàn)的。IState接口接收一個(gè)Context類實(shí)例,在IState的實(shí)現(xiàn)類中對(duì)其做相應(yīng)的邏輯處理,再返回給Context下一個(gè)狀態(tài),并交由Context實(shí)例對(duì)象進(jìn)行狀態(tài)的切換。當(dāng)然,你也可以直接就在狀態(tài)類中進(jìn)行狀態(tài)切換,就目前而言,我覺得也ok。
通過一個(gè)客戶端,讓我們來看看效果
public static void main(String[] args) {
Context context = new Context(Context.UNCONNECTED_STATE);
context.connect();
context.beginToLogin();
context.loginFailure();
context.connect();
context.beginToLogin();
context.loginSuccess();
context.logout();
}
>>>>>>>>>>>>>>>輸出>>>>>>>>>>>>>>>>>>>>>
Switch state from UNCONNECTED to CONNECTED
Switch state from CONNECTED to LOGINING
Switch state from LOGINING to UNCONNECTED
Switch state from UNCONNECTED to CONNECTED
Switch state from CONNECTED to LOGINING
Switch state from LOGINING to LOGIN_INTO_SYSTEM
Switch state from LOGIN_INTO_SYSTEM to UNCONNECTED
發(fā)現(xiàn)問題!
寫到這里,我重新審視開發(fā)-封閉原則:開放擴(kuò)展,封閉修改。我們現(xiàn)在如果要增加一個(gè)狀態(tài),登錄超時(shí)。我們可以增加一個(gè)類繼承AbstractState,然后實(shí)現(xiàn)各個(gè)操作的邏輯。還要在StateEnum中增加一種類型,在Context增加一個(gè)類成員變量,同時(shí),為了讓這個(gè)類派上用場(chǎng),需要修改與之相關(guān)聯(lián)的狀態(tài)類的邏輯,讓狀態(tài)有可能轉(zhuǎn)移到登錄超時(shí)。最少要修改3個(gè)類,好吧,這時(shí)你心里可能會(huì)冒一句:去他丫的開放封閉原則。
那如果突然有個(gè)需求,你的登錄系統(tǒng)需要有一個(gè)輸入驗(yàn)證碼的Action。你會(huì)需要修改IState接口,增加一個(gè)驗(yàn)證碼輸入方法。WTF,所有的實(shí)現(xiàn)類都要修改了。這狀態(tài)模式好像只是解耦了狀態(tài)和持有狀態(tài)的對(duì)象,將邏輯封裝進(jìn)對(duì)應(yīng)狀態(tài)類中。但是如果要增加某個(gè)狀態(tài)或者動(dòng)作,非常有可能面臨大量的修改。
此外,StateEnum枚舉類有些雞肋,我們只是通過枚舉來限定可能的狀態(tài),但此外好像就沒什么用了。增加狀態(tài)時(shí),還需要額外修改這個(gè)類。能不能利用下枚舉類的單例特性呢?最好能夠?qū)ontext中的表示狀態(tài)的類成員也解耦。
這個(gè)我想到了辦法,之前是通過在實(shí)例化狀態(tài)類是傳入StateEnum枚舉來限定狀態(tài)。我現(xiàn)在反過來,在枚舉對(duì)象實(shí)例化時(shí)傳入狀態(tài)類,這樣每個(gè)枚舉類本身就封裝了一個(gè)狀態(tài)類,而且絕對(duì)是單例的。
public enum StateEnum {
UNCONNECTED(0, "UNCONNECTED" , new UnconnectedState()),
CONNECTED(1, "CONNECTED", new ConnectedState()),
LOGINING(2, "LOGINING", new LoginingState()),
LOGIN_INTO_SYSTEM(3, "LOGIN_INTO_SYSTEM", new LoginIntoSystem());
private final int key;
private final String stateStr;
private final IState state;
StateEnum(int key, String stateStr, IState state)
{
this.key = key;
this.stateStr = stateStr;
this.state = state;
}
void printState()
{
System.out.println(String.format("current state: %d: %s", this.key, this.stateStr));
}
public IState getState()
{
return state;
}
}
但又有一個(gè)問題,假如對(duì)于某個(gè)狀態(tài),我有多種可選的實(shí)現(xiàn)類時(shí)(比如UnconnectedState1, UnconnectedState2),這個(gè)時(shí)候想要替換這個(gè)類的實(shí)現(xiàn)時(shí),我就需要修改StateEnum類了。小菜雞寫的代碼,還是沒辦法盡善盡美啊。
好在有大牛給出了最佳實(shí)踐——Spring state machine——可以供大家觀摩學(xué)習(xí)。
Spring中的狀態(tài)機(jī)
Spring有一個(gè)專門實(shí)現(xiàn)了狀態(tài)機(jī)的子項(xiàng)目——spring-statemachine-core,在spring應(yīng)用中添加如下依賴,開箱即用
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-core</artifactId>
<version>1.2.0.RELEASE</version>
</dependency>
使用spring來實(shí)現(xiàn)狀態(tài)機(jī),能夠更進(jìn)一步解耦功能類,讓代碼結(jié)構(gòu)層次更加清晰。下面大致實(shí)現(xiàn)一個(gè)小的Demo。
- 定義狀態(tài)枚舉
public enum RegStatusEnum {
// 未連接
UNCONNECTED,
// 已連接
CONNECTED,
// 正在登錄
LOGINING,
// 登錄進(jìn)系統(tǒng)
LOGIN_INTO_SYSTEM;
}
- 定義事件枚舉,事件的發(fā)生觸發(fā)狀態(tài)轉(zhuǎn)換
public enum RegEventEnum {
// 連接
CONNECT,
// 開始登錄
BEGIN_TO_LOGIN,
// 登錄成功
LOGIN_SUCCESS,
// 登錄失敗
LOGIN_FAILURE,
// 注銷登錄
LOGOUT;
}
- 配置狀態(tài)機(jī),通過注解打開狀態(tài)機(jī)功能。配置類一般要繼承EnumStateMachineConfigurerAdapter類,并且重寫一些configure方法以配置狀態(tài)機(jī)的初始狀態(tài)以及事件與狀態(tài)轉(zhuǎn)移的聯(lián)系。
import static com.qyz.dp.state.events.RegEventEnum.BEGIN_TO_LOGIN;
import static com.qyz.dp.state.events.RegEventEnum.CONNECT;
import static com.qyz.dp.state.events.RegEventEnum.LOGIN_FAILURE;
import static com.qyz.dp.state.events.RegEventEnum.LOGIN_SUCCESS;
import static com.qyz.dp.state.events.RegEventEnum.LOGOUT;
import static com.qyz.dp.state.state.RegStatusEnum.CONNECTED;
import static com.qyz.dp.state.state.RegStatusEnum.LOGINING;
import static com.qyz.dp.state.state.RegStatusEnum.LOGIN_INTO_SYSTEM;
import static com.qyz.dp.state.state.RegStatusEnum.UNCONNECTED;
import java.util.EnumSet;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import com.qyz.dp.state.events.RegEventEnum;
import com.qyz.dp.state.state.RegStatusEnum;
@Configuration
@EnableStateMachine // 開啟狀態(tài)機(jī)配置
public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<RegStatusEnum, RegEventEnum>{
/**
* 配置狀態(tài)機(jī)狀態(tài)
*/
@Override
public void configure(StateMachineStateConfigurer<RegStatusEnum, RegEventEnum> states) throws Exception {
states.withStates()
// 初始化狀態(tài)機(jī)狀態(tài)
.initial(RegStatusEnum.UNCONNECTED)
// 指定狀態(tài)機(jī)的所有狀態(tài)
.states(EnumSet.allOf(RegStatusEnum.class));
}
/**
* 配置狀態(tài)機(jī)狀態(tài)轉(zhuǎn)換
*/
@Override
public void configure(StateMachineTransitionConfigurer<RegStatusEnum, RegEventEnum> transitions) throws Exception {
// 1. connect UNCONNECTED -> CONNECTED
transitions.withExternal()
.source(UNCONNECTED)
.target(CONNECTED)
.event(CONNECT)
// 2. beginToLogin CONNECTED -> LOGINING
.and().withExternal()
.source(CONNECTED)
.target(LOGINING)
.event(BEGIN_TO_LOGIN)
// 3. login failure LOGINING -> UNCONNECTED
.and().withExternal()
.source(LOGINING)
.target(UNCONNECTED)
.event(LOGIN_FAILURE)
// 4. login success LOGINING -> LOGIN_INTO_SYSTEM
.and().withExternal()
.source(LOGINING)
.target(LOGIN_INTO_SYSTEM)
.event(LOGIN_SUCCESS)
// 5. logout LOGIN_INTO_SYSTEM -> UNCONNECTED
.and().withExternal()
.source(LOGIN_INTO_SYSTEM)
.target(UNCONNECTED)
.event(LOGOUT);
}
}
- 配置事件監(jiān)聽器,事件發(fā)生時(shí)會(huì)觸發(fā)的操作
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.annotation.OnTransition;
import org.springframework.statemachine.annotation.WithStateMachine;
@Configuration
@WithStateMachine
public class StateMachineEventConfig {
@OnTransition(source = "UNCONNECTED", target = "CONNECTED")
public void connect() {
System.out.println("Switch state from UNCONNECTED to CONNECTED: connect");
}
@OnTransition(source = "CONNECTED", target = "LOGINING")
public void beginToLogin() {
System.out.println("Switch state from CONNECTED to LOGINING: beginToLogin");
}
@OnTransition(source = "LOGINING", target = "LOGIN_INTO_SYSTEM")
public void loginSuccess() {
System.out.println("Switch state from LOGINING to LOGIN_INTO_SYSTEM: loginSuccess");
}
@OnTransition(source = "LOGINING", target = "UNCONNECTED")
public void loginFailure() {
System.out.println("Switch state from LOGINING to UNCONNECTED: loginFailure");
}
@OnTransition(source = "LOGIN_INTO_SYSTEM", target = "UNCONNECTED")
public void logout()
{
System.out.println("Switch state from LOGIN_INTO_SYSTEM to UNCONNECTED: logout");
}
}
- 通過注解自動(dòng)裝配一個(gè)狀態(tài)機(jī),這里寫了一個(gè)rest接口來觸發(fā)狀態(tài)機(jī)變化
@RestController
public class WebApi {
@Autowired
private StateMachine<RegStatusEnum, RegEventEnum> stateMachine;
@GetMapping(value = "/testStateMachine")
public void testStateMachine()
{
stateMachine.start();
stateMachine.sendEvent(RegEventEnum.CONNECT);
stateMachine.sendEvent(RegEventEnum.BEGIN_TO_LOGIN);
stateMachine.sendEvent(RegEventEnum.LOGIN_FAILURE);
stateMachine.sendEvent(RegEventEnum.LOGOUT);
}
}
>>>>>>>>>>>>>>>>>>>>>>>輸出結(jié)果>>>>>>>>>>>>>>>>>>>>>>>>>>
Switch state from UNCONNECTED to CONNECTED: connect
Switch state from CONNECTED to LOGINING: beginToLogin
Switch state from LOGINING to UNCONNECTED: loginFailure
從輸出可以看到,雖然send了4個(gè)事件,但只有三條輸出。原因是最后一個(gè)LOGOUT事件發(fā)生時(shí),狀態(tài)機(jī)是UNCONNECTED狀態(tài),沒有與LOGOUT事件關(guān)聯(lián)的狀態(tài)轉(zhuǎn)移,故不操作。
使用spring實(shí)現(xiàn)的狀態(tài)機(jī)將類之間的關(guān)系全部交由了IOC容器做管理,實(shí)現(xiàn)了真正意義上的解耦。果然Spring大法好啊。