cover_image

状态模式在星愿小程序的实践

刘鹏程 映客技术
2022年05月19日 09:40



太空星愿









星愿小程序开发来自架构支撑组,一个致力于协助公司快速迭代新业务的团队

图片


01
星愿简介



太空星愿,这是一个让用户可以向太空“许愿”的小程序。

图片

用户许愿后,愿望会被提交到服务器,经过审核等一系列流程,最终被发送给卫星。愿望内容会被展示在显示屏上与太空合影,用户可以付费获取照片。

图片

2022年3月5日,太空星愿成功发射卫星

图片图片

图片
02
契机        

某天产品过来找我,说下版本要给增加一个流程,对于一定时间后还没有为照片付费的用户,给他们颁发一个证书。

我一听,这好办,不就是加几个判断嘛。

先写个demo捋一下逻辑:

func demo() {   //...   wish := GetWish(ID)   //先来个switch判断类型,普通愿望才能颁发证书   switch wish.Type {   case CommonWish:      //再来个if判断状态,只有卫星已接收到的愿望才能颁发证书      if wish.State != SatelliteRecv {         //...      } else {         //...      }   case SuperWish:      //...   default:      //...   }   IssueCertify(wish)   //...}

写完一看,逻辑倒不复杂,就是这一大块if/esle/switch/case嵌套在代码里。。。

再一想,这小程序第一个版本都还没上线呢,就说要加流程了,搞不好过几天产品又要加流程,那不又得往代码里塞if/esle/switch/case。

加流程还好,就怕哪天产品说要改已有的流程,还得去一堆if/esle/switch/case里面找。

毕竟作为一个合格的程序员,怎么能看得懂自己几天前写的代码呢。

想想就头疼,得想办法改进一下,以绝后患。

图片


先画个流程图看看

图片

对流程图抽象一下:
当处于初始状态的愿望遇到一个事件时,就会触发状态的转移,从而达到另一个最终状态。

符合如下规则:

  • 类型、状态、事件是有限、可枚举的

  • 任一时刻愿望只处于确定的一个状态

  • 状态之间可以依据一定规则进行转移,转移规则和愿望的类型、状态有关


可以看出,这就是一个多状态、多事件、多类型的有限状态机。那么答案也很明显了,那就是状态模式。

图片
03
状态模式

状态模式是一种行为型模式,对象的行为基于其状态而改变。

  • 用法:当代码中包含大量与对象状态有关的条件语句时,将转换逻辑抽取成状态类及相应转换方法。

  • 优点:封装了转换逻辑,使其与状态对象合为一体,而不是作为一个个巨大的条件语句块分散在代码各处。

  • 缺点:状态模式的使用会导致类型和对象的大量增加,设计和实现都相对复杂,使用不当将导致代码混乱。

  • 典型应用场景:游戏引擎、工作审批流、电商订单流

图片
04
具体实现

我们先看看愿望结构体,和状态转移相关的是Type和State字段:

type Wish struct {   Type  int8 `json:"type"`  // 类型   State int8 `json:"state"` // 状态   // ...}


再来看看类型、状态、事件枚举:

type WishType = int8 //愿望类型
const ( TypeCommon WishType = iota + 1 // 普通愿望 TypeSuper // 霸屏愿望)
type WishState = int8 //愿望状态
const ( StateToBeCreate WishState = iota // 待创建 StateToBePaid // 待支付 StateToBeAudit // 待审核 StateUnPassAudit // 未通过审核 StatePassAudit // 通过审核 StateHasSpeed // 已加速 StateSatelliteRecv // 送达至卫星 StateImagePaid // 照片已付费 StateImageReturned // 照片已回传)
type WishEvent = int8 //愿望事件
const ( EventCreateWish WishEvent = iota + 1 // 许愿 EventPayForWish // 许愿付费 EventUnPassAudit // 审核未通过 EventPassAudit // 审核通过 EventModifyWish // 修改愿望 EventWishSpeed // 加速 EventSendToSatellite // 发送至卫星 EventPayForImage // 照片付费 EventReturnImage // 回传照片)


定义抽象状态类,封装状态转移逻辑:

type WishState interface {   Next(*Wish, define.WishEvent) bool}

抽象状态类包含一个Next方法,该方法需要接收一个Wish指针类型的对象,以及作用于这个对象的事件,然后执行状态转移操作,最后返回bool类型的转移结果(因为Go语言不像Java有Exception机制,所以需要显式返回)。


然后为各个状态实现抽象状态类,封装具体的转换逻辑:

type StatePassAudit struct{}    // 通过审核
func (s StatePassAudit) Next(wish *Wish, event define.WishEvent) bool { // ...}


因为愿望结构体保存的是int8类型的状态而不是状态类,还得提供一个状态类实例化函数,接收状态枚举并生成具体状态类:

var _wishStateMap = map[define.WishState]WishState{   define.StateToBeCreate:    StateToBeCreate{},   define.StateToBePaid:      StateToBePaid{},   define.StateToBeAudit:     StateToBeAudit{},   define.StateUnPassAudit:   StateUnPassAudit{},   define.StatePassAudit:     StatePassAudit{},   define.StateHasSpeed:      StateHasSpeed{},   define.StateSatelliteRecv: StateSatelliteRecv{},   define.StateImageReturned:   StateImageReturned{},}
var _defaultWishState = defaultWishState{}
func NewWishState(state define.WishState) WishState { if wishState, ok := _wishStateMap[state]; ok { return wishState } return _defaultWishState}
type defaultWishState struct{}
func (s defaultWishState) Next(_ *Wish, _ define.WishEvent) bool { return false}

之前说过状态模式的一个缺点是对象过多。这里考虑到状态类其实都是空结构体,不用担心共享对象问题,因此我将其设计成共享全局状态对象,避免了状态模式对象过多的问题。

此外,未从状态map获取到状态类时会返回defaultWishState,而不是返回空interface{},因为那可能导致panic。

图片

UML图如下:

图片


现在我们来增加颁发证书的逻辑,首先定义相关状态、事件枚举:

type WishState = int8 //愿望状态
const ( // ... StateCertifyReturned // 证书已回传)type WishEvent = int8 //愿望事件
const ( // ... EventReturnCertify // 回传证书)


StateCertifyReturned状态需要为处于StateSatelliteRecv状态的愿望触发EventReturnCertify事件才能到达,那么我们需要在StateSatelliteRecv状态类的Next方法增加相关逻辑:

type StateSatelliteRecv struct{}
func (s StateSatelliteRecv) Next(wish *Wish, event define.WishEvent) bool { switch event { // ... case define.EventReturnCertify: wish.State = define.StateCertifyReturned default: return false } return true}


此外,我们还要为StateCertifyReturned定义状态类并实现WishState接口:

type StateCertifyReturned struct{}
func (s StateCertifyReturned) Next(wish *Wish, event define.WishEvent) bool { return false}
var _wishStateMap = map[define.WishState]WishState{ // ... define.StateCertifyReturned: StateCertifyReturned{},}


StateCertifyReturned属于最终状态,不可再迁移到其它状态,所以直接返回false就行。


最后我们再写个demo试试:

func demo() {   //...   wish := GetWish(ID)   if ok := NewWishState(wish.State).Next(define.EventReturnCertify); !ok {      return   }   IssueCertify(wish)   //...}


是不是清爽多了?与状态转移相关的条件语句都已经被封装到了对应的状态类里,我们只需要传入愿望当前状态和要触发的事件,然后专注于剩下的业务逻辑即可。

图片
05
总结    

本文以星愿小程序为例,讲述了状态模式在实际业务场景中的应用。当对象的行为依赖于其状态,导致代码中充斥着大量if/else时,我们可以利用状态模式,抽取出冗长的条件分支语句,封装成一个个状态类,使我们从复杂的状态转移判断中脱离出来,专注于业务逻辑,大大提升代码的可读性和扩展性。


继续滑动看下一个
映客技术
向上滑动看下一个