初识
我第一次知道状态机,是在大学学习《数字电子技术基础》的时候。一块控制芯片有若干输入数据总线Data_in,一个CLK时钟震荡输入,还有一定数量的以高低电平组合来控制状态的输入。不同的状态,芯片会对输入的数据进行不同的处理。
再之后是读研时跟着导师做课题,用Verilog HDL写FPGA程序,仿真一些数字信号的处理算法,其中也大量使用了状态机编程。
还记得有一次和导师沟通科研时,他提及说状态机的这种编程模型,在软件行业也是有所应用的。当时我还是个编程战五渣,也不知道有设计模式这个东西,只是不以为意得应承地点点头。现在想想,还是蛮佩服导师的博学多知的。
再看状态机
状态机的官方定义如下:
The intent of the STATE pattern is to distribute state-specific logic across classes that represent an object’s state.
状态模式是为了将与状态有关的逻辑分写在代表对象状态的类中
我们来通过举例理解这句话。
想象你要实现一个登陆系统,用户将通过以下几个步骤与系统交互。
连接进登陆界面。
输入用户名密码,点击登陆
登陆成功则顺利进入系统,登陆失败则断开连接。
-
注销登录,断开连接。
这些步骤我们抽象成状态转移图来看会更加清晰
更一般的,我们稍微增加些健壮性的操作。
这样简单的逻辑,我们可以不假思索得很快的在一份代码中完成。只要使用switch语法,对对象当前的状态做判断,然后在给各个分支中写上各自的逻辑。但是,如果你需要增加一个中间状态,或者修改某一个分支的逻辑时,你将不得不修改这个类的代码,增加case分支,修改逻辑。这违反了软件设计中的“开放封闭原则”。为此,我们将状态模式的概念付诸实施,将与指定状态有关的逻辑操作分别写在对应的可代表状态的类里。
状态机模式
首先定义一个接口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的方法。不同的状态,对不同操作有不一样的实现。
- 未连接状态
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("还没有连接,不能登录");
}
}
- 连接状态
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("不是正在登录状态");
}
}
- 正在登陆状态
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("处在登录中");
}
}
- 进入系统状态
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。
- 定义状态枚举
public enum RegStatusEnum {
// 未连接
UNCONNECTED,
// 已连接
CONNECTED,
// 正在登录
LOGINING,
// 登录进系统
LOGIN_INTO_SYSTEM;
}
- 定义事件枚举,事件的发生触发状态转换
public enum RegEventEnum {
// 连接
CONNECT,
// 开始登录
BEGIN_TO_LOGIN,
// 登录成功
LOGIN_SUCCESS,
// 登录失败
LOGIN_FAILURE,
// 注销登录
LOGOUT;
}
- 配置状态机,通过注解打开状态机功能。配置类一般要继承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);
}
}
- 配置事件监听器,事件发生时会触发的操作
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");
}
}
- 通过注解自动装配一个状态机,这里写了一个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大法好啊。