什么是状态机

有限状态机(Finite-state machine)是一个非常有用的模型,可以模拟世界上大部分事物。 FROM JavaScript与有限状态机 - 阮一峰的网络日志

简单来说,状态机是一张有向图

stateDiagram-v2
direction LR
[*] --> s1
s1 --> s3: e1
s1 --> s2: e2
s2 --> s1: e3
s2 --> s3: e4

状态机的基本要素

SEA模型

State

状态。在编程中,类比于一个类、对象

Event

事件,类比于函数签名

Action

动作,类比于函数体、方法体

怎样实现一个状态机?

问题

Q: 下图是一个游戏中角色状态转换的状态图,请编程实现以下的状态机

stateDiagram-v2
direction LR
[*] --> A
A --> B: "x, +100"
B --> C: "x, +100"
C --> A: "z, -50"
B --> D: "y, +200"
D --> C: "z, -50"
C --> D: "x, +100"

[!NOTE] 通常来说,实现一个状态模式有三种方式

  • if-else方法
  • 查表法
  • 状态模式

if-else方法

class Context:
    def __init__(self):
        self.score = 0
        
def my_machine(ctx: Context, state: str, action: str) -> (Context, str):
    """return: (new_ctx, new_state)"""
    if state == "a":
        if action == "x":
            ctx.score += 100
            state = "b"
        elif action in ("y", "z"):
            raise TransitionNotAllow()
    elif state == "b":
        if action == "x":
            ctx.score += 100
            state = "c"
        elif action == "y":
            ctx.score += 200
            state = "d"
        elif action == "z":
            raise TransitionNotAllow()
    elif state == "c":
        if action == "x":
            ctx.score += 100
            state = "d"
        elif action == "z":
            ctx.score -= 50
            state = "a"
        elif action == "y":
            raise TransitionNotAllow()
    elif state == "d":
        if action == "z":
            ctx.score -= 50
            state = "c"
        elif action in ("x", "y"):
            raise TransitionNotAllow()
    
    return ctx, state

查表法

状态机能够表达为一张二维表,其中行表示当前的state,列表示event,行与列的交汇点表示在状态state下触发事件event后,系统会转移到哪个状态及其对应的action是什么。

如下表中,行C与x的交汇点取值为D/+100,表明当前状态是C,若触发了事件x,则转移至状态D,对应的action是分数+100。

(State, Event) x y z
A B / +100 x x
B C / +100 D / +200 x
C D / +100 x A / -50
D x x C / -50

一般通过实现两个二维数组(状态转移表、事件动作表)实现下列的二维表。其中状态转移表可用于获取下一状态是什么,事件动作表可用于获取某个状态在某一事件下对应的action是什么

class State:
    A = 0
    B = 1
    C = 2
    D = 3

class Event:
    x = 0
    y = 1
    z = 2

transitions = {
    State.A: {Event.x: State.B},
    State.B: {Event.x: State.C, Event.y: State.D},
    State.C: {Event.x: State.D, Event.z: State.A},
    State.D: {Event.z: State.C},
}

#  可能的改进: 将Event、Action抽象成类
actions = {  
    State.A: {Event.x: 100},
    State.B: {Event.x: 100, Event.y: 200},
    State.C: {Event.x: 100, Event.z: -50},
    State.D: {Event.z: -50},
}


class Context:
    def __init__(self, state: int):
        self.score = 0
        self.state = state

    def trigger(self, event: int):
        self.score += actions[self.state][event]
        self.state = transitions[self.state][event]


if __name__ == '__main__':
    ctx = Context(State.A)
    ctx.trigger(Event.x)  # no err
    ctx.trigger(Event.y)  # no err
    ctx.trigger(Event.z)  # no err

    ctx = Context(State.A)
    ctx.trigger(Event.x)  # no err
    ctx.trigger(Event.z)  # KeyError: 2,即禁止转移

状态模式

# state pattern + singleton + factory method

class TransitionNotAllow(Exception): pass


class BaseState:
    def x(self, ctx):
        ctx.score += 100
    def y(self, ctx):
        ctx.score += 200
    def z(self, ctx):
        ctx.score -= 50


class AState(BaseState):
    def x(self, ctx):
        super(AState, self).x(ctx)
        ctx.state = b_state

    def y(self, ctx):
        raise TransitionNotAllow()

    def z(self, ctx):
        raise TransitionNotAllow()


class BState(BaseState):
    def x(self, ctx):
        super(BState, self).x(ctx)
        ctx.state = c_state

    def y(self, ctx):
        super(BState, self).y(ctx)
        ctx.state = d_state

    def z(self, ctx):
        raise TransitionNotAllow()


class CState(BaseState):
    def x(self, ctx):
        super(CState, self).x(ctx)
        ctx.state = d_state

    def y(self, ctx):
        raise TransitionNotAllow()

    def z(self, ctx):
        super(CState, self).z(ctx)
        ctx.state = a_state


class DState(BaseState):
    def x(self, ctx):
        raise TransitionNotAllow()

    def y(self, ctx):
        raise TransitionNotAllow()

    def z(self, ctx):
        super(DState, self).z(ctx)
        ctx.state = c_state

a_state = AState()  # singleton method
b_state = BState()
c_state = CState()
d_state = DState()

class StateMachine:
    def __init__(self, state: str):
        state_cls = {"a": a_state, "b": b_state, "c": c_state, "d": d_state}  # factory methed
        self.state = state_cls[state]
        self.score = 0

    x = lambda self: self.state.x(self)  # state pattern
    y = lambda self: self.state.y(self)
    z = lambda self: self.state.z(self)


if __name__ == '__main__':
    machine = StateMachine("a")
    machine.x()
    machine.y()
    machine.z()

注意事项

  1. 不同状态下对于同一个event可能有不同的action,这些不同的action的实现是通过子类state覆写基类的相应方法来实现
  2. 若状态类是“无状态”的,则可做成单例。“无状态”的含义是,状态类没有自己的实例属性。实现手法举例
class BaseState:
    def __init__(self): pass
        
    def x(self, ctx):
        ctx.score += 100

总结

什么是状态模式?

状态模式是一种行为设计模式,让你能在一个对象的内部状态变化时改变其行为,使其看上去就像改变了自身所属的类一样。状态模式可以被用来实现状态机。

什么场景下应该使用状态模式?

[!NOTE]

  1. 如果对象需要根据自身当前状态进行不同行为,同时状态的数量非常多且与状态相关的代码会频繁变更的话,可使用状态模式
  2. 当相似状态和基于条件的状态机转换中存在许多重复代码时,可使用状态模式。
  3. 如果某个类需要根据成员变量的当前值改变自身行为, 从而需要使用大量的条件语句时, 可使用该模式。

怎样实现状态模式?

使用组合 + 委托实现状态模式。具体实现如下图

classDiagram
direction LR
StateMachine o--> BaseState
BaseState <|--> s1
BaseState <|--> s2
BaseState <|--> s3
BaseState ..> StateMachine: 引用
StateMachine ..> BaseState: 委托转发
class StateMachine {
e1()

e2()

e3()

e4()

}
class BaseState {
e1()
e2()
e3()
e4()
}
  1. 无状态。BaseState是无状态的,只通过引用持有上下文,不通过成员变量的方式持有上下文,从而可使状态类能做成单例。
  2. 组合。状态机持有成员变量BaseState。
  3. 委托转发。StateMachine将事件对应的处理逻辑,委托转发给其状态类。如StateMachine将方法e1转发给BaseState::e1,将e2转发给BaseState:e2

[!NOTE] 详细步骤如下

  1. 识别出Context类、State类

  2. 确定状态机中的State及Event,抽象出BaseState,将Event做成抽象方法,供子类重写

  3. 实现具体State类,为不同的Event编写不同的Action,若该Event下无对应的Action,可直接抛出异常

  4. 实现Context类

    1. (组合)将State类作为Context类的实例属性
    2. (委托)将 Event直接委托给State类

扩展

状态模式 + 工厂方法 + 单例

参考资料

  1. 状态设计模式
  2. 状态模式-将状态和行为封装成对象 - 码农充电站 - 博客园
  3. 产品经理的流程总是变,所以我搬出了大杀器状态机模式
  4. 设计模式大冒险第五关:状态模式,if/else的“终结者”