星愿小程序开发来自架构支撑组,一个致力于协助公司快速迭代新业务的团队
太空星愿,这是一个让用户可以向太空“许愿”的小程序。
用户许愿后,愿望会被提交到服务器,经过审核等一系列流程,最终被发送给卫星。愿望内容会被展示在显示屏上与太空合影,用户可以付费获取照片。
2022年3月5日,太空星愿成功发射卫星
某天产品过来找我,说下版本要给增加一个流程,对于一定时间后还没有为照片付费的用户,给他们颁发一个证书。
我一听,这好办,不就是加几个判断嘛。
先写个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里面找。
毕竟作为一个合格的程序员,怎么能看得懂自己几天前写的代码呢。
想想就头疼,得想办法改进一下,以绝后患。
先画个流程图看看
对流程图抽象一下:
当处于初始状态的愿望遇到一个事件时,就会触发状态的转移,从而达到另一个最终状态。
符合如下规则:
类型、状态、事件是有限、可枚举的
任一时刻愿望只处于确定的一个状态
状态之间可以依据一定规则进行转移,转移规则和愿望的类型、状态有关
可以看出,这就是一个多状态、多事件、多类型的有限状态机。那么答案也很明显了,那就是状态模式。
状态模式是一种行为型模式,对象的行为基于其状态而改变。
用法:当代码中包含大量与对象状态有关的条件语句时,将转换逻辑抽取成状态类及相应转换方法。
优点:封装了转换逻辑,使其与状态对象合为一体,而不是作为一个个巨大的条件语句块分散在代码各处。
缺点:状态模式的使用会导致类型和对象的大量增加,设计和实现都相对复杂,使用不当将导致代码混乱。
典型应用场景:游戏引擎、工作审批流、电商订单流
我们先看看愿望结构体,和状态转移相关的是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{},
}
最后我们再写个demo试试:
func demo() {
//...
wish := GetWish(ID)
if ok := NewWishState(wish.State).Next(define.EventReturnCertify); !ok {
return
}
IssueCertify(wish)
//...
}
是不是清爽多了?与状态转移相关的条件语句都已经被封装到了对应的状态类里,我们只需要传入愿望当前状态和要触发的事件,然后专注于剩下的业务逻辑即可。
本文以星愿小程序为例,讲述了状态模式在实际业务场景中的应用。当对象的行为依赖于其状态,导致代码中充斥着大量if/else时,我们可以利用状态模式,抽取出冗长的条件分支语句,封装成一个个状态类,使我们从复杂的状态转移判断中脱离出来,专注于业务逻辑,大大提升代码的可读性和扩展性。