重修设计模式-行为型-状态模式

news2024/11/25 18:52:35

重修设计模式-行为型-状态模式

先了解一下状态机的概念,状态机是软件编程中对一种状态场景的抽象表达,构成状态机三要素是:状态(State)、事件(Event)、动作(Action),事件也称为转移条件,事件驱动状态的转移,并触发对应的动作,其中动作的触发不是必须的。

状态机是一种抽象概念,而状态模式是状态机的一种编码实现方式,其实还有分支判断法和图表法可以实现状态机。

以电商中订单系统为例,订单有待付款,待发货,待收货,已完成和已取消状态,而且每个状态还要有特定的事件才能驱动状态的转移和动作触发,比如待付款状态的订单,由付款取消事件驱动订单到待发货已取消状态,不响应发货事件;待发货状态订单只由发货取消事件驱动到下一状态,而且取消后还要触发退款的动作,用一个图来表示这个关系:
在这里插入图片描述
订单是一种非常典型的状态机场景,这种场景的状态、事件和动作都是可以预见的,编码时可以先表达出状态机的三要素:

//订单状态:
enum class OrderState(val value: Int, val desc: String) {
    WAIT_PAYMENT(0, "待付款"),
    WAIT_SHIPMENT(1, "待发货"),
    WAIT_RECEIPT(2, "待收货"),
    COMPLETED(3, "已完成"),
    CANCELLED(4, "已取消")
}

//触发动作:
object ActionGroup {
    fun moneyToPlatform() {
        println("行为:付款给平台...")
    }

    fun moneyToSeller() {
        println("行为:金额打给商家...")
    }

    fun moneyToBuyer() {
        println("行为:金额退还给买家...")
    }
}


//状态机:
class OrderStateMachine {
    private var currentState: OrderState = OrderState.WAIT_PAYMENT

    //事件:买家付款
    fun payment() {}

    //事件:商家发货
    fun shipment() {}

    //事件:买家收货
    fun receipt() {}

    //事件:买家/商家取消
    fun cancelled() {}
}

下面是测试代码,共测试了三个流程,其中流程一、二状态是正常的状态流转,流程三在取消状态后再调用发货事件,用于检查程序是否响应这一错误事件。

fun main() {
    println("流程一:")
    val stateMachine1 = OrderStateMachine()
    stateMachine1.payment()
    stateMachine1.shipment()
    stateMachine1.receipt()
    println("")

    println("流程二:")
    val stateMachine2 = OrderStateMachine()
    stateMachine2.cancelled()
    println("")

    println("流程三:")
    val stateMachine3 = OrderStateMachine()
    stateMachine3.payment()
    stateMachine3.cancelled()
    stateMachine3.shipment()
    println("")
}

准备工作都做好了,下面开始用三种方法进行状态机的实现。

1.状态机实现—分支判断法:

这种方式会将需求简单的直译成代码,集中处理事件逻辑,并在每个事件中考虑所有状态的实现,下面按照这种方式将代码补全:

//状态机:
class OrderStateMachine {
    private var currentState: OrderState = OrderState.WAIT_PAYMENT  //已待付款作为初始状态

    //事件:买家付款
    fun payment() {
        println("事件:买家付款")
        when (currentState) {
            OrderState.WAIT_PAYMENT -> {
                ActionGroup.moneyToPlatform()
                currentState = OrderState.WAIT_SHIPMENT
            }

            OrderState.WAIT_SHIPMENT, OrderState.WAIT_RECEIPT, OrderState.COMPLETED, OrderState.CANCELLED -> {
                println("待发货、待收货、已完成和已取消的订单不用付款...")
            }
        }
        println("订单状态: ${currentState.desc}")
    }

    //事件:商家发货
    fun shipment() {
        println("事件:商家发货")
        when (currentState) {
            OrderState.WAIT_SHIPMENT -> {
                currentState = OrderState.WAIT_RECEIPT
            }

            OrderState.WAIT_PAYMENT, OrderState.WAIT_RECEIPT, OrderState.COMPLETED, OrderState.CANCELLED -> {
                println("待付款、待收货、已完成和已取消的订单不用发货...")
            }
        }
        printState()
    }

    //事件:买家收货
    fun receipt() {
        println("事件:买家收货")
        when (currentState) {
            OrderState.WAIT_RECEIPT -> {
                ActionGroup.moneyToSeller()
                currentState = OrderState.COMPLETED
            }

            OrderState.WAIT_PAYMENT, OrderState.WAIT_SHIPMENT, OrderState.COMPLETED, OrderState.CANCELLED -> {
                println("待付款,待发货、已完成和已取消的订单不用收货...")
            }
        }
        printState()
    }

    //事件:买家/商家取消
    fun cancelled() {
        println("事件:买家/商家取消")
        when (currentState) {
            OrderState.WAIT_PAYMENT -> {
                currentState = OrderState.CANCELLED
            }

            OrderState.WAIT_SHIPMENT -> {
                ActionGroup.moneyToBuyer()
                currentState = OrderState.CANCELLED
            }

            OrderState.WAIT_RECEIPT, OrderState.COMPLETED, OrderState.CANCELLED -> {
                println("待收货、已完成和已取消的订单不能取消...")
            }
        }
        printState()
    }

    fun printState() {
        println("订单状态: ${currentState.desc}")
    }

}

运行一下看结果:

流程一:
事件:买家付款
动作:付款给平台...
订单状态: 待发货
事件:商家发货
订单状态: 待收货
事件:买家收货
动作:金额打给商家...
订单状态: 已完成

流程二:
事件:买家/商家取消
订单状态: 已取消

流程三:
事件:买家付款
动作:付款给平台...
订单状态: 待发货
事件:买家/商家取消
动作:金额退还给买家...
订单状态: 已取消
事件:商家发货
待付款、待收货、已完成和已取消的订单不用发货...
订单状态: 已取消

可以看到流程一二状态正常流转,流程三已取消订单并不会响应发货事件,代码执行结果是符合预期的。

再看上述代码,包含了大量的 if-else / switch-case 判断( when 是 Kotlin语言表达 switch-case 的语法糖),这些冗长的分支逻辑很容易改错代码引发Bug,可读性很差。如果再增加 待评价 状态和 评价 事件,那么这时代码的改动会涉及到所有事件方法,代码维护性也很差。

这种实现方法只适合简单的状态机,对于复杂的状态机还是用下面两种实现方式。

2.状态机实现—查表法:

我们把事件也抽象成枚举:

//订单事件:
enum class OrderEvent(val value: Int, val desc: String) {
    PAYMENT(0, "事件:买家付款"),
    SHIPMENT(1, "事件:商家发货"),
    RECEIPT(2, "事件:买家收货"),
    CANCEL(3, "事件:买家/商家取消")
}

再根据上面的状态流转图,定义出状态的流转表:

状态\事件PAYMENTSHIPMENTRECEIPTCANCEL
WAIT_PAYMENTWAIT_SHIPMENT
(动作:moneyToPlatform)
\\CANCELLED
WAIT_SHIPMENT\WAIT_RECEIPT\CANCELLED
动作:moneyToBuyer
WAIT_RECEIPT\\COMPLETED
动作:moneyToSeller
\
COMPLETED\\\\
CANCELLED\\\\

这个表也是查表法的核心,只要能在代码中正确的表达这个表,就可以非常简单的实现状态机,这里用了一个取巧的方式,将状态枚举和事件枚举的 value 和所在数组下标进行了对应,代码如下:

//状态机:
class OrderStateMachine2 {
  	//状态-事件流转表
    private val STATE_EVENT_TABLE = arrayOf(
        arrayOf<OrderState?>(OrderState.WAIT_SHIPMENT, null, null, OrderState.CANCELLED),
        arrayOf<OrderState?>(null, OrderState.WAIT_RECEIPT, null, OrderState.CANCELLED),
        arrayOf<OrderState?>(null, null, OrderState.COMPLETED, null),
        arrayOf<OrderState?>(null, null, null, null),
        arrayOf<OrderState?>(null, null, null, null)
    )
  	//状态-动作触发表
    private val STATE_ACTION_TABLE = arrayOf(
        arrayOf<Function0<Unit>?>(::moneyToPlatform, null, null, null),
        arrayOf<Function0<Unit>?>(null, null, null, ::moneyToBuyer),
        arrayOf<Function0<Unit>?>(null, null, ::moneyToSeller, null),
        arrayOf<Function0<Unit>?>(null, null, null, null),
        arrayOf<Function0<Unit>?>(null, null, null, null)
    )

    private var currentState: OrderState = OrderState.WAIT_PAYMENT  //已待付款作为初始状态

    //事件:买家付款
    fun payment() {
        println("事件:买家付款")
        executeEvent(OrderEvent.PAYMENT)
        println("订单状态: ${currentState.desc}")
    }

    //事件:商家发货
    fun shipment() {
        println("事件:商家发货")
        executeEvent(OrderEvent.SHIPMENT)
        printState()
    }

    //事件:买家收货
    fun receipt() {
        println("事件:买家收货")
        executeEvent(OrderEvent.RECEIPT)
        printState()
    }

    //事件:买家/商家取消
    fun cancel() {
        println("事件:买家/商家取消")
        executeEvent(OrderEvent.CANCEL)
        printState()
    }

    private fun executeEvent(event: OrderEvent) {
        //触发动作
        STATE_ACTION_TABLE.getOrNull(currentState.value)?.getOrNull(event.value)?.invoke()

        val nextState = STATE_EVENT_TABLE.getOrNull(currentState.value)?.getOrNull(event.value)
        if (nextState != null) {
            currentState = nextState
        } else {
            println("${currentState.desc}不响应${event.desc}")
        }
    }

    fun printState() {
        println("订单状态: ${currentState.desc}")
    }
}

执行结果:

流程一:
事件:买家付款
动作:付款给平台...
订单状态: 待发货
事件:商家发货
订单状态: 待收货
事件:买家收货
动作:金额打给商家...
订单状态: 已完成

流程二:
事件:买家/商家取消
订单状态: 已取消

流程三:
事件:买家付款
动作:付款给平台...
订单状态: 待发货
事件:买家/商家取消
动作:金额退还给买家...
订单状态: 已取消
事件:商家发货
已取消不响应事件:商家发货
订单状态: 已取消

这种方式的核心是维护好两个表,如果新增状态和事件,那么只需要关注表关系是否正确即可,甚至无需对其他代码进行改动,比较适合状态比较多的场景。缺点是对于动作触发不是很灵活,由于每个动作触发的参数传递可能不一样,状态和动作甚至有依赖关系,这种场景下查表法就非常不灵活了。

3.状态机实现—状态模式:

查表法对于复杂动作场景有一定局限性,分支判断法的代码可读性和可维护性比较差,接下来就是主角-状态模式出场了。

状态模式其实就是对分支判断法的进一步封装,通过将事件触发导致的状态转移和动作执行,拆分到不同的状态类中,从而避免大量分支判断逻辑,提高代码可读性和可扩展性,这就是状态模式。

首先定义出事件接口:

//状态流转事件接口,各状态需实现:
interface IOrder {
    fun getDesc(): String

    //Kotlin中接口支持默认实现(高版本的Java也支持了)
    fun payment(stateMachine: OrderStateMachine3): Unit {
        println("${stateMachine.getOrderState().getDesc()}不响应事件:买家付款")
    }
    fun shipment(stateMachine: OrderStateMachine3): Unit {
        println("${stateMachine.getOrderState().getDesc()}不响应事件:商家发货")
    }
    fun receipt(stateMachine: OrderStateMachine3): Unit {
        println("${stateMachine.getOrderState().getDesc()}不响应事件:买家收货")
    }
    fun cancel(stateMachine: OrderStateMachine3): Unit {
        println("${stateMachine.getOrderState().getDesc()}不响应事件:买家/商家取消")
    }
}

定义所有状态类,并实现总的事件接口,然后根据具体状态选择实现抽象的事件方法,并在方法中实现状态流转和动作的触发逻辑。比如待付款状态订单只关心付款事件和取消事件,那么只实现这两个方法即可:

//object是Kotlin的单例写法,JVM 加载类时就创建了单例对象
//状态:待付款
object OrderWaitPayment : IOrder {
    override fun getDesc(): String = "待付款"

    override fun payment(stateMachine: OrderStateMachine3) {
        stateMachine.setOrderState(OrderWaitShipment)
        println("动作:付款给平台...")
    }

    override fun cancel(stateMachine: OrderStateMachine3) {
        stateMachine.setOrderState(OrderCanceled)
    }
}

//状态:待发货
object OrderWaitShipment : IOrder {
    override fun getDesc(): String = "待发货"

    override fun shipment(stateMachine: OrderStateMachine3) {
        stateMachine.setOrderState(OrderWaitReceipt)
    }

    override fun cancel(stateMachine: OrderStateMachine3) {
        stateMachine.setOrderState(OrderCanceled)
        println("动作:金额退还给买家...")
    }

}

//状态:待收货
object OrderWaitReceipt : IOrder {
    override fun getDesc(): String = "待收货"

    override fun receipt(stateMachine: OrderStateMachine3) {
        stateMachine.setOrderState(OrderCompleted)
        println("动作:金额打给商家...")
    }
}

//状态:已完成
object OrderCompleted: IOrder {
    override fun getDesc(): String = "已完成"
}

//状态:已取消
object OrderCanceled: IOrder {
    override fun getDesc(): String = "已取消"
}

状态机代码:

//状态机:
class OrderStateMachine3 {
    private var currentState: IOrder = OrderWaitPayment  //待付款作为初始状态

    fun setOrderState(orderState: IOrder) {
        currentState = orderState
    }

    fun getOrderState(): IOrder = currentState

    //事件:买家付款
    fun payment() {
        println("事件:买家付款")
        currentState.payment(this)
        printState()
    }

    //事件:商家发货
    fun shipment() {
        println("事件:商家发货")
        currentState.shipment(this)
        printState()
    }

    //事件:买家收货
    fun receipt() {
        println("事件:买家收货")
        currentState.receipt(this)
        printState()
    }

    //事件:买家/商家取消
    fun cancel() {
        println("事件:买家/商家取消")
        currentState.cancel(this)
        printState()
    }

    private fun printState() {
        println("订单状态: ${currentState.getDesc()}")
    }
}

执行结果:

流程一:
事件:买家付款
动作:付款给平台...
订单状态: 待发货
事件:商家发货
订单状态: 待收货
事件:买家收货
动作:金额打给商家...
订单状态: 已完成

流程二:
事件:买家/商家取消
订单状态: 已取消

流程三:
事件:买家付款
动作:付款给平台...
订单状态: 待发货
事件:买家/商家取消
动作:金额退还给买家...
订单状态: 已取消
事件:商家发货
已取消不响应事件:商家发货
订单状态: 已取消

代码输出符合预期,如果增加新的状态和事件,那么只需要新增个状态类和方法即可,扩展非常方便,可读性也很高。

缺点是如果状态非常多,也需要定义出大量的状态类,如果状态类的实现又只涉及状态流转而少有事件执行,那么类的模板代码甚至超过具体逻辑代码,就得不偿失了,这种情况图表法更适用。

总结

状态机三要素:状态(State)、事件(Event)、动作(Action),事件驱动状态的流转,并触发动作的执行。

实现状态及三种方式:

  • 分支判断法:

    优点:实现简单,适合状态较少的简单场景。

    缺点:大量 if-else 或 switch-case 代码,可读性和可扩展性差,不适合复杂逻辑。

  • 查表法:

    优点:代码中只需维护好状态流转表即可,代码比较直观,适合状态较多,且增加频繁的场景。

    缺点:不适合动作执行复杂的场景,如订单系统

  • 状态模式:

    优点:分支判断法的进一步封装,加强了代码可读性和扩展性,适合状态数量适中,动作执行复杂的场景。

    缺点:大量状态会导致状态类繁多,体积变大。

如何选择状态机的实现方法还需要根据具体场景,考虑当前需求实现健壮性,保持一定前瞻性,编码初期避免过度封装,适时重构,保持良好编码习惯。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2035780.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

basic_pentesting_2靶机

靶机地址&#xff1a;Basic Pentesting: 2 ~ VulnHub 攻击机kali和靶机要设在同一网段&#xff0c;查看靶机MAC地址&#xff1a; 一、信息收集 扫描目标主机 arp-scan -l 使用nmap扫描靶机开放的端口&#xff1a; nmap -A -sS -sV -v -p- 192.168.7.127 浏览器访问80端口&a…

数据中心安全建设整体解决方案(DOC原件22页)

数据中心的安全体系建设并非安全产品的堆砌&#xff0c;它是一个根据用户具体业务环境、使用习惯、安全策略要求等多个方面构建的一套生态体系&#xff0c;涉及众多的安全技术&#xff0c;实施过程需要涉及大量的调研、咨询等工作&#xff0c;还会涉及到众多的安全厂家之间的协…

2024接口自动化测试高频面试题!

一、json和字典的区别&#xff1f; json就是一个文本、字符串&#xff1b;有固定的格式&#xff0c;格式长的像python字典和列表的组合&#xff1b;以key-value的键值对形式来保存数据&#xff0c;结构清晰&#xff0c;。可以说是目前互联网项目开发中最常用的一种数据交互格式…

如何判定一个加密软件是否可靠

一、加密算法的安全性 算法类型&#xff1a;选择采用公认的高安全性加密算法的软件&#xff0c;如AES&#xff08;高级加密标准&#xff09;、RSA等。这些算法经过广泛验证&#xff0c;具有强大的加密能力。 密钥长度&#xff1a;较长的密钥长度能够增加破解难度&#xff0c;…

【微信小程序】页面配置

1. 页面配置文件的作用 小程序中&#xff0c;每个页面都有自己的 .json 配置文件&#xff0c;用来对当前页面的窗口外观、页面效果等进行配置。 2. 页面配置和全局配置的关系 3. 页面配置中常用的配置项

利用 Splunk 对人工智能数据基础设施进行 Spelunk 分析

概述 在企业数据方面&#xff0c;MinIO Enterprise Object Store 和 Splunk 有着共生关系。Splunk在其数字流处理器中使用MinIO。MinIO 是一个 Splunk SmartStore 端点。MinIO Enterprise Object Store 是一个高性能、兼容 Amazon S3 的分布式对象存储系统。通过遵循超大规模计…

免费【2024】springboot 个人用户博客系统设计与实现

博主介绍&#xff1a;✌CSDN新星计划导师、Java领域优质创作者、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和学生毕业项目实战,高校老师/讲师/同行前辈交流✌ 技术范围&#xff1a;SpringBoot、Vue、SSM、HTML、Jsp、PHP、Nodejs、Python、爬虫、数据可视化…

STM32学习笔记11-PWR电源控制

目录 PWR简介 电源框图 上电复位和掉电复位 可编程电压监测器 低功耗模式 模式选择 睡眠模式 停止模式 待机模式 低功耗模式应用 睡眠模式 停止模式 待机模式 PWR简介 PWR&#xff08;Power Control&#xff09;电源控制PWR负责管理STM32内部的电源供电部分&#…

Unity新输入系统结构概览

本文仅作笔记学习和分享&#xff0c;不用做任何商业用途 本文包括但不限于unity官方手册&#xff0c;unity唐老狮等教程知识&#xff0c;如有不足还请斧正 在学习新输入系统之前&#xff0c;我们需要对其构成有个印象 1.输入动作&#xff08;Inputaction&#xff09; 是定义输…

队列---学生信息输入输出

作业&#xff1a;链栈&#xff0c;自己实现一遍&#xff0c;但是节点存储不是整数&#xff0c;存储学生信息&#xff08;年龄&#xff0c;分数&#xff0c;姓名&#xff09;三级引用。 1、建立学生信息结构体&#xff0c;将data改为学生信息结构体类型。 2、循环入栈和入队。…

RPC 和 HTTP 理解

网上充斥着各类类似于这样的文章&#xff1a;rpc 比 http 快了多少倍&#xff1f;既然有了 http&#xff0c;为什么还要用 rpc 调用等等。遇到这类文章&#xff0c;说明对 http 和 rpc 是由理解误区的。 这里再次重复强调一遍&#xff0c;通信协议不是 rpc 最重要的部分&#x…

KubeSphere 部署 Kafka 集群实战指南

本文档将详细阐述如何利用 Helm 这一强大的工具&#xff0c;快速而高效地在 K8s 集群上安装并配置一个 Kafka 集群。 实战服务器配置(架构 1:1 复刻小规模生产环境&#xff0c;配置略有不同) 主机名IPCPU内存系统盘数据盘用途ksp-registry192.168.9.904840200Harbor 镜像仓库…

命令行参数环境变量

目录 前言&#xff1a; 命令行参数&#xff1a; 现象&#xff1a; 这些参数的意义&#xff1a; 为什么要这么做&#xff1f; 这些事是谁做的呢&#xff1f; 环境变量 现象&#xff1a; 创建环境变量&#xff1a; 结合程序理解&#xff1a; 前言&#xff1a; 我们在前…

R语言里认识机器学习

下面内容摘录自&#xff1a; 1章2节&#xff1a;关于人工智能、机器学习、统计学连和机器学习、R 与 ChatGPT 的探究-CSDN博客文章浏览阅读1k次。在现代科技发展的浪潮中&#xff0c;人工智能&#xff08;AI&#xff09;、机器学习&#xff08;ML&#xff09;、统计学、R 编程…

网络通信(TCP/UDP协议 三次握手四次挥手 )

三、TCP协议与UDP协议 1、TCP/IP、TCP、 UDP是什么 TCP/IP协议是一个协议簇&#xff0c;里面包括很多协议的&#xff0c; UDP只是其中的一个&#xff0c; 之所以命名为TCP/IP协议&#xff0c; 因为TCP、 IP协议是两个很重要的协议&#xff0c;就用他两命名了&#xff0c;而TCP…

告别知云单一选择,这些文献翻译工具同样值得信赖!

在翻译领域&#xff0c;知云文献翻译以其专业度和便捷性赢得了众多用户的青睐&#xff0c;但市场上还有许多其他翻译工具同样值得关注。本文将为您推荐几款优秀的翻译工具。 Foxit在线翻译 链接&#xff1a; https://fanyi.pdf365.cn/ Foxit在线翻译以其高效的翻译速度和准…

创建第一个Qt项目

创建第一个QT项目 创建工程名称一般不要有特殊符号&#xff0c;不要有中文 项目工程保存路径可修改&#xff0c;路径不要带中文 Base class中的三个选项 QMainWindow:主窗口类&#xff0c;包括菜单栏、工具栏、状态栏。 QWidget:可以创建一个空白的窗口&#xff0c;是所有界…

嵌入式软件--数据结构与算法 DAY 13

在嵌入式中&#xff0c;对算法的要求不高&#xff0c;但顺序查找和冒泡排序是经典算法&#xff0c;必须掌握。 1.算法定义 算法是一个用于解决特定问题的有限指令序列&#xff08;计算机可以执行的操作&#xff09;。通俗的理解就是可以解决特定问题的方法。 2.时间复杂度 …

手动和torch.nn实现卷积神经网络、空洞卷积、残差网络

一、数据集 1. 分类问题 数据集——车辆分类数据 ⚫输入图片,输出对应的类别 ⚫共1358张车辆图片 ⚫分别属于汽车、客车和货车三类 ⚫汽车:779张 ⚫客车:218张 ⚫货车:360张 ⚫每个类别随机取20-30%当作测试集 ⚫各图片的大小不一,需要将图片拉伸到相同大小 汽车 …

three.js 安装方法、基础简介、创建基础场景

threejs简介 Three.js是一个基于JavaScript编写的开源3D图形库&#xff0c;‌利用WebGL技术在网页上渲染3D图形。‌ 它提供了许多高级功能&#xff0c;‌如几何体、‌纹理、‌光照、‌阴影等&#xff0c;‌使得开发者能够快速创建复杂且逼真的3D场景。‌ threejs提供了丰富的功…