初識
我第一次知道狀態(tài)機遮婶,是在大學學習《數字電子技術基礎》的時候旗扑。一塊控制芯片有若干輸入數據總線Data_in肩豁,一個CLK時鐘震蕩輸入清钥,還有一定數量的以高低電平組合來控制狀態(tài)的輸入。不同的狀態(tài)缕坎,芯片會對輸入的數據進行不同的處理谜叹。
再之后是讀研時跟著導師做課題,用Verilog HDL寫FPGA程序艳悔,仿真一些數字信號的處理算法猜年,其中也大量使用了狀態(tài)機編程乔外。
還記得有一次和導師溝通科研時杨幼,他提及說狀態(tài)機的這種編程模型尚卫,在軟件行業(yè)也是有所應用的欣喧。當時我還是個編程戰(zhàn)五渣莲组,也不知道有設計模式這個東西,只是不以為意得應承地點點頭∏妈荆現(xiàn)在想想撵孤,還是蠻佩服導師的博學多知的。
再看狀態(tài)機
狀態(tài)機的官方定義如下:
The intent of the STATE pattern is to distribute state-specific logic across classes that represent an object’s state.
狀態(tài)模式是為了將與狀態(tài)有關的邏輯分寫在代表對象狀態(tài)的類中
我們來通過舉例理解這句話竭望。
想象你要實現(xiàn)一個登陸系統(tǒng)邪码,用戶將通過以下幾個步驟與系統(tǒng)交互。
連接進登陸界面咬清。
輸入用戶名密碼闭专,點擊登陸
登陸成功則順利進入系統(tǒng)旧烧,登陸失敗則斷開連接影钉。
-
注銷登錄,斷開連接掘剪。
這些步驟我們抽象成狀態(tài)轉移圖來看會更加清晰
更一般的平委,我們稍微增加些健壯性的操作。
這樣簡單的邏輯夺谁,我們可以不假思索得很快的在一份代碼中完成廉赔。只要使用switch語法肉微,對對象當前的狀態(tài)做判斷,然后在給各個分支中寫上各自的邏輯蜡塌。但是碉纳,如果你需要增加一個中間狀態(tài),或者修改某一個分支的邏輯時馏艾,你將不得不修改這個類的代碼村象,增加case分支,修改邏輯攒至。這違反了軟件設計中的“開放封閉原則”厚者。為此,我們將狀態(tài)模式的概念付諸實施迫吐,將與指定狀態(tài)有關的邏輯操作分別寫在對應的可代表狀態(tài)的類里库菲。
狀態(tài)機模式
首先定義一個接口IState,指定所有的動作(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);
}
定義一個抽象類志膀,封裝一些公共方法和實例成員
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是一個枚舉類熙宇,用來限定狀態(tài)的類型。通過在構造器中傳入一個枚舉溉浙,來指明這個類代表什么狀態(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的多個實現(xiàn)類,表示不同的狀態(tài)戳稽。所有狀態(tài)都需要實現(xiàn)IState的方法馆蠕。不同的狀態(tài),對不同操作有不一樣的實現(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("已經連接了,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("已經連接并且正在登錄乓序,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("處在登錄中");
}
}
- 進入系統(tǒng)狀態(tài)
public class LoginIntoSystem extends AbstractState {
public LoginIntoSystem(StateEnum stateEnum) {
super(stateEnum);
}
@Override
public IState connect(Context context) {
throw new RuntimeException("已經登錄進系統(tǒng)");
}
@Override
public IState beginToLogin(Context context) {
throw new RuntimeException("已經登錄進系統(tǒng)");
}
@Override
public IState loginFailure(Context context) {
throw new RuntimeException("已經登錄進系統(tǒng)");
}
@Override
public IState loginSuccess(Context context) {
IState nextState = Context.LOGIN_INTO_SYSTEM_STATE;
System.out.println(String.format("已經登錄進系統(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;
}
}
幾個狀態(tài)類中,有些操作的實現(xiàn)時沒有意義的替劈,比如在UnconnectedState寄雀,進行l(wèi)ogout操作是不符合邏輯的,于是直接拋出異常抬纸。
最后需要定義個“環(huán)境”類咙俩,用來感知當前狀態(tài),你可以理解為就是一個狀態(tài)機。
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接口類似的方法膜蛔。其內部實現(xiàn)時交由當前狀態(tài)類來實現(xiàn)的。IState接口接收一個Context類實例脖阵,在IState的實現(xiàn)類中對其做相應的邏輯處理皂股,再返回給Context下一個狀態(tài),并交由Context實例對象進行狀態(tài)的切換命黔。當然呜呐,你也可以直接就在狀態(tài)類中進行狀態(tài)切換,就目前而言悍募,我覺得也ok蘑辑。
通過一個客戶端,讓我們來看看效果
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ā)-封閉原則:開放擴展,封閉修改喜鼓。我們現(xiàn)在如果要增加一個狀態(tài)副砍,登錄超時。我們可以增加一個類繼承AbstractState庄岖,然后實現(xiàn)各個操作的邏輯豁翎。還要在StateEnum中增加一種類型,在Context增加一個類成員變量隅忿,同時心剥,為了讓這個類派上用場,需要修改與之相關聯(lián)的狀態(tài)類的邏輯硼控,讓狀態(tài)有可能轉移到登錄超時刘陶。最少要修改3個類,好吧牢撼,這時你心里可能會冒一句:去他丫的開放封閉原則。
那如果突然有個需求疑苫,你的登錄系統(tǒng)需要有一個輸入驗證碼的Action熏版。你會需要修改IState接口,增加一個驗證碼輸入方法捍掺。WTF撼短,所有的實現(xiàn)類都要修改了。這狀態(tài)模式好像只是解耦了狀態(tài)和持有狀態(tài)的對象挺勿,將邏輯封裝進對應狀態(tài)類中曲横。但是如果要增加某個狀態(tài)或者動作,非常有可能面臨大量的修改。
此外禾嫉,StateEnum枚舉類有些雞肋灾杰,我們只是通過枚舉來限定可能的狀態(tài),但此外好像就沒什么用了熙参。增加狀態(tài)時艳吠,還需要額外修改這個類。能不能利用下枚舉類的單例特性呢孽椰?最好能夠將Context中的表示狀態(tài)的類成員也解耦昭娩。
這個我想到了辦法,之前是通過在實例化狀態(tài)類是傳入StateEnum枚舉來限定狀態(tài)黍匾。我現(xiàn)在反過來栏渺,在枚舉對象實例化時傳入狀態(tài)類,這樣每個枚舉類本身就封裝了一個狀態(tài)類锐涯,而且絕對是單例的迈嘹。
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;
}
}
但又有一個問題,假如對于某個狀態(tài)全庸,我有多種可選的實現(xiàn)類時(比如UnconnectedState1, UnconnectedState2)秀仲,這個時候想要替換這個類的實現(xiàn)時,我就需要修改StateEnum類了壶笼。小菜雞寫的代碼神僵,還是沒辦法盡善盡美啊。
好在有大牛給出了最佳實踐——Spring state machine——可以供大家觀摩學習覆劈。
Spring中的狀態(tài)機
Spring有一個專門實現(xiàn)了狀態(tài)機的子項目——spring-statemachine-core保礼,在spring應用中添加如下依賴,開箱即用
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-core</artifactId>
<version>1.2.0.RELEASE</version>
</dependency>
使用spring來實現(xiàn)狀態(tài)機责语,能夠更進一步解耦功能類炮障,讓代碼結構層次更加清晰。下面大致實現(xiàn)一個小的Demo坤候。
- 定義狀態(tài)枚舉
public enum RegStatusEnum {
// 未連接
UNCONNECTED,
// 已連接
CONNECTED,
// 正在登錄
LOGINING,
// 登錄進系統(tǒng)
LOGIN_INTO_SYSTEM;
}
- 定義事件枚舉胁赢,事件的發(fā)生觸發(fā)狀態(tài)轉換
public enum RegEventEnum {
// 連接
CONNECT,
// 開始登錄
BEGIN_TO_LOGIN,
// 登錄成功
LOGIN_SUCCESS,
// 登錄失敗
LOGIN_FAILURE,
// 注銷登錄
LOGOUT;
}
- 配置狀態(tài)機,通過注解打開狀態(tài)機功能白筹。配置類一般要繼承EnumStateMachineConfigurerAdapter類智末,并且重寫一些configure方法以配置狀態(tài)機的初始狀態(tài)以及事件與狀態(tài)轉移的聯(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)機配置
public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<RegStatusEnum, RegEventEnum>{
/**
* 配置狀態(tài)機狀態(tài)
*/
@Override
public void configure(StateMachineStateConfigurer<RegStatusEnum, RegEventEnum> states) throws Exception {
states.withStates()
// 初始化狀態(tài)機狀態(tài)
.initial(RegStatusEnum.UNCONNECTED)
// 指定狀態(tài)機的所有狀態(tài)
.states(EnumSet.allOf(RegStatusEnum.class));
}
/**
* 配置狀態(tài)機狀態(tài)轉換
*/
@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ā)生時會觸發(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");
}
}
- 通過注解自動裝配一個狀態(tài)機系馆,這里寫了一個rest接口來觸發(fā)狀態(tài)機變化
@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);
}
}
>>>>>>>>>>>>>>>>>>>>>>>輸出結果>>>>>>>>>>>>>>>>>>>>>>>>>>
Switch state from UNCONNECTED to CONNECTED: connect
Switch state from CONNECTED to LOGINING: beginToLogin
Switch state from LOGINING to UNCONNECTED: loginFailure
從輸出可以看到,雖然send了4個事件顽照,但只有三條輸出由蘑。原因是最后一個LOGOUT事件發(fā)生時,狀態(tài)機是UNCONNECTED狀態(tài),沒有與LOGOUT事件關聯(lián)的狀態(tài)轉移尼酿,故不操作爷狈。
使用spring實現(xiàn)的狀態(tài)機將類之間的關系全部交由了IOC容器做管理,實現(xiàn)了真正意義上的解耦谓媒。果然Spring大法好啊淆院。