有限状态机FSM

简介: 关于状态机,以前写过[用Go实现一个状态机](https://mp.weixin.qq.com/s?__biz=MzUzNzAzMTc3MA==&mid=2247484850&idx=1&sn=5ba31ff066ddeeedab27f9ca9f1b9b58&scene=21#wechat_redirect),只是讲述了如何控制状态的流转,理论上不能算作完整的状态机。

关于状态机,以前写过用Go实现一个状态机,只是讲述了如何控制状态的流转,理论上不能算作完整的状态机。

一个完整的状态机其过程如下:发生一个event(事件)后,根据当前存在的状态(cur-state),决定执行的“动作”(action),并设置下一个状态号(transition)。

其中:

  1. 事件(Event)指的是在时间和空间上占有一定位置,并且对状态机来讲是有意义的那些事情。事件通常会引起状态的变迁,促使状态机从一种状态切换到另一种状态。
  2. 状态(State)指的是对象在其生命周期中的一种状况,处于某个特定状态中的对象必然会满足某些条件、执行某些动作或者是等待某些事件。
  3. 转换(Transition)指的是两个状态之间的一种关系,表明对象将在第一个状态中执行一定的动作,并将在某个事件发生同时某个特定条件满足时进入第二个状态。

这次我们看一下有限状态机及其实战。

FSM

定义

有限状态机(英语:finite-state machine,缩写:FSM)又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。

FSM可以把模型的多状态、多状态间的转换条件解耦。可以使维护变得容易,代码也更加具有可读性,也更加艺术。

源码

样例

github上https://github.com/looplab/fsm ,有1.8K Star,我们以这个开源项目为例,讲一下FSM的具体实现。此处用开门、关门为例,状态虽然很,但能很好的说明问题。

样例代码位置为:https://github.com/shidawuhen/asap/blob/master/controller/various/fsm.go

package main

import (
    "fmt"
    "github.com/looplab/fsm"
)

type Door struct {
    To  string
    FSM *fsm.FSM
}

func NewDoor(to string) *Door {
    d := &Door{
        To: to,
    }

    d.FSM = fsm.NewFSM(
        "closed",
        fsm.Events{
            {Name: "open", Src: []string{"closed"}, Dst: "open"},
            {Name: "close", Src: []string{"open"}, Dst: "closed"},
        },
        fsm.Callbacks{
            //指定状态
            "leave_closed": func(e *fsm.Event) { d.leaveClose(e) },
            "before_open":  func(e *fsm.Event) { d.beforeOpen(e) },
            "enter_open":   func(e *fsm.Event) { d.enterOpen(e) },
            "after_open":   func(e *fsm.Event) { d.afterOpen(e) },
            //通用状态
            "enter_state": func(e *fsm.Event) { d.enterState(e) },
        },
    )
    return d
}

func (d *Door) beforeOpen(e *fsm.Event) {
    fmt.Printf("beforeOpen, The door to %s is %s\n", d.To, e.Dst)
}

func (d *Door) enterOpen(e *fsm.Event) {
    fmt.Printf("enterOpen, The door to %s is %s\n", d.To, e.Dst)
}

func (d *Door) afterOpen(e *fsm.Event) {
    fmt.Printf("afterOpen, The door to %s is %s\n", d.To, e.Dst)
}

func (d *Door) leaveOpen(e *fsm.Event) {
    fmt.Printf("leaveOpen, The door to %s is %s\n", d.To, e.Dst)
}

func (d *Door) leaveClose(e *fsm.Event) {
    fmt.Printf("leaveClose, The door to %s is %s\n", d.To, e.Dst)
}

func (d *Door) enterState(e *fsm.Event) {
    fmt.Printf("The door to %s is %s\n", d.To, e.Dst)
}

func main() {
    door := NewDoor("heaven")
    fmt.Println("当前状态为:" + door.FSM.Current())

    err := door.FSM.Event("open")
    if err != nil {
        fmt.Println(err)
    }

    fmt.Println("当前状态为:" + door.FSM.Current())
    err = door.FSM.Event("close")
    if err != nil {
        fmt.Println(err)
    }
}

输出:

➜  myproject go run main.go
当前状态为:closed
beforeOpen, The door to heaven is open
leaveClose, The door to heaven is open
enterOpen, The door to heaven is open
The door to heaven is open
afterOpen, The door to heaven is open
当前状态为:open
The door to heaven is closed

说明

  1. NewDoor里的Events存放状态机的信息
  • Name:事件
  • Src:当前状态
  • Dst:目标状态

表示某事件发生时,如果当前为Src状态,可变换为Dst状态

  1. NewDoor里的Callbacks存放转换动作
  • 转换动作可分为通用转换和指定转换,通用转换状态格式为***\_state,指定转换状态格式为***\_状态名
  • 无论通用还是指定转换状态,都是四种,分别对应样例中的代码
  • 进入当前状态前做什么:before\_**
  • 离开上一个状态做什么:leave\_**
  • 进入当前状态做什么:enter\_**
  • 当前状态执行完做什么:after\_**
  • 执行顺序为:https://www.processon.com/view/link/6289e3bd1e08533ae716e7ad

图片

实例


FSM可以用在状态多、变化也多的地方,如履约单。一般订单履约涉及很多状态,而且这些状态经常会变更,使用FSM会方便很多。其中有几个实现要点:

  1. 订单上要记录当前状态
  2. 需要维护状态机,可以从两方面考虑
  • 画图:清晰的资料能帮我们快速了解当前整个状态机的情况
  • 状态机存储:可以将状态机写在代码中或存放到数据库中,格式如Events所示,至少需要有事件、当前状态、目标状态
  1. 转移实现
  • 转移有通用转移和指定转移,好的通用转移逻辑能增强复用性
  • 指定状态可以独立实现,因为转换代码已解耦,对系统影响很小
  1. 状态一致性
  • 先计算出目标状态,当正确完成所有操作后,更新订单状态为目标状态

总结

通过分析源码和实例,大家能够看到使用FSM不但能清晰维护状态机,而且对状态的更改、对转移功能的更改都实现了解耦,大大减少了维护成本。

同时通过合理的设计,赋予研发人员对状态转移操作极大的控制性,可以从离开、进入前、进入、完成后四个时机进行控制。

在友好程度上,我觉得是比Go设计模式(22)-状态模式更好一些的。

资料

  1. 有限状态机FSM详解(一)
  2. FSM学习笔记
  3. 状态机的两种写法
  4. Gofsm
  5. https://github.com/looplab/fsm

最后

大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)

我的个人博客为:https://shidawuhen.github.io/

往期文章回顾:

  1. 设计模式
  2. 招聘
  3. 思考
  4. 存储
  5. 算法系列
  6. 读书笔记
  7. 小工具
  8. 架构
  9. 网络
  10. Go语言
相关文章
|
开发工具 git 开发者
深入解析:取消 Git Pull 操作的完整指南
【2月更文挑战第29天】
2406 0
|
存储 NoSQL 大数据
mongodb数据库的优缺点
MongoDB 是一个流行的 NoSQL 数据库,主要用于大规模数据存储和处理。下面是 MongoDB 数据库的一些优点和缺点: ### 优点: 1. **灵活的模式设计**:MongoDB 是一个文档数据库,支持动态的模式设计,允许您存储不同结构和格式的数据。 2. **水平扩展**:MongoDB 支持水平扩展,可以轻松地在多个节点和服务器之间分布数据,以满足高并发和大规模数据处理的需求。 3. **丰富的查询功能**:MongoDB 提供了强大的查询语言和索引支持,允许您在大数据集上进行高效的数据检索和分析。 4. **高性能**:通过使用内存映射(mmap)和其他优化技术,M
1651 0
|
监控 NoSQL Redis
看完这篇就能弄懂Redis的集群的原理了
看完这篇就能弄懂Redis的集群的原理了
909 0
|
8月前
|
监控 Java 测试技术
微服务保护Sentinel
本课程深入讲解微服务中的雪崩问题及其解决方案,重点介绍阿里开源的流量治理组件Sentinel。内容涵盖Sentinel的部署与整合、限流模式(直接、关联、链路)、流控效果(快速失败、预热、排队等待)、熔断降级、线程隔离及规则持久化等核心知识点,结合Jmeter压测实战,帮助开发者构建高可用的分布式系统。
Go语言方法与接收者 -《Go语言实战指南》
本文介绍了 Go 语言中方法的相关概念和用法。方法是绑定到特定类型上的函数,包含值接收者和指针接收者两种形式。值接收者不会改变原始数据,而指针接收者可修改原始数据,且在处理大型结构体时性能更优。文章详细对比了方法与普通函数的区别,并说明了选择指针接收者的原因,如修改原始值、提升性能及保持一致性。此外,Go 支持为任意自定义类型定义方法,不仅限于结构体。最后通过表格总结了方法的核心概念和使用场景。
372 34
|
机器学习/深度学习 人工智能 分布式计算
Post-Training on PAI (1):一文览尽开源强化学习框架在PAI平台的应用
Post-Training(即模型后训练)作为大模型落地的重要一环,能显著优化模型性能,适配特定领域需求。相比于 Pre-Training(即模型预训练),Post-Training 阶段对计算资源和数据资源需求更小,更易迭代,因此备受推崇。近期,我们将体系化地分享基于阿里云人工智能平台 PAI 在强化学习、模型蒸馏、数据预处理、SFT等方向的技术实践,旨在清晰地展现 PAI 在 Post-Training 各个环节的产品能力和使用方法,欢迎大家随时交流探讨。
|
存储 开发工具 git
Git日常问题: 什么是LFS?及其错误解决办法
Git LFS(Git Large File Storage)是Git的一个扩展,用于管理大型文件,通过将大文件的实际内容存储在远程服务器上,而Git仓库中只保留一个轻量级的文本指针,从而加速仓库操作的速度并减小仓库大小。当遇到Git LFS相关错误时,通常需要安装Git LFS工具并按照官方文档进行配置。
1845 2
Git日常问题: 什么是LFS?及其错误解决办法
|
Go 调度
使用ants并发任务,事半功倍
使用ants并发任务,事半功倍
659 0
|
存储 安全 程序员
|
运维 监控 机器人
线上观看 3 万+!「智能运维MeetUp」精彩回顾,探讨智能体构建新方向
围绕大模型、可观测性、智能机器人、SysOM 等热门话题,分享系统运维硬核技术、优化实践等干货。