如何写一个状态机


状态机是软件工程中很常见的一种架构。它是记录事物发展阶段的一个常用手段。首先来介绍下状态机的几个概念。(我们拿“管理员登陆后台”作为例子描述这些概念)

  1. State —— 状态机一般要包含两个以上状态。对于管理员登陆,这里有两个状态:“登录状态和未登录状态”。
  2. Event —— 事件就是执行某个操作的触发条件或者口令。对于上面的例子,“点击登录按钮”就是一个事件。
  3. Action —— 事件发生以后要执行动作。例如事件是“点击登录按钮”,动作是“登陆中”。编程的时候,一个 Action 一般就对应一个函数。
  4. Transition —— 也就是从一个状态变化为另一个状态。例如“登陆成功”就是一个变换。

一个典型的状态机转换过程如下图:

img1

上面简单介绍了状态机的概念,那么在不同的业务领域如何定义这些状态机的概念呢?

如何定义状态机的概念

在定义状态的过程中,其实就是一个抽象概念的过程。如果了解过逻辑学的都知道,现实生活中我们描述的事物其实都可以抽象为命题。命题本质上就是状态机的State,Event就是命题的条件,Action本质上就是通过命题和条件的推导过程。而Transition就是命题推导完成的结论。所以状态机变化的过程本质上就是一个命题的证明过程。P1(Condition) —> P2

所以当我们拿到业务的时候,首先要分离出哪些是已知的命题,哪些是条件。而我们就是要通过这些已知命题和条件,推导出结论的过程。

在定义完状态机的概念后,我们下面看下这些概念的使用原则。

State使用规则

State至少会有两种,一种是默认的状态,一种是输出的状态,默认的状态相当于已知的命题,输出的状态相当于要推导的结论。所以很重要的一点是在定义State这种数据类型的时候,要选择对外不可变的类型,一旦出现可变类型的状态说明状态机的推导过程有一定缺陷。

对于管理员登陆的例子,输出的是登陆和未登录的状态。我们可以这样定义状态。


enum {
    unLogin,
    login
} LoginState

Class userLoginState {
    final LoginState loginstate // state

}

Event的规则

Event相当于命题的条件,因为条件往往是用来做推导过程的,并不需求保存在状态机中,所以如果涉及到Event这种变量不要用对外可见的变量。在状态机中,需要存储的往往是推导的结论,表示State的变量会是对外可见的变量。所以在写状态机时,要注意区分哪些是Event用到的变量,哪些是State用到的,便于变量的定义。

对于管理员登陆的例子,“用户点击登陆按钮”这个Event,这个是状态机触发的条件,从这个条件命题中,我们可以分离出来一个属性变量(User)和一个方法(clickLoginButton)。下面就根据Event规则定义这些条件。


enum {
    unLogin,
    login
} LoginState

// Event条件使用的变量,尽量不要做对外暴露
Class UserInfo {
    final String name
    final String password
}

Class userLoginState {
    final LoginState loginstate // state

    // 状态机的触发事件,往往是需要外界来调用的,这里用一个函数来表示,有时候可以用一个变量来表示。
    clickLoginButton () {
        // event check and read data
        UserInfo user = new user('name','passwork')
    }
}

Action的规则

Action就是我们推导的过程了。Action大多是无副作用的函数。何为无副作用的函数,简单的讲就是不会操作任何全局变量的函数,因为Action仅仅是计算的过程,还没有涉及到最终的结果命题的生成。另外由于状态机往往是对某一个事物时间周期内的计算,所以Action尽量用函数闭合的操作。什么是闭合操作,就是函数输入的数据类型和输出的数据类型尽量保证一直,这样在命题的推导过程中,就避免了类型的转换。

对于上面的例子,“登陆行为”这个Action如何合理的表示,首先我们拿到了状态机的触发条件,同时要操作的Event的条件数据是UserInfo。根据上面的原则就很容易定义Action的操作。


enum {
    unLogin,
    login
} LoginState

Class UserInfo {
    final String name
    final String password
}

Class userLoginState {
    final LoginState loginstate // state

    clickLoginButton () {
        // event check and read data
        UserInfo user = new user('name','passwork')

        // action
        login(user)
    }

    private login(UserInfo user) {
        // some request
        loginRequest(user,function(user){
            
        })
    }

    // 这个就相当于做了闭合操作,使用的函数参数和返回的参数一致,这里要注意的一点user尽量不可变的变量进行传递防止中间状态出现。
    private loginRequest(UserInfo user,callback) {
        //request
        callback(user)
    }
}

Transition的规则

Transition是和Action相反的,是有副作用的函数,因为Transition就是生成我们需要推导的命题,所以必然会操作全局的State。在定义有副作用的函数时,我们尤其要小心,尽量用最小化原则,避免一些其他变量的关联操作。这里还有个原则在生产最终的State时,可能会有很多中间状态,中间状态尤其不能对外开放,一定是在内部使用,否则状态机很容易在计算的过程中,中间状态被外部改掉,造成状态机错乱。

我们再来看管理员登陆的例子,用户在登陆过程中可能会产生那些有副作用的函数和中间状态。显而易见户登陆成功后的操作函数应该是会改变loginstate。另外在登录过程中,为了防止频繁的请求,对正在登录的重复请求做拦截,这个就相当于产生了一个中间状态,使用这个中间状态尤其要小心,因为这会造成我们原本没有副作用的Action的函数,产生了副作用。


enum {
    unLogin,
    login
} LoginState

Class UserInfo {
    final String name
    final String password
}

Class userLoginState {
    final LoginState loginstate // state

    private BOOL isLogining // 中间状态,不要对外暴露只是内部使用

    clickLoginButton () {
        // event check and read data
        UserInfo user = new user('name','passwork')

        // action
        login(user)
    }

    private login(UserInfo user) {
        if (self.islogining) {
            return;
        }
        self.islogining = YES
        // some request
        loginRequest(user,function(user){
            self.islogining = NO
            transitionState(success)
        })
    }

    // 这个就相当于做了闭合操作,使用的函数参数和返回的参数一致,这里要注意的一点user尽量不可变的变量进行传递防止中间状态出现。
    private loginRequest(UserInfo user,callback) {
        //request
        callback(user)
    }

    // 有副作用的函数,尽量用最小化原则,所以islogining这种中间状态不要写到这个函数里。
    private transitionState(BOOL isSuccess) {
        // Transition
        self.loginstate = isSuccess?login:unlogin
    }
}

总结

从上面分析状态机的过程中,可以看出来任何的数据结构和算法其实都是逻辑推导的过程,还是文章开头表示的这个公式P1(Condition) —> P2,所以本质上首先要确定哪些是已知的命题,哪些是条件,哪些是结论,从而形成我们的推导过程。

状态机描述起来其实是一个很简单的数据结构和算法,在推导的过程中,我们引入了程序设计中常用的原则,控制了推导过程的边界,本质上是让状态机推导代码更加内聚,不要暴露过多的细节给外部环境,这样的状态机才能更加稳定的运行。

如果你喜欢这篇文章,谢谢你的赞赏

图3

如有疑问请联系我