Go设计模式(22)-状态模式

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

UML类图位置:https://www.processon.com/view/link/60d29bf3e401fd49502afd25

本文代码链接为:https://github.com/shidawuhen/asap/blob/master/controller/design/22status.go

1.定义

1.1状态模式

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

UML:

图片

1.2分析

状态机有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" {
      //状态迁移+动作执行
   }
}

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

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

2.应用场景

这么多年都没有用过状态模式,使用场景确实有点少。

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

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

3.代码实现

图片

马里奥状态有小马里奥(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)
}

/**
 * @Author: Jason Pang
 * @Description: 小马里奥
 */
type SmallMarioStatus struct {
  mario *Mario
}

/**
 * @Author: Jason Pang
 * @Description: 设置马里奥
 * @receiver s
 * @param mario
 */
func (s *SmallMarioStatus) SetMario(mario *Mario) {
  s.mario = mario
}

func (s *SmallMarioStatus) Name() {
  fmt.Println("小马里奥")
}

/**
 * @Author: Jason Pang
 * @Description: 获得蘑菇变为超级马里奥
 * @receiver s
 */
func (s *SmallMarioStatus) ObtainMushroom() {
  s.mario.status = &SuperMarioStatus{
    mario: s.mario,
  }
  s.mario.score += 100
}

/**
 * @Author: Jason Pang
 * @Description: 获得斗篷变为斗篷马里奥
 * @receiver s
 */
func (s *SmallMarioStatus) ObtainCape() {
  s.mario.status = &CapeMarioStatus{
    mario: s.mario,
  }
  s.mario.score += 200
}

/**
 * @Author: Jason Pang
 * @Description: 遇到怪兽减100
 * @receiver s
 */
func (s *SmallMarioStatus) MeetMonster() {
  s.mario.score -= 100
}

/**
 * @Author: Jason Pang
 * @Description: 超级马里奥
 */

type SuperMarioStatus struct {
  mario *Mario
}

/**
 * @Author: Jason Pang
 * @Description: 设置马里奥
 * @receiver s
 * @param mario
 */
func (s *SuperMarioStatus) SetMario(mario *Mario) {
  s.mario = mario
}

func (s *SuperMarioStatus) Name() {
  fmt.Println("超级马里奥")
}

/**
 * @Author: Jason Pang
 * @Description: 获得蘑菇无变化
 * @receiver s
 */
func (s *SuperMarioStatus) ObtainMushroom() {

}

/**
 * @Author: Jason Pang
 * @Description:获得斗篷变为斗篷马里奥
 * @receiver s
 */
func (s *SuperMarioStatus) ObtainCape() {
  s.mario.status = &CapeMarioStatus{
    mario: s.mario,
  }
  s.mario.score += 200
}

/**
 * @Author: Jason Pang
 * @Description: 遇到怪兽变为小马里奥
 * @receiver s
 */
func (s *SuperMarioStatus) MeetMonster() {
  s.mario.status = &SmallMarioStatus{
    mario: s.mario,
  }
  s.mario.score -= 200
}

/**
 * @Author: Jason Pang
 * @Description: 斗篷马里奥
 */
type CapeMarioStatus struct {
  mario *Mario
}

/**
 * @Author: Jason Pang
 * @Description: 设置马里奥
 * @receiver s
 * @param mario
 */
func (c *CapeMarioStatus) SetMario(mario *Mario) {
  c.mario = mario
}

func (c *CapeMarioStatus) Name() {
  fmt.Println("斗篷马里奥")
}

/**
 * @Author: Jason Pang
 * @Description:获得蘑菇无变化
 * @receiver c
 */
func (c *CapeMarioStatus) ObtainMushroom() {

}

/**
 * @Author: Jason Pang
 * @Description: 获得斗篷无变化
 * @receiver c
 */
func (c *CapeMarioStatus) ObtainCape() {

}

/**
 * @Author: Jason Pang
 * @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()
}


输出:

➜ myproject go run main.go

小马里奥

-------------------获得蘑菇

超级马里奥

-------------------获得斗篷

斗篷马里奥

-------------------遇到怪兽

小马里奥

总结

仔细看上面的代码

  • 对事件触发状态的转移及动作的执行的改动会很简单

  • 可快速增加新的事件

  • 增加新的状态也方便,只需添加新的状态类,少量修改已有代码

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

最后

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

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

图片

往期文章回顾:

  1. 设计模式

  2. 招聘

  3. 思考

  4. 存储

  5. 算法系列

  6. 读书笔记

  7. 小工具

  8. 架构

  9. 网络

  10. Go语言

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员麻辣烫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值