状态机是一种非常好的表达逻辑跳转的方式,但是本身容易导致代码的耦合,维护性不佳。状态机模式是状态机结合面向对象之后的产物,既保持了状态机,清晰的逻辑表达能力,又加入了面向对象的高内聚性和可扩展性,提升代码的可维护性,是我非常喜欢的一种设计模式。
最近碰到一个问题,比较适合用状态机来描述,果断的使用了状态机模式,但是编写代码的过程中出现了一个隐藏很深的BUG。
背景
最近在做网络编程,网卡会有开关,我需要在网卡打开的时候创建对象,关闭的时候销毁对象,但是创建可能失败,这个时候,我需要不断的重试,直到成功为止。我把这个问题分成了三个状态:stop、ready、runing,他们之间的转换关系如下:
1 | +-------+ up +-------+ |
stop
状态收到网络UP消息的时候会转换为ready
状态,准备创建对象,如果对象创建失败会一直在ready
状态,直到创建成功之后,转换为runing
状态。
编码实现
抽取基类
我抽象出公共的状态State
,里面有OnUp
和OnDown
两个函数:
1 | std::unique_ptr<state> state; |
编写子类
实现这个对象的三个子类:
1 | class StopState : public State { |
到目前为止,上面的代码都能正常的work,直到我们的最后一步,初始化状态。
初始化状态
我们需要根据网络初始状态来判断是初始化成为stop
状态还是ready
状态。
1 | void Init() { |
逻辑上来说,这段代码似乎没有什么问题,但测试用例死活无法通过,我最终通过打印追踪发现原来我的状态一直停留在ReadyState
。
问题分析
上面这段代码,逻辑上没有问题,真正的问题出现在最后一步,状态初始化的地方:
1 | state.reset(new ReadyState); |
按照我们的逻辑,我们设置初始化状态为ready
,然后程序开始构造对象,并最终在成功构造之后一直停留在runing
状态。但是函数调用是栈式调用而不是顺序调用,展开后的函数调用栈如下:
1 | ReadyState::ReadyState(); |
从这个调用栈中,我们可以发现我们确实切换到了running
状态,但是很快我们又回到了ready
,这里的根本问题在于,我们CreateWidget
这个函数的调用放在了ReadyState
的构造函数中,从而导致RunningState
仅仅存在了非常短的一段时间。
理想中,我们的函数调用顺序应该是这样的:
1 | ReadyState::ReadyState(); |
这样我们最终就留在了running
状态中,所以解决这个问题的方案是把CreateWidget
的调用从构造函数中挪出来。
1 | class ReadyState : public State { |
1 | void Init() { |
反思
尽量避免在状态的构造函数中切换到另外一个状态