状态
本文只讨论计算机里面的状态,并且只是讨论对象,对象其实是抽象的产物,所以状态也取决于我们是如何对对象进行抽象和建模的,根据建模方法不同,对象也不同。对象分为有状态的对象和无状态的对象,无状态对象特指那种特性形态固定不变的对象,通常他们在面向对象领域都是单例的,而有状态对象不同,有状态对象通常是多例的。举个例子
- 有状态对象:Session对象,Session的概念在很多系统中会存在,因为它对每一次会话都会建立该次会话的特征
- 无状态对象:UserFactory对象,作为一个工厂对象,通常就是无状态的
以前的老系统EJB,有激活和钝化的概念,但现在的编程系统都基本自己负责实现有状态对象的存储了,DDD中有一个概念就是rebuild模式,就是对象的重建。简单的状态是不需要关注的,但是如果要根据状态进行驱动,那么就要对状态进行管理,下面介绍几种状态驱动的模型,第一种是普通的状态,第二种是状态机,第三种是有限Action的状态管理器
普通枚举状态
这种方式的状态是最简单的对象状态表示,我们时不时都会用上,例如你会在一个实例类中加一个字段:boolean isFinished ;用作表示对象是否已经是完成状态,或者:boolean isRunning;用做表示对象是否在运行状态,这种状态一般都是可枚举完的,而且不像状态模式是状态之间的扭转,而是由调用者设置属性状态,例如 obj.setFinished ;
注意,你可以不必拘泥于一种方式组织枚举状态,你也可以用一个栈组织这些状态,例如:
/** * * 代表一个对话管理的目标状态 * **/ private Stack<Goal> interGoal = new Stack(); public void addGoal (Goal newGoal){ interGoal.push(newGoal); } public void doGoal (){ Goal currentGoal = interGoal.peek(); currentGoal.doGoal; interGoal.poll; }
如上,对象的状态是对话目标,对话目标可以用栈来表示,这样的好处是:状态可以记录,回退;所以状态的表示,只需要你巧妙利用数据结构,就能达到一些不可思议的效果。
FMS 有限状态机
状态机是相对一个系统而言的,其实准确来说,是一个比较核心的对象而言,这个对象的职责和功能,是通过状态机这种数学模型建模得以运作。例如我们的聚合根对象。
状态机,很多文章介绍会说直接等同于有限状态机(FMS),FSM 解决一个输入序列,经过 FSM,最终停留在什么状态这样一个问题。比较注重的是系统(对象)的状态,和状态之间的转换,状态是历史输入的一种结果,对于一个系统而言,状态机基本包含以下几种模型:
- 事件(Event)
- 动作(Action)
- 状态(State):现态,次态
- 转换
状态 State:状态机的状态是有限的,例如一个机器人对话系统的Session对象,就可以抽象出几种状态:
- 初始状态;
- 思考决策状态;
- 动作执行状态;
- 待用户回复状态;
- 待服务返回状态;
- 结束状态。
而当前时刻,Session只能处于一个状态中;状态转换前后,前一个状态称为现态,后一个状态称为次态。
事件 Event:状态机通常是通过事件驱动运行的,事件代表状态机可以处理的事件,通常发送一件事后,会把事件类型和事件内容一并传递给状态机。状态机接收到事件消息后,调用相应的动作去处理该事件,处理结果可能会是现态到次态的转换。
动作 Action:状态机是如何响应不同的事件的呢,其实每一个状态,都会遇到不同的事件,所以对不同的事件,也是有不同的处理的。比喻在待服务返回状态,发生了待用户说要转换意图的事件,那么事件就是《转换意图》,事件内容是《取消订票》,那么就需要执行 《初始化意图决策流程》这个动作,然后把状态转换为《思考决策状态》。通常事件和状态可以组合为一张表:
属性 | 初始状态 | 思考决策状态 | 动作执行状态 | 待用户回复状态 | 待服务返回状态 | 结束状态 |
新意图事件 | Action1 | Action2 | —— | —— | —— | Action3 |
服务返回事件 | —— | —— | Action2 | Action1 | —— | Action1 |
用户返回事件 | —— | Action1 | —— | —— | Action2 | —— |
服务异常事件 | —— | —— | Action3 | —— | —— | Action3 |
状态转换:介绍完动作和事件后,我们就清楚转换是怎么得到的了,也就是状态 s1 接收到事件 event1 后,执行了某些动作 action1,然后还把状态转换为了 s2,下图是机器人Session对象的状态转换图。
TCP 连接协议状态
下面给出一个TCP协议的有限状体机的示例,该例子估计大家都看得明白,Client客户端和Server服务端,都对应有自己的状态State和动作Action,分别对应着事件Event和转换
状态:closed、syn_sent、established、fin_wait1、fin_wait2、time_wait、listen、syn_received、close_wait、last_act
以上状态区别,一般还按照自身是发起端还是主动关闭端区分,只有close和established两种状态是两个端共有的。
事件:事件即该端口接收到网络的请求,sysackfin等,这里还有事件附带的数据,例如syn =a ,里面的a就是数据,我们一般称之为 payload
动作和状态转换关系具体可参考下图:
状态策略
状态模式是设计模式的内容,但和状态机不态一样。一个类如果有状态,那么其状态的表示是非常多的,而状态模式很多时候就用一个状态类代表其所处的状态。有时候你需要的系统的状态是很难穷举的。所以状态模式是一种比较低级的应用了。
无限的状态,有限的Action ——》状态策略:通常,我们的对象建模很难做成状态机,因为状态很可能是无限的,但不管如何,我们的系统执行动作是有限的,也就是State是无限的,而Action是有限的,毕竟Action需要我们程序员手写去实现,那么无限的状态,如果对应这一个个有限的Action呢,我们可以抽象一个根据当前状态选择Action的算法,我们把该算法抽象为<状态策略>。在介绍状态策略之前,最好大家先学习一个框架Redux
Redux:首先介绍下Redux,前端框架的一种,这个自称是状态机实现,每一个组件都是状态机,这个是我认为最合适作为状态机定义的实现,因为React接受外界输入action后,会根据state重新渲染组件,所以会带有观察者模式的感觉,所以我认为它更接近状态机的本质 —— 状态驱动
下面一段redux的小demo,代表整个redux的工作原理
function createStore(reducer, preloadedState) { let state = preloadedState let listeners = [] function subscribe(listener) { listeners.push(listener) } function getState() { return state } function dispatch(action) { state = reducer(state, action) for (listener of listeners) { listener() } } /**通过一个不匹配任何 reducer的 type,来全部的初始值*/ /**redux 中是使用一个随机的字符串来保证不匹配任何 reducer, 这里使用了 ES6 的 Symbol 类型*/ dispatch({ type: Symbol() }); return { subscribe, getState, dispatch } }
策略:Policy,是一种Action选择的算法实现,它的输入是当前状态,输出是动作 Action,策略可以用规则实现,也可以用机器学习等算法实现。我们现场定义一个策略的接口
public class Policy{ // 根据当前状态,选择处理这个状态的策略 Action chooseAction(State state); }
有了接口,我们就不用担心具体的实现,只需要执行Action即可。而Action的执行,当然是交给状态管理器了。随着状态管理器执行完Action,Action会改变状态,也会出发观察者,所有外界就得到通知,外界处理完后会回馈到系统中,系统收到反馈后,这里注意的是,Action会收到这种反馈,然后改变状态State,当State改变后,系统可以把新的State传递给Policy,然后继续这个良性循环,从而整个状态机,得到了驱动。
状态管理器:Redux就是一个状态管理器,但是比较无奈的是,它接受的输入是外界的Action,使得状态改变,然后通知所有状态的观察者,这种模式的实现,也只有前端比较合适应用了,下面我给大家再介绍一个例子,那就是对话系统的例子,DST(Dialog State Tracker)和DP(Dialog Policy),前者为对话状态跟踪,后者为对话策略,其中对话策略可以用机器学习算法实现;
状态机与线程安全
状态模式,一般情况下,都是一件事一件事发生和处理的,但是有时候我们需要处理的系统并非如此简单,而是大量的事件在不同的事件发生,这个时候,我们可以有以下几种建模方式:
1、单线程:这种最简单,一个状态机维护一个事件队列,所有事件按照时间的顺序入队,然后用单一的线程处理该队列的事件,这样可以保证每个事件的事务性,但缺点也是有的,处理速度会变得更慢,而且会导致无事件的时候,线程闲置,优点是简单,而且可以给事件作优先级队列,使得重要的事件可以优先处理。
2、多状态:如果事件数量较少,我们可以把事件的先来后到的所有组合情况的状态都抽象出来,每一个事件的组合发生顺序都有一个对应的状态,优点是事件可以多线程,但缺点是状态会过多,难维护,所以适合事件数量较少的情况,最好是两三个事件。
3、保持幂等:状态按照业务需求设计,让状态机可以多线程运作事件,但也并非没有同步机制,可以用状态的改变的原子性同步,会有个缺点就是可能会在状态切换的时候发生ABA问题,但ABA也可以理解的,毕竟事件都有权利按照自己的事务执行,所以在action的设计中,要特别的注意相关所有状态对象的幂等处理,保证同一个消息被状态机处理多次,效果是相同的,也就是包装action独立于事件即可。