• golang设计模式——状态模式


    状态模式

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SAQ0iQcl-1660620676952)(C:/Users/86158/AppData/Roaming/Typora/typora-user-images/image-20220815223714920.png)]

    状态模式使用的相对较少,主要是因为会引入大量的状态类,导致代码比较难维护。但是合适的场景使用状态模式,可以把复杂的判断逻辑简化。

    状态模式:当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。

    UML

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3YdfBGfR-1660620676953)(C:/Users/86158/AppData/Roaming/Typora/typora-user-images/image-20220816103641221.png)]

    分析

    状态机有3个组成部分:状态(State)、事件(Event)、动作(Action)。事件触发状态的转移及动作的执行。

    只看定义和UML,可能比较难理解使用状态模式有什么好处,举个例子就清晰了。

    假设有四种状态A、B、C、D,同时有四种触发事件E1、E2、E3、E4,如果不使用状态模式,写出来的样子是这样的:

    func E1() {
       if status == "A" {
          //状态迁移+动作执行
       } else if status == "B" {
          //状态迁移+动作执行
       } else if status == "C" {
          //状态迁移+动作执行
       } else if status == "D" {
          //状态迁移+动作执行
       }
    }
    
    func E2() {
       if status == "A" {
          //状态迁移+动作执行
       } else if status == "B" {
          //状态迁移+动作执行
       } else if status == "C" {
          //状态迁移+动作执行
       } else if status == "D" {
          //状态迁移+动作执行
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    单看伪代码可能觉得还好,但是细想一想,如果动作执行比较复杂,代码是不是就很丑了。后期如果状态或者事件变更,如何确保每一处都进行了更改?这时候状态模式便起作用了。

    我们创建四个类,如UML中的ConcreteStateA、ConcreteStateB、ConcreteStateC、ConcreteStateD,分别代表四种状态。每个状态类中有4个Handle函数,分别对应4个事件。通过这种方式,将糅杂在一起的逻辑进行了拆分,代码看起来优雅了很多。

    应用场景

    实际业务场景中做过跨境履约单的状态机,履约单需要经历接单、清关中、清关成功、发货等状态。这种场景相对简单,状态只能单方向流转、单独的接口触发指定状态流转到下一个状态,复杂的部分在于下一个状态可能有多个,有的可以跳过。这种情况下,使用数组维护状态机,比状态模式要好。

    如果状态多、动作执行逻辑复杂,那使用状态模式还是挺合理的,一般游戏中使用状态模式相对多一些。本次借用《设计模式之美》里超级马里奥的例子,使用超级马里奥介绍实在是太合适了,一是因为马里奥有多种状态、多种触发事件,特别适合使用状态模式;二是超级马里奥大家都玩过,业务情况大家都熟悉。为了帮助大家回忆,我找了马里奥全系列变身形态https://zhuanlan.zhihu.com/p/250931383。

    代码实现

    马里奥状态有小马里奥(Small Mario)、超级马里奥(Super Mario)、斗篷马里奥(Cape Mario),小马里奥吃了蘑菇变为超级马里奥,小马里奥和超级马里奥获得斗篷变成斗篷马里奥,超级马里奥和斗篷马里奥碰到怪物变成小马里奥。

    package main
    
    import "fmt"
    
    type Mario struct {
    	score  int64
    	status MarioStatus
    }
    
    type MarioStatus interface {
    	Name()
    	ObtainMushroom()
    	ObtainCape()
    	MeetMonster()
    	SetMario(mario *Mario)
    }
    
    /**
     * @Description: 小马里奥
     */
    type SmallMarioStatus struct {
    	mario *Mario
    }
    
    /**
     * @Description: 设置马里奥
     * @receiver s
     * @param mario
     */
    func (s *SmallMarioStatus) SetMario(mario *Mario) {
    	s.mario = mario
    }
    
    func (s *SmallMarioStatus) Name() {
    	fmt.Println("小马里奥")
    }
    
    /**
     * @Description: 获得蘑菇变为超级马里奥
     * @receiver s
     */
    func (s *SmallMarioStatus) ObtainMushroom() {
    	s.mario.status = &SuperMarioStatus{
    		mario: s.mario,
    	}
    	s.mario.score += 100
    }
    
    /**
     * @Description: 获得斗篷变为斗篷马里奥
     * @receiver s
     */
    func (s *SmallMarioStatus) ObtainCape() {
    	s.mario.status = &CapeMarioStatus{
    		mario: s.mario,
    	}
    	s.mario.score += 200
    }
    
    /**
     * @Description: 遇到怪兽减100
     * @receiver s
     */
    func (s *SmallMarioStatus) MeetMonster() {
    	s.mario.score -= 100
    }
    
    /**
     * @Description: 超级马里奥
     */
    
    type SuperMarioStatus struct {
    	mario *Mario
    }
    
    /**
     * @Description: 设置马里奥
     * @receiver s
     * @param mario
     */
    func (s *SuperMarioStatus) SetMario(mario *Mario) {
    	s.mario = mario
    }
    
    func (s *SuperMarioStatus) Name() {
    	fmt.Println("超级马里奥")
    }
    
    /**
     * @Description: 获得蘑菇无变化
     * @receiver s
     */
    func (s *SuperMarioStatus) ObtainMushroom() {
    
    }
    
    /**
     * @Description:获得斗篷变为斗篷马里奥
     * @receiver s
     */
    func (s *SuperMarioStatus) ObtainCape() {
    	s.mario.status = &CapeMarioStatus{
    		mario: s.mario,
    	}
    	s.mario.score += 200
    }
    
    /**
     * @Description: 遇到怪兽变为小马里奥
     * @receiver s
     */
    func (s *SuperMarioStatus) MeetMonster() {
    	s.mario.status = &SmallMarioStatus{
    		mario: s.mario,
    	}
    	s.mario.score -= 200
    }
    
    /**
     * @Description: 斗篷马里奥
     */
    type CapeMarioStatus struct {
    	mario *Mario
    }
    
    /**
     * @Description: 设置马里奥
     * @receiver s
     * @param mario
     */
    func (c *CapeMarioStatus) SetMario(mario *Mario) {
    	c.mario = mario
    }
    
    func (c *CapeMarioStatus) Name() {
    	fmt.Println("斗篷马里奥")
    }
    
    /**
     * @Description:获得蘑菇无变化
     * @receiver c
     */
    func (c *CapeMarioStatus) ObtainMushroom() {
    
    }
    
    /**
     * @Description: 获得斗篷无变化
     * @receiver c
     */
    func (c *CapeMarioStatus) ObtainCape() {
    
    }
    
    /**
     * @Description: 遇到怪兽变为小马里奥
     * @receiver c
     */
    func (c *CapeMarioStatus) MeetMonster() {
    	c.mario.status = &SmallMarioStatus{
    		mario: c.mario,
    	}
    	c.mario.score -= 200
    }
    func main() {
    	mario := Mario{
    		status: &SmallMarioStatus{},
    		score:  0,
    	}
    	mario.status.SetMario(&mario)
    
    	mario.status.Name()
    	fmt.Println("-------------------获得蘑菇\n")
    	mario.status.ObtainMushroom()
    
    	mario.status.Name()
    	fmt.Println("-------------------获得斗篷\n")
    	mario.status.ObtainCape()
    
    	mario.status.Name()
    	fmt.Println("-------------------遇到怪兽\n")
    	mario.status.MeetMonster()
    
    	mario.status.Name()
    }
    
    • 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
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185

    输出:

    ➜ myproject go run main.go

    小马里奥

    ——————-获得蘑菇

    超级马里奥

    ——————-获得斗篷

    斗篷马里奥

    ——————-遇到怪兽

    小马里奥

    总结

    仔细看上面的代码

    • 对事件触发状态的转移及动作的执行的改动会很简单
    • 可快速增加新的事件
    • 增加新的状态也方便,只需添加新的状态类,少量修改已有代码

    坏处就是类特别多,类里的函数也会特别多,即使这些函数根本无用。不过能获得更好的扩展性,还是值得的。

    实例

    通过下面的例子可以发现,引入状态模式来写状态机会有引入比较多的结构体,并且改动代码的时候如果要新增或者是删除某一个状态的话,修改也需要在其他状态的结构体方法中修改,所以这个不太适合状态经常变更或者是状态很多的情况

    代码

    // Package state 状态模式
    // 笔记请查看: https://lailin.xyz/state.html
    // 这是一个工作流的例子,在企业内部或者是学校我们经常会看到很多审批流程
    // 假设我们有一个报销的流程: 员工提交报销申请 -> 直属部门领导审批 -> 财务审批 -> 结束
    // 在这个审批流中,处在不同的环节就是不同的状态
    // 而流程的审批、驳回就是不同的事件
    package state
    
    import "fmt"
    
    // Machine 状态机
    type Machine struct {
    	state IState
    }
    
    // SetState 更新状态
    func (m *Machine) SetState(state IState) {
    	m.state = state
    }
    
    // GetStateName 获取当前状态
    func (m *Machine) GetStateName() string {
    	return m.state.GetName()
    }
    
    func (m *Machine) Approval() {
    	m.state.Approval(m)
    }
    
    func (m *Machine) Reject() {
    	m.state.Reject(m)
    }
    
    // IState 状态
    type IState interface {
    	// 审批通过
    	Approval(m *Machine)
    	// 驳回
    	Reject(m *Machine)
    	// 获取当前状态名称
    	GetName() string
    }
    
    // leaderApproveState 直属领导审批
    type leaderApproveState struct{}
    
    // Approval 获取状态名字
    func (leaderApproveState) Approval(m *Machine) {
    	fmt.Println("leader 审批成功")
    	m.SetState(GetFinanceApproveState())
    }
    
    // GetName 获取状态名字
    func (leaderApproveState) GetName() string {
    	return "LeaderApproveState"
    }
    
    // Reject 获取状态名字
    func (leaderApproveState) Reject(m *Machine) {}
    
    func GetLeaderApproveState() IState {
    	return &leaderApproveState{}
    }
    
    // financeApproveState 财务审批
    type financeApproveState struct{}
    
    // Approval 审批通过
    func (f financeApproveState) Approval(m *Machine) {
    	fmt.Println("财务审批成功")
    	fmt.Println("出发打款操作")
    }
    
    // 拒绝
    func (f financeApproveState) Reject(m *Machine) {
    	m.SetState(GetLeaderApproveState())
    }
    
    // GetName 获取名字
    func (f financeApproveState) GetName() string {
    	return "FinanceApproveState"
    }
    
    // GetFinanceApproveState GetFinanceApproveState
    func GetFinanceApproveState() IState {
    	return &financeApproveState{}
    }
    
    • 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
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87

    单元测试

    package state
    
    import (
    	"testing"
    
    	"github.com/stretchr/testify/assert"
    )
    
    func TestMachine_GetStateName(t *testing.T) {
    	m := &Machine{state: GetLeaderApproveState()}
    	assert.Equal(t, "LeaderApproveState", m.GetStateName())
    	m.Approval()
    	assert.Equal(t, "FinanceApproveState", m.GetStateName())
    	m.Reject()
    	assert.Equal(t, "LeaderApproveState", m.GetStateName())
    	m.Approval()
    	assert.Equal(t, "FinanceApproveState", m.GetStateName())
    	m.Approval()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
  • 相关阅读:
    Day46:项目-购物车案例
    基于JAVA基于MVC框架的在线书店设计计算机毕业设计源码+系统+mysql数据库+lw文档+部署
    11_printf函数移植串口通信
    Spring-IoC源码分析
    Datawhale 2024 年 AI 夏令营第二期——基于术语词典干预的机器翻译挑战赛
    数商云供应链集成系统开发方案:多行业集成平台管理自动化
    整合SM框架时出现的异常
    【Linux网络(一)初识计算机网络】
    GPIO八种工作模式
    MacOS 环境编译 JVM 源码
  • 原文地址:https://blog.csdn.net/qq_53267860/article/details/126362751