zoukankan      html  css  js  c++  java
  • 使用Golang实现状态机

    微信公众号:[double12gzh]
    关注容器技术、关注Kubernetes。问题或建议,请公众号留言。

    1. 背景

    在计算机领域中,状态机是一个比较基础的概念。在我们的日常生活中,我们可以看到许多状态机的例子,如:交通信息号灯、电梯、自动售货机等。

    基于FSM的编程也是一个强大的工具,可以对复杂的状态转换进行建模,它可以大大简化我们的程序。

    2. 什么是状态机

    有限状态机(FSM)或简称状态机,是一种计算的数学模型。它是一个抽象的机器,在任何时间都可以处于有限的状态之一。FSM可以根据一些输入从一个状
    态变为另一个状态;从一个状态到另一个状态的变化称为转换。

    一个FSM由三个关键要素组成:初始状态所有可能状态的列表触发状态转换的输入

    下面我们以旋转门作为FSM建模的一个简单例子(来自Wikipedia)

    和其他FSM一样,转门的状态机有三个元素:

    • 它的初始状态是 "锁定"
    • 它有两种可能的状态。"锁定 "和 "解锁"
    • 两个输入将触发状态变化。"推 "和 "硬币"

    3. 实现状态机

    接下来,我将建立一个模拟旋转门行为的命令行程序。当程序启动时,它会提示用户输入一些命令,然后它将根据输入的命令改变其状态。

    3.1 版本1 简单直接

    
    package main
    
    import (
    	"bufio"
    	"fmt"
    	"log"
    	"os"
    	"strings"
    )
    
    // 旋转门状态
    type State uint32
    
    const (
    	Locked State = iota
    	Unlocked
    )
    
    // 相关的命令
    const (
    	CmdCoin = "coin"
    	CmdPush = "push"
    )
    
    func main() {
    	state := Locked
    	reader := bufio.NewReader(os.Stdin)
    	prompt(state)
    
    	for {
    		cmd, err := reader.ReadString('
    ')
    		if err != nil {
    			log.Fatalln(err)
    		}
    		cmd = strings.TrimSpace(cmd)
    		
    		switch state {
    		case Locked:
    			if cmd == CmdCoin {
    				fmt.Println("解锁, 请通行")
    				state = Unlocked
    			} else if cmd == CmdPush {
    				fmt.Println("禁止通行,请先解锁")
    			} else {
    				fmt.Println("命令未知,请重新输入")
    			}
    		case Unlocked:
    			if cmd == CmdCoin {
    				fmt.Println("大兄弟,门开着呢,别浪费钱了")
    			} else if cmd == CmdPush {
    				fmt.Println("请通行,通行之后将会关闭")
    				state = Locked
    			} else {
    				fmt.Println("命令未知,请重新输入")
    			}
    		}
    	}
    }
    
    func prompt(s State) {
    	m := map[State]string{
    		Locked:   "Locked",
    		Unlocked: "Unlocked",
    	}
    	fmt.Printf("当前的状态是: [%s], 请输入命令: [coin|push]
    ", m[s])
    }
    

    说明:

    • 首先定义两个状态Locked/Unlocked和两个支持的命令CmdCoin/CmdPush
    • 在main函数中设定了旋转门的初始状态为Locked
    • 后面启动一个无限循环,等待用户输入命令,并根据不同的状态处理不同的命令

    问题与优化:

    • 我们必须处理每个状态的未知命令,这可以通过小的重构来改进。
    • 如果我们把状态转换的逻辑提取到一个函数中,程序的表达能力会更强。

    3.2 版本2 重构优化

    ...
    func main() {
    	...
    
    	for {
    		cmd, err := reader.ReadString('
    ')
    		if err != nil {
    			log.Fatalln(err)
    		}
    		state = step(state, strings.TrimSpace(cmd))
    	}
    }
    
    func step(state State, cmd string) State {
    	if cmd != CmdCoin && cmd != CmdPush {
    		fmt.Println("未知命令,请重新输入")
    		return state
    	}
    
    	switch state {
    	case Locked:
    		if cmd == CmdCoin {
    			fmt.Println("已解锁,请通行")
    			state = Unlocked
    		} else {
    			fmt.Println("禁止通行,请先解锁")
    		}
    	case Unlocked:
    		if cmd == CmdCoin {
    			fmt.Println("大兄弟,别浪费钱了,现在已经解锁了")
    		} else {
    			fmt.Println("请通行,通行之后将会关闭")
    			state = Locked
    		}
    	}
    	return state
    }
    ...
    

    实现上,一个状态机通常会使用状态转换表来表示,如下:

    3.3 版本3 状态转换表

    通过上面的分析下,针对上述实现再次优化,这次引入状态转换表的实现

    ...
    
    func main() {
    	...
      
    	for {
    		// 读取用户的输入
    		cmd, err := reader.ReadString('
    ')
    		if err != nil {
    			log.Fatalln(err)
    		}		
    
    		// 获取状态转换表中的值
    		tupple := CommandStateTupple{strings.TrimSpace(cmd), state}
    
    		if f := StateTransitionTable[tupple]; f == nil {
    			fmt.Println("未知命令,请重新输入")
    		} else {
    			f(&state)
    		}
    	}
    }
    
    // CommandStateTupple 用于存放状态转换表的结构体
    type CommandStateTupple struct {
    	Command string
    	State   State
    }
    
    // TransitionFunc 状态转移方程
    type TransitionFunc func(state *State)
    
    // StateTransitionTable 状态转换表
    var StateTransitionTable = map[CommandStateTupple]TransitionFunc{
    	{CmdCoin, Locked}: func(state *State) {
    		fmt.Println("已解锁,请通行")
    		*state = Unlocked
    	},
    	{CmdPush, Locked}: func(state *State) {
    		fmt.Println("禁止通行,请先行解锁")
    	},
    	{CmdCoin, Unlocked}: func(state *State) {
    		fmt.Println("大兄弟,已解锁了,别浪费钱了")
    	},
    	{CmdPush, Unlocked}: func(state *State) {
    		fmt.Println("请尽快通行,通行后将自动上锁")
    		*state = Locked
    	},
    }
    
    ...
    

    采用这种方法,所有可能的转换都列在表格中。它易于维护和理解。如果需要一个新的转换,只需增加一个表项。
    由于FSM是一个抽象的机器,我们可以更进一步,以面向对象的方式实现它。

    3.4 版本4 通过class来抽象

    这里我们将会引入一个新的类Turnstile,这个类有一个属性State和一个方法ExecuteCmd。当需要进行状态转换时,就调用ExecuteCmd,
    并且ExecuteCmd是唯一能触发状态发生转换的途径。

    类图如下

    完整的代码实现如下:

    package main
    
    import (
    	"bufio"
    	"fmt"
    	"log"
    	"os"
    	"strings"
    )
    
    type State uint32
    
    const (
    	Locked State = iota
    	Unlocked
    )
    
    const (
    	CmdCoin = "coin"
    	CmdPush = "push"
    )
    
    type Turnstile struct {
    	State State
    }
    
    // ExecuteCmd 执行命令
    func (p *Turnstile) ExecuteCmd(cmd string) {
    	tupple := CmdStateTupple{strings.TrimSpace(cmd), p.State}
    	if f := StateTransitionTable[tupple]; f == nil {
    		fmt.Println("unknown command, try again please")
    	} else {
    		f(&p.State)
    	}
    }
    
    func main() {
    	machine := &Turnstile{State: Locked}
    	prompt(machine.State)
    	reader := bufio.NewReader(os.Stdin)
    
    	for {
    		cmd, err := reader.ReadString('
    ')
    		if err != nil {
    			log.Fatalln(err)
    		}
    
    		machine.ExecuteCmd(cmd)
    	}
    }
    
    type CmdStateTupple struct {
    	Cmd   string
    	State State
    }
    
    type TransitionFunc func(state *State)
    
    var StateTransitionTable = map[CmdStateTupple]TransitionFunc{
    	{CmdCoin, Locked}: func(state *State) {
    		fmt.Println("已解锁,请通行")
    		*state = Unlocked
    	},
    	{CmdPush, Locked}: func(state *State) {
    		fmt.Println("禁止通行,请先解锁")
    	},
    	{CmdCoin, Unlocked}: func(state *State) {
    		fmt.Println("大兄弟,不要浪费钱了")
    	},
    	{CmdPush, Unlocked}: func(state *State) {
    		fmt.Println("请尽快通行,然后将会锁定")
    		*state = Locked
    	},
    }
    
    func prompt(s State) {
    	m := map[State]string{
    		Locked:   "Locked",
    		Unlocked: "Unlocked",
    	}
    	fmt.Printf("当前的状态是: [%s], 请输入命令:[coin|push]
    ", m[s])
    }
    

    运行一下上面的代码,可以看到如下的输出:

    F:hello>go run main.go
    当前的状态是: [Locked], 请输入命令:[coin|push]
    coin
    已解锁,请通行
    push
    请尽快通行,然后将会锁定
    fuck
    unknown command, try again please
    push
    禁止通行,请先解锁
    push
    禁止通行,请先解锁
    coin
    已解锁,请通行
    push
    请尽快通行,然后将会锁定
    push
    禁止通行,请先解锁
    

    4. 小结

    在这个故事中,我们介绍了FSM的概念,并建立了一个基于FSM的程序,同时,我们提供了四个版本的实现方式来实现FSM:

    • v1,以直接的形式实现FSM。
    • v2,做一些重构以减少代码重复。
    • v3、引入状态转换表
    • v4,用OOP重构
  • 相关阅读:
    Python模块time、datetime
    Python装饰器、内置函数之金兰契友
    Python生成器、推导式之前襟后裾
    Python函数二(函数名,闭包,迭代器)之杵臼之交
    Python函数(一)之杵臼之交
    katalon系列二:selenium IDE的替代者——Katalon Recorder
    katalon系列一:初识Katalon Studio自动化测试工具
    windows下使用RoboCopy命令进行文件夹增量备份
    Selenium+Python自动化之如何绕过登录验证码
    UI自动化录制工具----UI Recorder
  • 原文地址:https://www.cnblogs.com/double12gzh/p/13621445.html
Copyright © 2011-2022 走看看