【设计模式】订单状态流传中的状态机与状态模式
文章目录
- 1. 前言
 - 2.状态模式
 - 2.1.订单状态流转案例
 - 2.1.1.状态枚举定义
 - 2.1.2.状态接口与实现
 - 2.1.3.状态机
 - 2.1.4.测试
 
- 2.2.退款状态的拓展
 - 2.2.1.代码拓展
 - 2.2.2.测试
 
- 2.3.小结
 
- 3.总结
 
1. 前言
状态模式一般是用在对象内部的状态流转场景中,用来实现状态机。
什么是状态机呢?
 状态机是对状态转移的抽象,由事件、状态、动作组成,事件有时候也被称为转移事件或者转移,当事件触发时,可以将状态由一个状态变更为另一个状态,并执行动作。其中,事件和状态是必须存在的,动作可以不要。
下面是一张状态图,表达的就是一个状态机的模型。
 
 通俗来讲,就是对状态的变更做了一定的限制,不能随意的修改状态,而是只有处于某个特定的状态时,才能变更到另一个特定的状态。
2.状态模式
状态模式将状态抽象成一个个的状态对象,状态机当前持有某个状态对象,就表示当前的状态机处于什么状态。
 然后将事件处理为一个个的方法,每个方法中会操作状态机修改状态,有需要的情况下,在修改状态的同时还可以执行某些动作。
把通用部分提取出来后,可以得到这样一个通用类图:
 
可以看到上面的StateMachine和State关系是双向的,这是因为状态机需要持有状态对象来表示当前状态,以及通过当前的状态对象中的方法进行状态的流转,而流转的结果需要重新set到状态机中,又要求State必须持有状态机对象。
 当然,这里State对StateMachine的关系也可以通过依赖来表示。
2.1.订单状态流转案例
假设现在有一个商品订单的状态流转需求,状态图如下:
 
 这里没有加退款的状态,后续的拓展例子上会加上,用这种方式来体验状态模式的拓展性。
我们拿着这个图的时候,可以简单的在脑海里面过一遍如果通过if/else或者switch来做,应该要怎么写,后续如果想把退款的状态加入进去又该怎么拓展,这种方式应该大家都会,就不在这里赘述了。
接下来,就一步步的通过状态模式来实现这么一个状态机。
2.1.1.状态枚举定义
定义状态枚举主要是为了统一状态常量,因为订单是需要落库的,我们在持久化到数据库时,不能把状态对象保存进去,所以会涉及到状态常量与状态对象的互相转换。定义的枚举如下:
import lombok.Getter;@Getter
public enum OrderStateEnum {WAIT_PAYMENT(1, "待支付"),WAIT_DELIVER(2, "待发货"),WAIT_RECEIVE(3, "待收货"),RECEIVED(4, "已收货"),CANCEL(5, "已取消");private final int state;private final String desc;OrderStateEnum(int state, String desc) {this.state = state;this.desc = desc;}public int getState() {return state;}public String getDesc() {return desc;}
}
 
2.1.2.状态接口与实现
先上代码:
public interface OrderState {OrderStateEnum orderStateType();default void pay(OrderStateMachine stateMachine) {System.out.println("|--当前订单状态不支持支付,已忽略");}default void cancel(OrderStateMachine stateMachine) {System.out.println("|--当前订单状态不支持取消,已忽略");}default void deliver(OrderStateMachine stateMachine) {System.out.println("|--当前订单状态不支持发货,已忽略");}default void receive(OrderStateMachine stateMachine) {System.out.println("|--当前订单状态不支持收货,已忽略");}
}
 
接口中定义的pay,cancel等方法就是事件,供子类进行实现,相信大家也发现了,这些事件没有定义成抽象方法,而是通过default定义成了一个实例方法。不太清楚为什么的同学,可以先思考一下为什么要这么定义。
其实这么定义的好处是各个状态子类只需要实现自己需要的方法,而不用把所有的方法都实现一遍,这种做法在Spring中也比较常见,在JDK8之前通常是用xxxWrapper来实现的,JDK8之后就重构为直接使用default方法来实现了。
举个例子:后续如果需要加入退款状态,接口中也会新增一个提交退款的事件,在各个子类中,选择需要实现提交退款事件的状态子类进行重写即可,而不需要所有的子类都重写。
有多少个状态,就有多少个实现类,并按照上面的状态图,在对应的状态中实现自己需要的事件。
- 待支付状态:有支付和取消两种事件
public class WaitPaymentState implements OrderState {@Overridepublic OrderStateEnum orderStateType() {return OrderStateEnum.WAIT_PAYMENT;}@Overridepublic void pay(OrderStateMachine stateMachine) {stateMachine.setCurrentState(new WaitDeliverState());}@Overridepublic void cancel(OrderStateMachine stateMachine) {stateMachine.setCurrentState(new CancelState());} } - 待发货状态:有发货事件
public class WaitDeliverState implements OrderState {@Overridepublic OrderStateEnum orderStateType() {return OrderStateEnum.WAIT_DELIVER;}@Overridepublic void deliver(OrderStateMachine stateMachine) {stateMachine.setCurrentState(new WaitReceiveState());} } - 待收货状态:有收货事件
public class WaitReceiveState implements OrderState {@Overridepublic OrderStateEnum orderStateType() {return OrderStateEnum.WAIT_RECEIVE;}@Overridepublic void receive(OrderStateMachine stateMachine) {stateMachine.setCurrentState(new ReceivedState());} } - 已收货状态:状态结束点,没有其他事件
public class ReceivedState implements OrderState {@Overridepublic OrderStateEnum orderStateType() {return OrderStateEnum.RECEIVED;}} - 取消状态:状态结束点,没有其他事件
public class CancelState implements OrderState {@Overridepublic OrderStateEnum orderStateType() {return OrderStateEnum.CANCEL;} } 
2.1.3.状态机
状态机中需要持有当前状态对象,同时需要把状态接口中的事件同步定义到状态机中,以便外部业务对象调用。
 除此之外,状态枚举常量与状态对象之间的映射关系也可以直接配置在当前状态机中,功能更加内聚。
public class OrderStateMachine {public static final Map<OrderStateEnum, OrderState> ORDER_STATE_MAP = new HashMap<>();static {ORDER_STATE_MAP.put(OrderStateEnum.WAIT_PAYMENT, new WaitPaymentState());ORDER_STATE_MAP.put(OrderStateEnum.WAIT_DELIVER, new WaitDeliverState());ORDER_STATE_MAP.put(OrderStateEnum.WAIT_RECEIVE, new WaitReceiveState());ORDER_STATE_MAP.put(OrderStateEnum.RECEIVED, new ReceivedState());ORDER_STATE_MAP.put(OrderStateEnum.CANCEL, new CancelState());}private OrderState currentState;public OrderStateMachine(OrderStateEnum orderStateEnum) {this.currentState = ORDER_STATE_MAP.get(orderStateEnum);}public OrderState getCurrentState() {return currentState;}public void setCurrentState(OrderState currentState) {this.currentState = currentState;}void pay() {currentState.pay(this);}void deliver() {currentState.deliver(this);}void receive() {currentState.receive(this);}void cancel() {currentState.cancel(this);}}
 
2.1.4.测试
做一下状态机的测试,由于打印的日志重复度很高,这里取了个巧,将函数作为参数封装了一下:
public class OrderService {public static void main(String[] args) {OrderStateMachine stateMachine = new OrderStateMachine(OrderStateEnum.WAIT_DELIVER);invoke(stateMachine::pay, "用户支付", stateMachine);invoke(stateMachine::deliver, "商家发货", stateMachine);invoke(stateMachine::receive, "用户收货", stateMachine);invoke(stateMachine::cancel, "取消支付", stateMachine);}public static void invoke(Runnable runnable, String desc, OrderStateMachine stateMachine) {System.out.println(desc + "前订单状态: " + stateMachine.getCurrentState().orderStateType().getDesc());runnable.run();System.out.println(desc + "后订单状态: " + stateMachine.getCurrentState().orderStateType().getDesc());System.out.println("------------------");}
}
 
以待发货作为状态常量创建了一个状态机,状态机当前的状态就是待发货,下面的四个调用中,第1,4个是不会改变状态的,第2,3个会改变状态,下面以执行结果来验证猜测:
用户支付前订单状态: 待发货
|--当前订单状态不支持支付,已忽略
用户支付后订单状态: 待发货
------------------
商家发货前订单状态: 待发货
商家发货后订单状态: 待收货
------------------
用户收货前订单状态: 待收货
用户收货后订单状态: 已收货
------------------
取消支付前订单状态: 已收货
|--当前订单状态不支持取消,已忽略
取消支付后订单状态: 已收货
------------------
 
2.2.退款状态的拓展
通过状态模式来实现状态机,看重的就是它带来的拓展性和易维护性,所以在原有的基础上,加上退款的事件和状态,一起看看需要做些什么事。
2.2.1.代码拓展
下面是加入了退款的状态图:
 
 通过状态图可以看到,需要加入:
- 两个状态:退款中和已退款
 - 两个事件:申请退款和确认退款
 - 原有状态拓展:待发货、待收货、已收货 3个状态中都需要引入申请退款事件
 
综上,一步一步的拓展代码:
- 第一步:拓展枚举常量
public enum OrderStateEnum {WAIT_PAYMENT(1, "待支付"),WAIT_DELIVER(2, "待发货"),WAIT_RECEIVE(3, "待收货"),RECEIVED(4, "已收货"),CANCEL(5, "已取消"),REFUNDING(6, "退款中"),REFUNDED(7, "已退款"),;// 省略后续代码…… } - 第二步:拓展状态接口
public interface OrderState {// 省略已有代码……default void refund(OrderStateMachine stateMachine) {System.out.println("|--当前订单状态不支持退款,已忽略");}default void confirmRefund(OrderStateMachine stateMachine) {System.out.println("当前订单状态不支持确认退款,已忽略");} } - 第三步:新增两个状态,退款中与已退款
 
public class RefundingState implements OrderState {@Overridepublic OrderStateEnum name() {return OrderStateEnum.REFUNDING;}@Overridepublic void confirmRefund(OrderStateMachine stateMachine) {stateMachine.setCurrentState(new RefundedState());}
}
 
public class RefundedState implements OrderState {@Overridepublic OrderStateEnum name() {return OrderStateEnum.REFUNDED;}
}
 
- 第四步:拓展原有状态,待发货,待收货,已收货
 
public class WaitDeliverState implements OrderState {// 省略已有代码……@Overridepublic void refund(OrderStateMachine stateMachine) {stateMachine.setCurrentState(new RefundingState());}
}
 
public class WaitReceiveState implements OrderState {// 省略已有代码……@Overridepublic void refund(OrderStateMachine stateMachine) {stateMachine.setCurrentState(new RefundingState());}}
 
public class ReceivedState implements OrderState {// 省略已有代码……@Overridepublic void refund(OrderStateMachine stateMachine) {stateMachine.setCurrentState(new RefundingState());}}
 
- 第五步:拓展状态机
 
public class OrderStateMachine {public static final Map<OrderStateEnum, OrderState> ORDER_STATE_MAP = new HashMap<>();static {// 省略已有状态……ORDER_STATE_MAP.put(OrderStateEnum.REFUNDING, new RefundingState());ORDER_STATE_MAP.put(OrderStateEnum.REFUNDED, new RefundedState());}// 省略已有方法……void refund() {currentState.refund(this);}void confirmRefund() {currentState.confirmRefund(this);}
}
 
2.2.2.测试
在上面的代码中可以看到,都是在对配置进行追加,而没有对原有的逻辑做任何的修改,然后写一个测试:
public class OrderService {public static void main(String[] args) {OrderStateMachine stateMachine = new OrderStateMachine(OrderStateEnum.WAIT_DELIVER);invoke(stateMachine::pay, "用户支付", stateMachine);invoke(stateMachine::deliver, "商家发货", stateMachine);invoke(stateMachine::receive, "用户收货", stateMachine);invoke(stateMachine::cancel, "取消支付", stateMachine);invoke(stateMachine::refund, "申请退款", stateMachine);invoke(stateMachine::confirmRefund, "确认退款", stateMachine);}public static void invoke(Runnable runnable, String desc, OrderStateMachine stateMachine) {System.out.println(desc + "前订单状态: " + stateMachine.getCurrentState().orderStateType().getDesc());runnable.run();System.out.println(desc + "后订单状态: " + stateMachine.getCurrentState().orderStateType().getDesc());System.out.println("------------------");}}
 
查看日志,是否触发退款:
用户支付前订单状态: 待发货
|--当前订单状态不支持支付,已忽略
用户支付后订单状态: 待发货
------------------
商家发货前订单状态: 待发货
商家发货后订单状态: 待收货
------------------
用户收货前订单状态: 待收货
用户收货后订单状态: 已收货
------------------
取消支付前订单状态: 已收货
|--当前订单状态不支持取消,已忽略
取消支付后订单状态: 已收货
------------------
申请退款前订单状态: 已收货
申请退款后订单状态: 退款中
------------------
确认退款前订单状态: 退款中
确认退款后订单状态: 已退款
------------------
 
2.3.小结
从上面的代码可以看到,通过状态模式可以很轻松的对状态进行拓展。
不过上面的例子中没有对状态机中的动作进行实现,其实动作和状态转换的逻辑放在一起就可以了,即通过事件(方法调用) 可以变更状态,同时也能够触发对应的动作。
此外,代码中只是状态机的流程,实际的开发中应该将状态机关联到对应的业务实体中,通过业务实体的实时状态来创建状态机,在完成状态流转之后再将状态更新到业务实体中。
3.总结
本篇主要讲述了如何通过状态模式来实现一个状态机。状态模式的实现,代码结构清晰(相对于if/else,switch)拓展性强,同时也起到了良好的封装效果(状态在状态机内部流转,业务流程不需要关心状态到底是怎么流转的)。
当然缺点就是类膨胀问题,类会比较多,如果状态非常复杂的情况下,也可以采取其他办法来实现状态机,例如查表法。
总之,要分析并实现一个业务流程中的状态流转的时候,先画出状态图,以状态图为指导来选择状态机的实现方式即可,在状态相对不那么复杂的情况下,可以优先考虑使用状态模式。
附:《【UML建模】状态图(State Machine Diagram)》
相关文章:
【设计模式】订单状态流传中的状态机与状态模式
文章目录 1. 前言2.状态模式2.1.订单状态流转案例2.1.1.状态枚举定义2.1.2.状态接口与实现2.1.3.状态机2.1.4.测试 2.2.退款状态的拓展2.2.1.代码拓展2.2.2.测试 2.3.小结 3.总结 1. 前言 状态模式一般是用在对象内部的状态流转场景中,用来实现状态机。 什么是状态…...
2.js中attr()用来修改或者添加属性或者属性值
attr()可以用来修改或者添加属性或者属性值 例:<input type"button" class"btn btn-info" id"subbtn" style"font-size:12px" value"我也说一句"/>1.如果想获取input中value的值 $(#subbtn).attr(value);…...
【虫洞攻击检测】使用多层神经网络的移动自组织网络中的虫洞攻击检测研究(Matlab代码实现)
💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 ⛳️座右铭&a…...
微分流形学习之一:基本定义
微分流形学习之一:基本定义引入 引言一、微分流形的历史简介二、拓扑空间三、微分流形 引言 本文是作者在学习微分流形的时候的笔记,尽量严格完整,并带有一定理解,绝不是结论的简单罗列。如果读者知道数学分析中的 ϵ − δ \ep…...
[C++]笔记-制作自己的静态库
一.静态库的创建 在项目属性c/c里面,选用无预编译头,创建头文件与cpp文件,需要注意release模式下还是debug模式,在用库时候要与该模式相匹配,库的函数实现是外界无法看到的,最后在要使用的项目里面导入.h文件和.lib文件 二.使用一个循环给二维数组赋值 行数 : 第几个元素 / …...
优酷视频码率、爱奇艺视频码率、B站视频码率、抖音视频码率对比
优酷视频码率、爱奇艺视频码率与YouTube视频码率对比 优酷视频码率: 优酷的视频码率可以根据视频质量、分辨率和内容类型而变化。一般而言,优酷提供了不同的码率选项,包括较低的标清(SD)码率和较高的高清(…...
用pytorch实现google net
GoogleNet(也称为Inception v1)是由Google在2014年提出的一个深度卷积神经网络架构。它在ImageNet Large Scale Visual Recognition Challenge (ILSVRC) 2014比赛中取得了优秀的成绩,并引起了广泛的关注。 GoogleNet的设计目标是构建一个更…...
2023-8-15差分矩阵
题目链接:差分矩阵 #include <iostream>using namespace std;const int N 1010;int n, m, q; int a[N][N], b[N][N];void insert(int x1, int y1, int x2, int y2, int c) {b[x1][y1] c;b[x1][y2 1] - c;b[x2 1][y1] - c;b[x2 1][y2 1] c; }int main…...
物理公式分类
(99 封私信 / 81 条消息) 定义式和决定式有什么区别,怎么区分? - 知乎 (zhihu.com) 1、首先,定义一个物理符号(物理量)来表征物理世界最直观/最基本的物理现象,例如,长度(米…...
vue实现登录注册
目录 一、登录页面 二、注册页面 三、配置路由 一、登录页面 <template><div class"login_container" style"background-color: rgb(243,243,243);height: 93.68vh;background-image: url(https://ts1.cn.mm.bing.net/th/id/R-C.f878c96c4179c501a6…...
SpringBoot复习:(55)在service类中的方法上加上@Transactional注解后,Spring底层是怎么生成代理对象的?
SpringBoot run方法代码如下: 可以看到它会调用refreshContext方法来刷新Spring容器,这个refreshContext方法最终会调用AbstractApplicationContext的refresh方法,代码如下 如上图,refresh方法最终会调用finisheBeanFactoryInit…...
常用的图像校正方法
在数字图像处理中,常用的校正方法包括明场均匀性校正、查找表(LUT)校正和伽玛(Gamma)校正。这些校正方法分别针对不同的图像问题,可以改善图像质量,提升图像的可读性和可分析性。下面是这三种校…...
AWS security 培训笔记
云计算的好处 Amazon S3 (Storage) Amazon EC2 (Compute) 上图aws 的几个支柱:安全是其中一个啦 其中安全有几个方面 IAMdetection基础架构保护数据保护应急响应 关于云供应商的责任 data center 原来长这样 ,据说非常之隐蔽的 如果有天退役了…...
设计模式之代理模式(Proxy)的C++实现
1、代理模式的提出 在组件的开发过程中,有些对象由于某种原因(比如对象创建的开销很大,或者对象的一些操作需要做安全控制,或者需要进程外的访问等),会使Client使用者在操作这类对象时可能会存在问题&…...
vim 配置环境变量与 JDK 编译器异常
vim 配置环境变量 使用 vim 打开系统中的配置信息(不存在将会创建): vim ~/.bash_profile 以配置两个版本 JDK 为例(前提是已安装 JDK),使用上述命令打开配置信息: 输入法调成英文,输入 i&…...
TiDB v7.1.0 跨业务系统多租户解决方案
本文介绍了 TiDB 数据库的资源管控技术,并通过业务测试验证了效果。资源管控技术旨在解决多业务共用一个集群时的资源隔离和负载问题,通过资源组概念,可以限制不同业务的计算和 I/O 资源,实现资源隔离和优先级调度,提高…...
【题解】二叉树中和为某一值的路径(一)
二叉树中和为某一值的路径(一) 题目链接:二叉树中和为某一值的路径(一) 解题思路1:递归 我们或许想记录下每一条从根节点到叶子节点的路径,计算出该条路径的和,但此种思路用递归稍麻烦,我们可以试着把和转换为差&am…...
css中变量和使用变量和运算
变量: 语法:--css变量名:值; --view-theme: #1a99fb; css使用变量: 语法:属性名:var( --css变量名 ); color: var(--view-theme); css运算: 语法:属性名…...
数据结构之线性表的类型运用Linear Lists: 数组,栈,队列,链表
线性表 定义 一个最简单,最基本的数据结构。一个线性表由多个相同类型的元素穿在一次,并且每一个元素都一个前驱(前一个元素)和后继(后一个元素)。 线性表的类型 常见的类型:数组、栈、队列…...
成瘾机制中微生物群的神秘角色
谷禾健康 成瘾是一种大脑疾病,受害者无法控制地对某种物质或行为产生强烈的依赖和渴求,尽管这种行为会产生有害的后果。成瘾包括一系列物质滥用障碍,例如药物、酒精、香烟,过度饮食。近年来,吸毒成瘾急剧上升ÿ…...
深入剖析AI大模型:大模型时代的 Prompt 工程全解析
今天聊的内容,我认为是AI开发里面非常重要的内容。它在AI开发里无处不在,当你对 AI 助手说 "用李白的风格写一首关于人工智能的诗",或者让翻译模型 "将这段合同翻译成商务日语" 时,输入的这句话就是 Prompt。…...
【杂谈】-递归进化:人工智能的自我改进与监管挑战
递归进化:人工智能的自我改进与监管挑战 文章目录 递归进化:人工智能的自我改进与监管挑战1、自我改进型人工智能的崛起2、人工智能如何挑战人类监管?3、确保人工智能受控的策略4、人类在人工智能发展中的角色5、平衡自主性与控制力6、总结与…...
基于FPGA的PID算法学习———实现PID比例控制算法
基于FPGA的PID算法学习 前言一、PID算法分析二、PID仿真分析1. PID代码2.PI代码3.P代码4.顶层5.测试文件6.仿真波形 总结 前言 学习内容:参考网站: PID算法控制 PID即:Proportional(比例)、Integral(积分&…...
Golang dig框架与GraphQL的完美结合
将 Go 的 Dig 依赖注入框架与 GraphQL 结合使用,可以显著提升应用程序的可维护性、可测试性以及灵活性。 Dig 是一个强大的依赖注入容器,能够帮助开发者更好地管理复杂的依赖关系,而 GraphQL 则是一种用于 API 的查询语言,能够提…...
376. Wiggle Subsequence
376. Wiggle Subsequence 代码 class Solution { public:int wiggleMaxLength(vector<int>& nums) {int n nums.size();int res 1;int prediff 0;int curdiff 0;for(int i 0;i < n-1;i){curdiff nums[i1] - nums[i];if( (prediff > 0 && curdif…...
React19源码系列之 事件插件系统
事件类别 事件类型 定义 文档 Event Event 接口表示在 EventTarget 上出现的事件。 Event - Web API | MDN UIEvent UIEvent 接口表示简单的用户界面事件。 UIEvent - Web API | MDN KeyboardEvent KeyboardEvent 对象描述了用户与键盘的交互。 KeyboardEvent - Web…...
Java多线程实现之Thread类深度解析
Java多线程实现之Thread类深度解析 一、多线程基础概念1.1 什么是线程1.2 多线程的优势1.3 Java多线程模型 二、Thread类的基本结构与构造函数2.1 Thread类的继承关系2.2 构造函数 三、创建和启动线程3.1 继承Thread类创建线程3.2 实现Runnable接口创建线程 四、Thread类的核心…...
CSS设置元素的宽度根据其内容自动调整
width: fit-content 是 CSS 中的一个属性值,用于设置元素的宽度根据其内容自动调整,确保宽度刚好容纳内容而不会超出。 效果对比 默认情况(width: auto): 块级元素(如 <div>)会占满父容器…...
【Redis】笔记|第8节|大厂高并发缓存架构实战与优化
缓存架构 代码结构 代码详情 功能点: 多级缓存,先查本地缓存,再查Redis,最后才查数据库热点数据重建逻辑使用分布式锁,二次查询更新缓存采用读写锁提升性能采用Redis的发布订阅机制通知所有实例更新本地缓存适用读多…...
Cilium动手实验室: 精通之旅---13.Cilium LoadBalancer IPAM and L2 Service Announcement
Cilium动手实验室: 精通之旅---13.Cilium LoadBalancer IPAM and L2 Service Announcement 1. LAB环境2. L2公告策略2.1 部署Death Star2.2 访问服务2.3 部署L2公告策略2.4 服务宣告 3. 可视化 ARP 流量3.1 部署新服务3.2 准备可视化3.3 再次请求 4. 自动IPAM4.1 IPAM Pool4.2 …...
