当前位置: 首页 > news >正文

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

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

先了解一下状态机的概念,状态机是软件编程中对一种状态场景的抽象表达,构成状态机三要素是:状态(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 代码,可读性和可扩展性差,不适合复杂逻辑。

  • 查表法:

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

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

  • 状态模式:

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

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

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

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • Java面试八股之什么是消息队列
  • 智慧景区系统:科技赋能旅游新体验
  • 理解 Go 语言的分组操作
  • JAVA中的ArrayDeque和LinkedList实现Deque,前者不能存NULL结点,后者可以存放NULL。
  • 【upload]-ini-[SUCTF 2019]CheckIn-笔记
  • MySQL 中主键索引的页分裂:深入探讨
  • STM32家族系列的区别
  • 使用go实现一个简单的聊天服务器
  • 机器学习/深度学习——梯度下降法(Gradient descent)详解. 步骤清晰 0基础可看
  • 月薪5W的项目经理是如何面试的?这份面试攻略请收好!
  • Jetson Orin NX 功耗模式选择:MAXN与25W模式的对比与优化建议
  • 聊一聊 Netty 数据搬运工 ByteBuf 体系的设计与实现
  • IP地址证如何实现HTTPS访问?(内网IP、公网IP)
  • Python 之Scikit-learn(五) -- Scikit-learn提供的数据集及其详细介绍
  • 用户案例|Maple 软件在德国亚琛工业大学化学教学中的创新应用
  • nginx 负载服务器优化
  • spark本地环境的搭建到运行第一个spark程序
  • UMLCHINA 首席专家潘加宇鼎力推荐
  • Vue 动态创建 component
  • vue从创建到完整的饿了么(11)组件的使用(svg图标及watch的简单使用)
  • 类orAPI - 收藏集 - 掘金
  • 浅析微信支付:申请退款、退款回调接口、查询退款
  • 详解NodeJs流之一
  • shell使用lftp连接ftp和sftp,并可以指定私钥
  • 阿里云移动端播放器高级功能介绍
  • 仓管云——企业云erp功能有哪些?
  • ​14:00面试,14:06就出来了,问的问题有点变态。。。
  • ​软考-高级-信息系统项目管理师教程 第四版【第23章-组织通用管理-思维导图】​
  • #NOIP 2014#Day.2 T3 解方程
  • #中国IT界的第一本漂流日记 传递IT正能量# 【分享得“IT漂友”勋章】
  • #周末课堂# 【Linux + JVM + Mysql高级性能优化班】(火热报名中~~~)
  • $().each和$.each的区别
  • $L^p$ 调和函数恒为零
  • (09)Hive——CTE 公共表达式
  • (2)MFC+openGL单文档框架glFrame
  • (Forward) Music Player: From UI Proposal to Code
  • (八)光盘的挂载与解挂、挂载CentOS镜像、rpm安装软件详细学习笔记
  • (分享)一个图片添加水印的小demo的页面,可自定义样式
  • (附源码)spring boot校园拼车微信小程序 毕业设计 091617
  • (附源码)计算机毕业设计ssm本地美食推荐平台
  • (附源码)计算机毕业设计ssm基于B_S的汽车售后服务管理系统
  • (黑马出品_高级篇_01)SpringCloud+RabbitMQ+Docker+Redis+搜索+分布式
  • (接上一篇)前端弄一个变量实现点击次数在前端页面实时更新
  • (实战)静默dbca安装创建数据库 --参数说明+举例
  • (四)React组件、useState、组件样式
  • .Family_物联网
  • .NET 4 并行(多核)“.NET研究”编程系列之二 从Task开始
  • .net core 使用js,.net core 使用javascript,在.net core项目中怎么使用javascript
  • .NET Micro Framework 4.2 beta 源码探析
  • .NET 的静态构造函数是否线程安全?答案是肯定的!
  • .NET 反射 Reflect
  • .NET/C# 的字符串暂存池
  • .NET面试题(二)
  • .Net下的签名与混淆
  • .NET中使用Redis (二)