状态机设计模式

初识

我第一次知道状态机,是在大学学习《数字电子技术基础》的时候。一块控制芯片有若干输入数据总线Data_in,一个CLK时钟震荡输入,还有一定数量的以高低电平组合来控制状态的输入。不同的状态,芯片会对输入的数据进行不同的处理。

再之后是读研时跟着导师做课题,用Verilog HDL写FPGA程序,仿真一些数字信号的处理算法,其中也大量使用了状态机编程。


FPGA
FPGA

还记得有一次和导师沟通科研时,他提及说状态机的这种编程模型,在软件行业也是有所应用的。当时我还是个编程战五渣,也不知道有设计模式这个东西,只是不以为意得应承地点点头。现在想想,还是蛮佩服导师的博学多知的。

再看状态机

状态机的官方定义如下:

The intent of the STATE pattern is to distribute state-specific logic across classes that represent an object’s state.
状态模式是为了将与状态有关的逻辑分写在代表对象状态的类中

我们来通过举例理解这句话。

想象你要实现一个登陆系统,用户将通过以下几个步骤与系统交互。

  1. 连接进登陆界面。

  2. 输入用户名密码,点击登陆

  3. 登陆成功则顺利进入系统,登陆失败则断开连接。

  4. 注销登录,断开连接。


    登录流程图
    登录流程图

    这些步骤我们抽象成状态转移图来看会更加清晰


    登录状态转移图
    登录状态转移图

    更一般的,我们稍微增加些健壮性的操作。
    登录状态转移健壮性增强
    登录状态转移健壮性增强

    这样简单的逻辑,我们可以不假思索得很快的在一份代码中完成。只要使用switch语法,对对象当前的状态做判断,然后在给各个分支中写上各自的逻辑。但是,如果你需要增加一个中间状态,或者修改某一个分支的逻辑时,你将不得不修改这个类的代码,增加case分支,修改逻辑。这违反了软件设计中的“开放封闭原则”。为此,我们将状态模式的概念付诸实施,将与指定状态有关的逻辑操作分别写在对应的可代表状态的类里。

状态机模式

UML视图
UML视图

首先定义一个接口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是一个枚举类,用来限定状态的类型。通过在构造器中传入一个枚举,来指明这个类代表什么状态。

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的多个实现类,表示不同的状态。所有状态都需要实现IState的方法。不同的状态,对不同操作有不一样的实现。

  1. 未连接状态
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("还没有连接,不能登录");
    }

}
  1. 连接状态
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("不是正在登录状态");
    }

    @Override
    public IState loginSuccess(Context context) {
        throw new RuntimeException("不是正在登录状态");
    }

    @Override
    public IState logout(Context context) {
        throw new RuntimeException("不是正在登录状态");
    }
}
  1. 正在登陆状态
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("处在登录中");
    }
}
  1. 进入系统状态
public class LoginIntoSystem extends AbstractState {
    
    public LoginIntoSystem(StateEnum stateEnum) {
        super(stateEnum);
    }
    @Override
    public IState connect(Context context) {
        throw new RuntimeException("已经登录进系统");

    }

    @Override
    public IState beginToLogin(Context context) {
        throw new RuntimeException("已经登录进系统");

    }

    @Override
    public IState loginFailure(Context context) {
        throw new RuntimeException("已经登录进系统");

    }

    @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) {
        IState nextState = Context.UNCONNECTED_STATE;
        System.out.println(String.format("Switch state from %s to %s", context.getState(), nextState));
        return nextState;
    }
}

几个状态类中,有些操作的实现时没有意义的,比如在UnconnectedState,进行logout操作是不符合逻辑的,于是直接抛出异常。

最后需要定义个“环境”类,用来感知当前状态,你可以理解为就是一个状态机。

public class Context {
    // 将各种状态定义成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接口类似的方法。其内部实现时交由当前状态类来实现的。IState接口接收一个Context类实例,在IState的实现类中对其做相应的逻辑处理,再返回给Context下一个状态,并交由Context实例对象进行状态的切换。当然,你也可以直接就在状态类中进行状态切换,就目前而言,我觉得也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

发现问题!

写到这里,我重新审视开发-封闭原则:开放扩展,封闭修改。我们现在如果要增加一个状态,登录超时。我们可以增加一个类继承AbstractState,然后实现各个操作的逻辑。还要在StateEnum中增加一种类型,在Context增加一个类成员变量,同时,为了让这个类派上用场,需要修改与之相关联的状态类的逻辑,让状态有可能转移到登录超时。最少要修改3个类,好吧,这时你心里可能会冒一句:去他丫的开放封闭原则。
那如果突然有个需求,你的登录系统需要有一个输入验证码的Action。你会需要修改IState接口,增加一个验证码输入方法。WTF,所有的实现类都要修改了。这状态模式好像只是解耦了状态和持有状态的对象,将逻辑封装进对应状态类中。但是如果要增加某个状态或者动作,非常有可能面临大量的修改。

此外,StateEnum枚举类有些鸡肋,我们只是通过枚举来限定可能的状态,但此外好像就没什么用了。增加状态时,还需要额外修改这个类。能不能利用下枚举类的单例特性呢?最好能够将Context中的表示状态的类成员也解耦。

这个我想到了办法,之前是通过在实例化状态类是传入StateEnum枚举来限定状态。我现在反过来,在枚举对象实例化时传入状态类,这样每个枚举类本身就封装了一个状态类,而且绝对是单例的。

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;
    }
}

但又有一个问题,假如对于某个状态,我有多种可选的实现类时(比如UnconnectedState1, UnconnectedState2),这个时候想要替换这个类的实现时,我就需要修改StateEnum类了。小菜鸡写的代码,还是没办法尽善尽美啊。

好在有大牛给出了最佳实践——Spring state machine——可以供大家观摩学习。

Spring中的状态机

Spring有一个专门实现了状态机的子项目——spring-statemachine-core,在spring应用中添加如下依赖,开箱即用

        <dependency>
            <groupId>org.springframework.statemachine</groupId>
            <artifactId>spring-statemachine-core</artifactId>
            <version>1.2.0.RELEASE</version>
        </dependency>

使用spring来实现状态机,能够更进一步解耦功能类,让代码结构层次更加清晰。下面大致实现一个小的Demo。

  1. 定义状态枚举
public enum RegStatusEnum {
    // 未连接
    UNCONNECTED,
    // 已连接
    CONNECTED,
    // 正在登录
    LOGINING,
    // 登录进系统
    LOGIN_INTO_SYSTEM;
}
  1. 定义事件枚举,事件的发生触发状态转换
public enum RegEventEnum {
    // 连接
    CONNECT,
    // 开始登录
    BEGIN_TO_LOGIN,
    // 登录成功
    LOGIN_SUCCESS,
    // 登录失败
    LOGIN_FAILURE,
    // 注销登录
    LOGOUT;
}
  1. 配置状态机,通过注解打开状态机功能。配置类一般要继承EnumStateMachineConfigurerAdapter类,并且重写一些configure方法以配置状态机的初始状态以及事件与状态转移的联系。
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 // 开启状态机配置
public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<RegStatusEnum, RegEventEnum>{

    /**
     * 配置状态机状态
     */
    @Override
    public void configure(StateMachineStateConfigurer<RegStatusEnum, RegEventEnum> states) throws Exception {
        states.withStates()
        // 初始化状态机状态
        .initial(RegStatusEnum.UNCONNECTED)
        // 指定状态机的所有状态
        .states(EnumSet.allOf(RegStatusEnum.class));
    }

    /**
     * 配置状态机状态转换
     */
    @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);
    }
}
  1. 配置事件监听器,事件发生时会触发的操作
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");
    }
}
  1. 通过注解自动装配一个状态机,这里写了一个rest接口来触发状态机变化
@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事件发生时,状态机是UNCONNECTED状态,没有与LOGOUT事件关联的状态转移,故不操作。

使用spring实现的状态机将类之间的关系全部交由了IOC容器做管理,实现了真正意义上的解耦。果然Spring大法好啊。

参考

Spring状态机参考文档

服务端指南 | 状态机设计

状态模式——State (更好的实现状态机)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,724评论 6 342
  • 懂道理 最近自己一直在考虑的一个问题,比如我。我在犯什么错误的时候,...
    f961ff2e749a阅读 446评论 4 0
  • 人与人之间的关系总是很奇妙,有的人即使相识颇久,也不过是能见面打声招呼,而有的人却是相见恨晚的。 真儿姐姐与我一般...
    筱梦依866阅读 467评论 0 0
  • 决定我们生活方式的并不是过去的经历,而是我们自己赋予经历的意义。(无论过去发生什么,那都不起决定作用,过去有没有精...
    掉鱼的猫阅读 326评论 0 0