什么是状态机
有限状态机(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()
注意事项
- 不同状态下对于同一个event可能有不同的action,这些不同的action的实现是通过子类state覆写基类的相应方法来实现
- 若状态类是“无状态”的,则可做成单例。“无状态”的含义是,状态类没有自己的实例属性。实现手法举例
class BaseState:
def __init__(self): pass
def x(self, ctx):
ctx.score += 100
总结
什么是状态模式?
状态模式是一种行为设计模式,让你能在一个对象的内部状态变化时改变其行为,使其看上去就像改变了自身所属的类一样。状态模式可以被用来实现状态机。
什么场景下应该使用状态模式?
[!NOTE]
- 如果对象需要根据自身当前状态进行不同行为,同时状态的数量非常多且与状态相关的代码会频繁变更的话,可使用状态模式
- 当相似状态和基于条件的状态机转换中存在许多重复代码时,可使用状态模式。
- 如果某个类需要根据成员变量的当前值改变自身行为, 从而需要使用大量的条件语句时, 可使用该模式。
怎样实现状态模式?
使用组合 + 委托实现状态模式。具体实现如下图
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()
}
- 无状态。BaseState是无状态的,只通过引用持有上下文,不通过成员变量的方式持有上下文,从而可使状态类能做成单例。
- 组合。状态机持有成员变量BaseState。
- 委托转发。StateMachine将事件对应的处理逻辑,委托转发给其状态类。如StateMachine将方法e1转发给BaseState::e1,将e2转发给BaseState:e2
[!NOTE] 详细步骤如下
识别出Context类、State类
确定状态机中的State及Event,抽象出BaseState,将Event做成抽象方法,供子类重写
实现具体State类,为不同的Event编写不同的Action,若该Event下无对应的Action,可直接抛出异常
实现Context类
- (组合)将State类作为Context类的实例属性
- (委托)将 Event直接委托给State类
扩展
状态模式 + 工厂方法 + 单例