用 Swift 实现一个简单的状态机

September 28, 2017

使用(有限)状态机对于复杂的状态转移有很好的理解和简化作用。一个状态机一般有以下特征:

  • 状态(state)总数是有限的;
  • 任何时刻只在一种状态之中;
  • 接收到某个事件(event)触发后,会从一种状态转移(transition)到另一种状态。

下面就以番茄工作法的流程作为例子实现一个状态机。

番茄工作法

上图是番茄工作法的一个状态转移图。可以看到一共有 4 个状态:闲时、工作、短休息和长休息。还有这些状态之间的 5 个转移。

首先来定义各个状态和触发各个状态转移的事件:

protocol StateType: Hashable {}
enum State: StateType {
    case idle
    case work
    case shortBreak
    case longBreak   
}

protocol EventType: Hashable {}
enum Event: EventType {
    case startWork
    case startShortBreak
    case startLongBreak
    case backToIdle
}

这样,我们的状态机可以先定义为:

class StateMachine<S: StateType, E: EventType> {
    private(set) var currentState: S
    
    init(_ initialState: S) {
        self.currentState = initialState
    }
}

let stateMachine = StateMachine<State, Event>(.idle)

在初始化的时候除了要指定状态和事件的具体类型外,还需要提供一个初始状态作为当前状态。

接着为了处理事件发生,添加一个记录状态转移的方法:

func listen(_ event: E, transit fromState: S, to toState: S, callback: @escaping () -> Void) {
}

我们监听事件 event 发生时,如果当前状态为 fromState,那么转移状态到 toState,并且执行回调方法。

为了方便回调时获取整个转移过程,我们再定义一个 Transition 结构体用于封装这些内容:

struct Transition<S: StateType, E: EventType> {
    let event: E
    let fromState: S
    let toState: S
    
    init(event: E, fromState: S, toState: S) {
        self.event = event
        self.fromState = fromState
        self.toState = toState
    }
}

StateMachine 里再定义一个 Operation 的对象,把对应的回调和转移绑定一起:

private struct Operation<S: StateType, E: EventType> {
    let transition: Transition<S, E>
    let triggerCallback: (Transition<S, E>) -> Void
}
   
private var routes = [S: [E: Operation<S, E>]]()

记录状态转移的方法再改为:

func listen(_ event: E, transit fromState: S, to toState: S, callback: @escaping (Transition<S, E>) -> Void) {
    var route = routes[fromState] ?? [:]
    let transition = Transition(event: event, fromState: fromState, toState: toState)
    let operation = Operation(transition: transition, triggerCallback: callback)
    route[event] = operation
    routes[fromState] = route
}

最后添加触发事件的函数:

func trigger(_ event: E) {
    guard let route = routes[currentState]?[event] else { return }
    
    route.triggerCallback(route.transition)
    currentState = route.transition.toState
}

这样我们简易的状态机就能开始工作了。示例用法如下:

let stateMachine = StateMachine<State, Event>(.idle)

stateMachine.listen(.startWork, transit: .idle, to: .work) { (transition) in
    ...
}

stateMachine.trigger(.startWork)

可以看到,有限状态机的写法,逻辑清晰,表达力强,有利于封装事件。一个对象的状态越多、发生的事件越多,就越适合采用有限状态机的写法。

总的行数不超过 60 行,完整的代码在这里

Reference

JavaScript与有限状态机