Golang实现状态机

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('\n')
		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]\n", m[s])
}

说明:

  • 首先定义两个状态Locked/Unlocked和两个支持的命令CmdCoin/CmdPush
  • 在main函数中设定了旋转门的初始状态为Locked
  • 后面启动一个无限循环,等待用户输入命令,并根据不同的状态处理不同的命令
    问题与优化:
  • 我们必须处理每个状态的未知命令,这可以通过小的重构来改进。
  • 如果我们把状态转换的逻辑提取到一个函数中,程序的表达能力会更强。

3.2 版本2 重构优化

...
func main() {
	...

	for {
		cmd, err := reader.ReadString('\n')
		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('\n')
		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 通过对象来抽象

这里我们将会引入一个新的类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('\n')
		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]\n", 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重构
posted @ 2022-03-25 17:42  無花無酒鋤作田  阅读(1008)  评论(0编辑  收藏  举报