• 设计模式-状态模式 golang实现


    一 什么是有限状态机 

    有限状态机,英⽂翻译是 Finite State Machine,缩写为 FSM,简称为状态机。

    状态机不是指一台实际机器,而是指一个数学模型。说白了,一般就是指一张状态转换图。

    已订单交易为例:

    1.1四大概念

    下面来给出状态机的四大概念。

    1. State ,状态。一个状态机至少要包含两个状态。例如上商家交易有 已下单、已支付、已发货等多种状态。
    2. Event,事件。事件也称为转移条件(Transition Condition)。例如 客户下单、 客户完成支付、商家发货 都是一个事件。
    3. Action ,动作。事件发生以后要执行动作。例如用户支付,扣减用户余额就是动作。编程的时候,一个 Action 一般就对应一个函数。不过动作不是必须的,也可能只转移状态,不执⾏任何动作。
    4. Transition ,变换。也就是从一个状态变化为另一个状态。例如 订单从“已支付”转换到“已发货”。

    二 状态机的实现方法

    将上面业务流程翻译成骨架代码:

    1. type State int64
    2. const StateWaitingPayment State = 1 //等待支付
    3. const StateWaitingShip State = 2 //支付成功待发货
    4. // 订单状态机
    5. type LeaseStateMachine struct {
    6. State State //订单状态
    7. }
    8. // 订单支付成功
    9. func (p *LeaseStateMachine) EventPaySuccess() {
    10. //todo
    11. }
    12. // 取消了订单
    13. func (p *LeaseStateMachine) EventCancelOrder() {
    14. //todo
    15. }
    16. // 商家发货
    17. func (p *LeaseStateMachine) EventShipped() {
    18. //todo
    19. }
    20. // 确认收货
    21. func (p *LeaseStateMachine) EventConfirmReceipt() {
    22. //todo
    23. }

    2.1 分支逻辑

    最简单直接的实现⽅式是,参照状态转移 图,将每⼀个状态转移,直译成代码。这样编写的代码会包含⼤量的 if-else 或 switch-case 分⽀判断逻辑。

    1. type State int64
    2. const StateWaitingPayment State = 1 //等待支付
    3. const StateWaitingShip State = 2 //支付成功待发货
    4. const StateWaitingShipped State = 3 //发货成功
    5. const StateWaitingOrderSuccess State = 4 //订单结束
    6. const StateWaitingOrderCancel State = 5 //订单取消
    7. // 租赁订单状态机
    8. type LeaseStateMachine struct {
    9. State State //订单状态
    10. }
    11. // 订单支付成功
    12. func (p *LeaseStateMachine) EventPaySuccess() {
    13. if p.State == StateWaitingPayment {
    14. p.State = StateWaitingShip
    15. }
    16. }
    17. // 取消了订单
    18. func (p *LeaseStateMachine) EventCancelOrder() {
    19. if p.State == StateWaitingShip ||
    20. p.State == StateWaitingPayment {
    21. p.State = StateWaitingOrderCancel
    22. }
    23. }
    24. // 商家发货
    25. func (p *LeaseStateMachine) EventShipped() {
    26. if p.State == StateWaitingShip {
    27. p.State = StateWaitingShipped
    28. }
    29. }
    30. // 确认收货
    31. func (p *LeaseStateMachine) EventConfirmReceipt() {
    32. if p.State == StateWaitingShipped {
    33. p.State = StateWaitingOrderSuccess
    34. }
    35. }

    2.2 查表法

    除了⽤状态转移图来表示之外,状态机还可以⽤⼆维表来表示;将上面的状态图转换成二维表如下

    当前状态/事件

    E支付成功

    E发货E取消订单E确认收货
    等待支付支付成功待发货///
    支付成功待发货/发货成功订单取消/
    已发货///订单结束
    订单结束////
    订单取消////

     

    使用查表表修改上述代码:
     

    1. type State int64
    2. const StateWaitingPayment State = 1 //等待支付
    3. const StateWaitingShip State = 2 //支付成功待发货
    4. const StateWaitingShipped State = 3 //发货成功
    5. const StateWaitingOrderSuccess State = 4 //订单结束
    6. const StateWaitingOrderCancel State = 5 //订单取消
    7. type Event int64
    8. const (
    9. EventPay Event = 1 //支付事件
    10. EventShip Event = 2 //发货 事件
    11. EventCancel Event = 3 //取消订单 事件
    12. EventConfirmReceipt Event = 4 //确认收货
    13. )
    14. // 状态二维表配置
    15. var StateTable map[State]map[Event]State = map[State]map[Event]State{
    16. StateWaitingPayment: {
    17. EventPay: StateWaitingShip, //待支付订单 ,支付事件 => 已支付
    18. },
    19. StateWaitingShip: {
    20. EventShip: StateWaitingShipped,
    21. EventCancel: StateWaitingOrderCancel,
    22. },
    23. //.......
    24. }
    25. // 租赁订单状态机
    26. type LeaseStateMachine struct {
    27. State State //订单状态
    28. }
    29. // 订单支付成功
    30. func (p *LeaseStateMachine) EventPaySuccess() {
    31. p.ExecEventConfirmReceipt(EventPay)
    32. }
    33. // 取消了订单
    34. func (p *LeaseStateMachine) EventCancelOrder() {
    35. p.ExecEventConfirmReceipt(EventCancel)
    36. }
    37. // 商家发货
    38. func (p *LeaseStateMachine) EventShipped() {
    39. p.ExecEventConfirmReceipt(EventShip)
    40. }
    41. // 确认收货
    42. func (p *LeaseStateMachine) EventConfirmReceipt() {
    43. p.ExecEventConfirmReceipt(EventConfirmReceipt)
    44. }
    45. // 执行事件
    46. func (p *LeaseStateMachine) ExecEventConfirmReceipt(event Event) {
    47. EventNewStateTable, ok := StateTable[p.State]
    48. if ok {
    49. newState, ok := EventNewStateTable[event]
    50. if ok {
    51. p.State = newState
    52. }
    53. }
    54. }

    在查表法的代码实现中,事件触发的动作只是简单状态变换,所以⽤⼀个 int 类型 的⼆维数组 actionTable 就能表示。但是,如果要执⾏ 动作并⾮这么简单,⽽是⼀系列复杂的逻辑操作(⽐如加减积分、写数据库,还有可能发 送消息通知等等),我们就没法⽤如此简单的⼆维数组来表示了。

    2.3状态模式

    状态模式通过将事件触发的状态转移和动作执⾏,拆分到不同的状态类中,来避免分⽀判断

    逻辑。
     

    1.定义interface 所有事件

    1. type ILeaseState interface {
    2. //定义事件
    3. EventPay() //支付事件
    4. EventShip() //发货事件
    5. EventCancel() //取消订单事件
    6. EventConfirmReceipt() //确认收货事件
    7. }

    2.状态类实现 事件对应的action

    将事件对饮的代码逻辑被分散到各个状态类中。

    1. //==================================================================
    2. // 待支付状态
    3. type StateWaitingPaymentImp struct{}
    4. // 订单支付成功
    5. func (p *StateWaitingPaymentImp) EventPay() {
    6. //todo 更新订单状态
    7. }
    8. // 发货
    9. func (p *StateWaitingPaymentImp) EventShip() {
    10. //不做处理
    11. }
    12. // 取消
    13. func (p *StateWaitingPaymentImp) EventCancel() {
    14. //todo 取消
    15. }
    16. // 确认收货事件
    17. func (p *StateWaitingPaymentImp) EventConfirmReceipt() {
    18. //不做处理
    19. }
    20. //==================================================================
    21. // 支付成功 状态
    22. type StateWaitingShipImp struct{}
    23. // 订单支付成功
    24. func (p *StateWaitingShipImp) EventPay() {
    25. //不做任何处理
    26. }
    27. // 发货
    28. func (p *StateWaitingShipImp) EventShip() {
    29. //更新订单未发货
    30. }
    31. // 取消
    32. func (p *StateWaitingShipImp) EventCancel() {
    33. //更新订单未发货
    34. }
    35. // 确认收货事件
    36. func (p *StateWaitingShipImp) EventConfirmReceipt() {
    37. //不做处理
    38. }
    39. //===============================================================
    40. //........其他状态对应的事件

    三 总结

    实现方法对比

    实现方法优点缺点
    分支逻辑
    • 简单、直接,易理解。
    • 对简单的状态机首选该方法实现。

    • 对于复杂的状态机来说,代码中充斥着⼤量的 ifelse 或者 switch-case 分⽀判断逻辑,可读性和可维护性差。

      易漏写或者错写某个状态转移。
      如果哪天修改了状态机 中的某个状态转移,我们要在冗⻓的分⽀逻辑中找到对应的代码进⾏修改,很容易改错,导致 bug。
     
    查表法
    • 查表法的代码实现更加清晰,可读性和可维护性更好。
    • 当修改 状态机时,只需要修改 transitionTable 和 actionTable 状态转移配置
       
    • 查表法的实现⽅式有⼀定局限性,
      执行的action只能是简单的状态转移操作。

      如果要执⾏的action是⼀系列复杂的逻辑操作(⽐如加减积分、写数据库,还有可能发送消息通知等等),我们就没法⽤如此简单的⼆维数组来表示了。
    状态模式
     
    • 对于状态并不多、状态转移也⽐较简单,但事件触发执⾏的action包含的业务逻辑可能⽐较复杂的状态机来说,⾸选状态模式
     
    • 状态模式会引⼊⾮常多的状态类,会导致代码⽐较难维护

    像电商下单这种状态并不多,状态转移也⽐较简单,但事件触发执⾏的动作包含的业务逻辑可能会⽐较复杂,更加推荐使⽤状态模式来实现。

    像游戏⽐较复杂的状态机,包含的状态⽐较多,优先推荐使⽤查表法,

  • 相关阅读:
    MySQL数据库四:MySQL数据库
    MES生产管理系统与供应链协同管理
    2021年11月数据库流行度排行:openGauss 跃居第三,人大金仓晋身前十
    Opencv项目实战:07 人脸识别和考勤系统
    IDEA中pom文件出现灰色且有删除的线解决方案
    d的整与nan
    在线翻译软件-什么实时在线翻译软件比较好用?
    aop-动态代理,cglib动态代理,面向切面编程,aop的实现方法
    SQL 注入漏洞详解
    【python环境搭建】一台电脑下安装不同python版本时如何安装模块
  • 原文地址:https://blog.csdn.net/qq_16399991/article/details/134288687