Go从入门到精通——接口(interface)——示例:实现有限状态机(FSM)

示例:实现有限状态机(FSM)

  有限状态机(Finite-State Machine,FSM),表示有限个状态及在这些状态间的转移和动作等行为的数学模型。

  本例将实现状态接口、状态管理器及一系列的状态和使用状态的逻辑。

1、状态的概念

  状态机中的状态与状态间能够自由转换。但是现实当中的状态却不一定能够自由转换,例如:人可以从站立状态转移到卧倒状态,却不能从卧倒状态直接转移到跑步状态,需要先经过站立状态后再转移到跑步状态。

  每个状态可以设置它可以转移到的状态。一些状态机还允许在同一个状态间互相转换,这也需要根据实际情况进行配置。

2、自定义状态需要实现的接口

  有限状态机系统需要指定一个状态需具备的属性和功能,由于状态需要由用户自定义,为了统一管理状态,就需要使用接口定义状态。状态机从状态接口查询到用户的自定义状态应该具备的属性有:

  • 名称:对应 State 接口的 Name() 方法。
  • 状态是否允许在同状态间转移,对应 State 接口的 EnableSameTransit() 方法。
  • 能否从当前状态转移到指定的某一状态,对应 State 接口的 CanTransitTo() 方法。

  除此之外,状态在转移时会发生的事件可以由状态机制通过状态接口的方法通知用户自己的状态,对应的两个方法 OnBegin() 和 OnEnd(),分别代表状态转移前和状态转移后。

  详细的状态定义过程,代码如下:

代码1-1 状态接口(fsm/state.go) 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main
 
import "reflect"
 
//声明接口状态。此接口用于状态管理器内部保存和外部实现
type State interface {
 
    //获取状态名字
    Name() string
 
    //需要实现是否允许同状态间的互相转换
    EnableSameTransit() bool
 
    //响应状态开始时
    OnBegin()
 
    //响应状态结束时
    OnEnd()
 
    //需要实现同状态能否转移到指定的状态
    CanTransitTo(name string) bool
}
 
//从状态实例获取状态名称
func StateName(name State) string {
 
    if name == nil {
        return "none"
    }
 
    //通过给定的状态接口查找状态的名称。使用反射获取状态的名称。
    return reflect.TypeOf(name).Elem().Name()
}

 3、状态基本信息

  State 接口定义的方法,在用户自定义时都是重复的,为了避免重复地编写很多代码,使用 StateInfo 类协助用户实现一些默认的实现。

  StateInfo 包含有名称,在状态初始化时被赋值。StateInfo 同时实现了 OnBegin()、OnEnd() 方法。此外,StateInfo 的 EnableSameTransit() 方法还能判断是否允许状态在同类状态中转换,CanTransiTo() 方法能判断是否能转移到某个目标状态,代码如下:

代码1-2 状态信息(fms/info.go)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main
 
//状态的基础信息和默认实现。声明一个结构体,拥有 name 成员。
type StateInfo struct {
    //状态名
    name string
}
 
//状态名
func (s *StateInfo) Name() string { return s.name }
 
//setName() 方法的首字母小写,表示这个方法只能在同包内被调用。
//这里我们希望 setName() 不能被使用者在状态初始化后随意修改名称,而是通过后面提到的状态管理器自动赋值。
func (s *StateInfo) setName(name string) { s.name = name }
 
//允许同状态转移
func (s *StateInfo) EnableSameTransit() bool { return false }
 
//OnBegin()方法进行默认实现
func (s *StateInfo) OnBegin() {}
 
//OnEnd()方法进行默认实现
func (s *StateInfo) OnEnd() {}
 
//默认可以转移到任何状态
func (s *StateInfo) CanTransitTo(name string) bool { return true }

4、状态管理

  状态管理器管理和维护状态的声明周期。用户根据需要,将需要进行状态转移和控制的状态实现后添加(StateManager 的 Add() 方法)到状态管理器里,状态管理器使用名称对这些状态进行维护,同一个状态只允许一个实例存在。状态管理器可以通过回调函数(StateManager 的 OnChange 成员)提供状态转移的通知。状态管理器对状态的管理和维护代码如下:

代码 1-3 状态管理器( fsm/statemgr.go)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package main
 
import (
    "errors"
)
 
//状态管理器
type StateManager struct {
 
    //已经添加的状态。声明一个以状态为键,以 State 接口为值的 map。
    stateByName map[string]State
 
    //状态改变时,状态管理器的成员 OnChange() 函数回调会被调用。
    OnChange func(from, to State)
 
    //记录当前状态。当状态发生改变时,记录当前状态。
    currentState State
}
 
//添加一个状态到管理器中
func (sm *StateManager) Add(s State) {
 
    //获取状态的名称。添加状态时,无须提供 name,状态管理器内部会根据 State 的实例和反射查询处状态的名称。
    name := StateName(s)
 
    //将 s(State 接口) 通过类型断言转换为带有 setName()方法(name string) 的接口。
    //接着调用这个接口的 setName() 方法设置状态的名称。使用该方法可以快速调用一个接口实现的其他方法。
    s.(interface {
        setName(name string)
    }).setName(name)
 
    //根据 name,在已经添加的状态中检查是否有重名的 name。
    if sm.Get(name) != nil {
        panic("duplicate state: " + name)
    }
 
    //根据名字保存到 map 中
    sm.stateByName[name] = s
}
 
//根据名称获取指定状态
func (sm *StateManager) Get(name string) State {
    if v, ok := sm.stateByName[name]; ok {
        return v
    }
 
    return nil
}
 
//初始化状态管理器
func NewStateManager() *StateManager {
    return &StateManager{
        stateByName: make(map[string]State),
    }
}

5、在状态间转移

  状态管理器不仅管理状态的实例,还可以控制当前的状态及转移到新的状态。

  状态管理器从当前状态转移到给定名称的状态过程中,如果发现状态不存在、目标状态不能转换及同类状态不能转换时,将返回 error 错误对象,这些错误 Err 开头,在包(package)里提前定义好。本例一共涉及 3 种错误,分别是:

  • 状态没有找到的错误,对应 ErrStateNotFound。
  • 禁止在同状态间转移的错误,对应 ErrForbidSameStateTransit。
  • 不能转移到指定状态的错误,对应 ErrCannotTransitToState。

  状态转移时,还会调用状态管理器的 OnChange() 函数进行外部通知。

  状态管理器的状态转移代码参考如下:

代码 1-3 状态管理器( fsm/statemgr.go)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
//状态没有找到的错误
var ErrStateNotFound = errors.New("state not found")
 
//禁止在同状态间转移的错误
var ErrForbidSameStateTransit = errors.New("forbid same state transit")
 
//不能转移到指定状态的错误
var ErrCannotTransitToState = errors.New("cannot transit to state")
 
//获取当前状态
func (sm *StateManager) CurrentState() State { return sm.currentState }
 
//当前状态能否转移到目标状态
func (sm *StateManager) CanCurrTransitTo(name string) bool {
 
    if sm.currentState == nil {
        return true
    }
 
    //相同的状态不用转换
    if sm.currentState.Name() == name && !sm.currentState.EnableSameTransit() {
        return false
    }
 
    //使用当前状态,检查能否转移到指定名字的状态
    return sm.currentState.CanTransitTo(name)
}
 
//转移到指定状态
func (sm *StateManager) Transit(target string) error {
 
    //获取目标状态
    next := sm.Get(target)
 
    if next == nil {
        return ErrStateNotFound
    }
 
    //记录转移前的状态
    preState := sm.currentState
 
    //当前状态
    if sm.currentState != nil {
 
        //相同的状态不用转换
        if sm.currentState.Name() == target && !sm.currentState.EnableSameTransit() {
            return ErrForbidSameStateTransit
        }
 
        //不能转移到目标状态
        if !sm.currentState.CanTransitTo(target) {
            return ErrCannotTransitToState
        }
 
        //结束当前状态
        sm.currentState.OnEnd()
    }
 
    //将当前状态切换为要转移到的目标状态
    sm.currentState = next
 
    //调用新状态的开始
    sm.currentState.OnBegin()
 
    //通知回调
    if sm.OnChange != nil {
        sm.OnChange(preState, sm.currentState)
    }
 
    return nil
}

 6、自定义状态实现状态接口

  状态的定义和状态管理器的功能已经编写完成,接下来就开始解决具体问题。在解决问题前需要知道有哪些问题:

(1)有哪些状态需要用户自定义实现?

  在使用状态机时,首先需要定义一些状态,并按照 State 状态接口进行实现,以方便自定义的状态能够被状态管理器管理和转移。

  本代码定义 3个 状态:闲置(Idle)、移动(Move)、跳跃(Jump)。

(2)这些状态的关系是怎么样的?

  这 3个 状态间的关系可以通过下图来描述:

  3个 状态可以自由转移,但移动(Move)状态只能单向转移到跳跃(Jump)状态。Move 状态可以自我转换,也就是同类转换。

3个状态间的转移关系

 状态的转移关系还可以用表格来描述,如下表:

表1-1 使用表格表示状态转移关系
下方为当前状态,右方为目标状态 Idle闲置 Move移动 Jump跳跃
Idle闲置 同类不能转移 允许转移 允许转移
Move移动 允许转移 同类允许转移 允许转移
Jump跳跃 允许转移 不允许转移 同类不允许转移

(3)如何组织这些状态间的转移?

  定义 3种 状态的结构体并内嵌 StateInfo 结构以实现 State 接口中的默认接口。再根据每个转台各自不同的特点,返回状态的转移特点(EnableSameTransit() 及 CnTransitTo() 方法等)及重新实现 OnBegin() 和 OnEnd() 方法的事件回调。详细代码如下:

 代码 1-4  一系列状态实现(fsm/main.go)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main
 
import "fmt"
 
//闲置状态
type IdleState struct {
    StateInfo //使用 StateInfo 实现基础接口
}
 
//重新实现状态开始
func (i *IdleState) OnBegin() {
    fmt.Println("IdleState begin")
}
 
//重新实现状态结束
func (i *IdleState) OnEnd() {
    fmt.Println("IdleState end")
}
 
//移动状态
type MoveState struct {
    StateInfo
}
 
func (m *MoveState) OnBegin() {
    fmt.Println("MoveState begin")
}
 
//允许移动状态互相转换
func (m *MoveState) EnableSameTransit() bool {
    return true
}
 
//跳跃状态
type JumpState struct {
    StateInfo
}
 
func (j *JumpState) OnBegin() {
    fmt.Println("JumpState begin")
}
 
//跳跃状态不能转移到移动状态
func (j *JumpState) CanTransitTo(name string) bool {
    return name != "MoveState"
}

7、使用状态机

  3种 自定义状态定义完成后,需要将所有代码整合起来。将自定义状态添加到状态管理器(StateManager)中,同时在状态改变(StateManager 的 OnChange 成员)时,打印状态转移的详细日志。

  在状态转移时,获得转移时可能发生的错误,并且打印错误,详细代码实现请参考 main.go 文件:

 代码 1-4  一系列状态实现(fsm/main.go)
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func main() {
 
    //实例化一个状态管理器
    sm := NewStateManager()
 
    //响应状态转移的通知
    sm.OnChange = func(from, to State) {
 
        //打印状态转移的流向
        fmt.Printf("%s ---> %s\n\n", StateName(from), StateName(to))
    }
 
    //添加3个状态
    sm.Add(new(IdleState))
    sm.Add(new(MoveState))
    sm.Add(new(JumpState))
 
    //在不同状态间转移
    transitAndRepo(sm, "IdleState")
    transitAndRepo(sm, "MoveState")
    transitAndRepo(sm, "MoveState")
    transitAndRepo(sm, "JumpState")
    transitAndRepo(sm, "JumpState")
    transitAndRepo(sm, "IdleState")
}
 
//封装转移状态和输出日志
func transitAndRepo(sm *StateManager, target string) {
    if err := sm.Transit(target); err != nil {
        fmt.Printf("FAILED! %s ---> %s, %s\n\n", sm.CurrentState().Name(), target, err.Error())
    }
}

8、运行代码

posted @   左扬  阅读(368)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 上周热点回顾(2.17-2.23)
levels of contents
点击右上角即可分享
微信分享提示