【设计模式】订单状态流传中的状态机与状态模式

news2024/11/23 20:23:15

文章目录

  • 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.状态模式

状态模式将状态抽象成一个个的状态对象,状态机当前持有某个状态对象,就表示当前的状态机处于什么状态。
然后将事件处理为一个个的方法,每个方法中会操作状态机修改状态,有需要的情况下,在修改状态的同时还可以执行某些动作。

把通用部分提取出来后,可以得到这样一个通用类图:
在这里插入图片描述

可以看到上面的StateMachineState关系是双向的,这是因为状态机需要持有状态对象来表示当前状态,以及通过当前的状态对象中的方法进行状态的流转,而流转的结果需要重新set到状态机中,又要求State必须持有状态机对象。
当然,这里StateStateMachine的关系也可以通过依赖来表示。

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 {
    
        @Override
        public OrderStateEnum orderStateType() {
            return OrderStateEnum.WAIT_PAYMENT;
        }
    
        @Override
        public void pay(OrderStateMachine stateMachine) {
            stateMachine.setCurrentState(new WaitDeliverState());
        }
    
        @Override
        public void cancel(OrderStateMachine stateMachine) {
            stateMachine.setCurrentState(new CancelState());
        }
    }
    
  • 待发货状态:有发货事件
    public class WaitDeliverState implements OrderState {
    
        @Override
        public OrderStateEnum orderStateType() {
            return OrderStateEnum.WAIT_DELIVER;
        }
    
        @Override
        public void deliver(OrderStateMachine stateMachine) {
            stateMachine.setCurrentState(new WaitReceiveState());
        }
    }
    
  • 待收货状态:有收货事件
    public class WaitReceiveState implements OrderState {
    
        @Override
        public OrderStateEnum orderStateType() {
            return OrderStateEnum.WAIT_RECEIVE;
        }
    
        @Override
        public void receive(OrderStateMachine stateMachine) {
            stateMachine.setCurrentState(new ReceivedState());
        }
    }
    
  • 已收货状态:状态结束点,没有其他事件
    public class ReceivedState implements OrderState {
    
        @Override
        public OrderStateEnum orderStateType() {
            return OrderStateEnum.RECEIVED;
        }
    
    }
    
  • 取消状态:状态结束点,没有其他事件
    public class CancelState implements OrderState {
    
        @Override
        public 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 {

    @Override
    public OrderStateEnum name() {
        return OrderStateEnum.REFUNDING;
    }

    @Override
    public void confirmRefund(OrderStateMachine stateMachine) {
        stateMachine.setCurrentState(new RefundedState());
    }
}
public class RefundedState implements OrderState {

    @Override
    public OrderStateEnum name() {
        return OrderStateEnum.REFUNDED;
    }
}
  • 第四步:拓展原有状态,待发货待收货已收货
public class WaitDeliverState implements OrderState {
    // 省略已有代码……
    
    @Override
    public void refund(OrderStateMachine stateMachine) {
        stateMachine.setCurrentState(new RefundingState());
    }
}
public class WaitReceiveState implements OrderState {
    // 省略已有代码……
    
    @Override
    public void refund(OrderStateMachine stateMachine) {
        stateMachine.setCurrentState(new RefundingState());
    }

}
public class ReceivedState implements OrderState {
    // 省略已有代码……

    @Override
    public 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)》

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

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

相关文章

Lnton羚通关于PyTorch的保存和加载模型基础知识

SAVE AND LOAD THE MODEL (保存和加载模型) PyTorch 模型存储学习到的参数在内部状态字典中&#xff0c;称为 state_dict, 他们的持久化通过 torch.save 方法。 model models.shufflenet_v2_x0_5(pretrainedTrue) torch.save(model, "../../data/ShuffleNetV2_X0.5.pth…

如何在前端实现WebSocket发送和接收TCP消息(多线程模式)

目录 第一步&#xff1a;创建WebSocket连接第二步&#xff1a;监听WebSocket事件第三步&#xff1a;发送消息第四步&#xff1a;后端处理函数说明 当在前端实现WebSocket发送和接收TCP消息时&#xff0c;可以使用以下步骤来实现多线程模式。本文将详细介绍如何在前端实现WebSoc…

DDT数据驱动+Pytest+Allure+自定义代码封装

DDT数据驱动PytestAllure自定义代码封装 CASE --判断运行--单文件去进行运行 CASE --判断 运行–单文件去进行运行 一次性运行多个case——Pytest 1.前置条件&#xff1a; pip install pytest 2.Pytest——脚手架——可以站在它的身上做一系列的事情 3.规则的遵守 4.test_开头…

传感网应用开发实训室建设方案

传感网应用开发实训室概述 物联网是我国战略性新兴产业的重要组成部分&#xff0c;《物联网“十二五”发展规划》圈定了10大领域重点示范工程&#xff0c;第一个关键技术创新工程提出“充分发挥企业主体作用&#xff0c;积极利用高校和研究所实验室的现有研究成果&#xff0c;在…

springBoot 配置文件引入 redis 的相关参数说明

在Spring Boot应用中使用Redis作为缓存或数据存储时&#xff0c;可以在应用的配置文件中配置相关参数。下面是常用的Redis配置参数及其说明&#xff1a; spring.redis.host: Redis服务器主机地址&#xff0c;默认为localhost。spring.redis.port: Redis服务器端口&#xff0c;…

游戏工作室如何使用代理服务器防封

嘿&#xff0c;各位游戏工作室的小伙伴们&#xff01;作为一名专业的程序员&#xff0c;我今天要和大家分享一个有关代理服务器的技巧&#xff0c;这个技巧可以帮助你们解决封号和封禁的问题。 首先&#xff0c;我们得明白为什么要使用代理服务器来解决封号和封禁的问题。在我们…

酷开科技 | 酷开系统影视库,中外影片一网打尽

相信大家在日常的生活中也会遇到一些令人焦虑的事情&#xff0c;这个时候你们又是如何处理的呢&#xff1f;你可以听听音乐、出去步行或者看场电影。认真聆听音乐中的每一段旋律&#xff0c;感受乐器的灵魂&#xff0c;特别能够让人沉静下来&#xff1b;走在城市的街道上&#…

RabiitMq-4工作队列/消息应答/消息持久化/不公平分发

1.工作队列 工作机制类似一个生产者&#xff0c;多个消费者。工作队列采用轮训的机制&#xff0c;即工作线程一次只能处理一个消息&#xff0c;轮流处理 公共方法 public class MqUtiles {public static final String QUEUE_NAME"hello";public static Channel fu…

马修斯相关系数MCC简介

在评估机器学习模型的性能时&#xff0c;F1score都被首选指标。在本文中&#xff0c;我们将介绍一个值得更多关注和认可的替代度量:马修斯相关系数(MCC)。 F1score通过协调准确率和召回率来计算&#xff0c;旨在在两者之间取得平衡。但是假设我们有一个具有以下混淆矩阵的数据集…

亿发软件:多门店进销存商品信息管理解决方案,专业记账开单软件

物资难以有效管理&#xff1a;不同种类的物资繁多&#xff0c;难以实现一体化管理&#xff1b;数据更新缓慢&#xff1a;数据无法实时更新&#xff0c;难以进行成本和毛利核算&#xff0c;导致企业盈利状况不明晰&#xff1b;沟通效率低下&#xff1a;收发货单分配不及时&#…

应用在水土壤水分检测中的国产电容传感芯片

土壤含水量测定是指土壤中各种液态水分的定量确定。有时还包括冰和部分矿物结晶水&#xff0c;一般可分为采样法和原位测定法两大类。采样法是在田间采样后测定土样含水量&#xff0c;原位测定法是利用仪器设备直接在田间测定土壤含水量的方法。 农业是支撑国民经济建设和发展…

Docker容器:docker的资源控制及docker数据管理

文章目录 一.docker的资源控制1.CPU 资源控制1.1 资源控制工具1.2 cgroups有四大功能1.3 设置CPU使用率上限1.4 进行CPU压力测试1.5 设置50%的比例分配CPU使用时间上限1.6 设置CPU资源占用比&#xff08;设置多个容器时才有效&#xff09;1.6.1 两个容器测试cpu1.6.2 设置容器绑…

10个最强大的3D城市建模软件

城市设计师在塑造城市的物质环境方面发挥着至关重要的作用。 他们创建和规划公共空间、设计建筑并创造反映社区独特特征的城市景观。 为了实现这些目标&#xff0c;城市设计师严重依赖 3D 建模软件。 这些软件程序提供了创建、可视化和操作建筑物和景观 3D 模型的能力&#xff…

PHREEQC模型化学热力学理论和数据库.dat、各种模拟反应平衡反应模拟、化学动力模拟、反应迁移模拟

PHREEQC是一个用于计算多种低温水文地球化学反应的计算机软件&#xff0c;以离子缔合水模型为基础的PHREEQC能够&#xff08;1&#xff09;计算物质形成种类与饱和指数&#xff1b;&#xff08;2&#xff09;模拟地球化学反演过程&#xff1b;&#xff08;3&#xff09;计算批反…

Python数据分析实战-多线程并发处理列表(附源码和实现效果)

实现功能 Python数据分析实战-多线程并发处理列表 实现代码 import threading有15个列表&#xff0c;尝试多进程并发处理&#xff0c;每个列表一个进程&#xff0c;进程数和 CPU 核数一致def sum_list(lst):return sum(lst)if __name__ __main__:lists [[1,2,3], [4,5,6], …

实现el-table两列多选框且不可同时勾选

1、效果图如下&#xff0c;功能&#xff1a;必修和选修不可同时勾选 2、代码如下 <template><el-table :data"addTableData" style"width: 100%"><el-table-column label"必修" width"55px" align"center"…

CentOS7 上安装 Percona XtraBackup

介绍 Percona XtraBackup是一款适用于基于MySQL的服务器的开源热备份实用程序&#xff0c;在备份期间不会锁定数据库。它可以备份MySQL 5.1、5.5、5.6 和 5.7 服务器上的InnoDB、XtraDB 和MyISAM表的数据&#xff0c;以及带有 XtraDB 的 Percona Server。 下载网址 2.4文档地…

中大许少辉博士中国建筑出版传媒八一新书《乡村振兴战略下传统村落文化旅游设计》百度百科新闻

中大许少辉博士中国建筑出版传媒八一新书《乡村振兴战略下传统村落文化旅游设计》百度百科新闻&#xff1a; 乡村振兴战略下传统村落文化旅游设计 - 百度百科 https://baike.baidu.com/item/乡村振兴战略下传统村落文化旅游设计/62588677 概览 《乡村振兴战略下传统村落文化旅游…

链游再进化 Web3版CSGO来袭

过去几年&#xff0c;游戏开发者们一直希望借Web3这个价值流通网络&#xff0c;改造传统游戏的经济系统&#xff0c;将虚拟资产的掌管权交给用户&#xff0c;让资产自由地在市场流通。 Web3游戏发展史上&#xff0c;涌现过CryptoKitties、Axie Infinity两大爆款&#xff0c;但…

C语言暑假刷题冲刺篇——day3

目录 一、选择题 二、编程题 &#x1f388;个人主页&#xff1a;库库的里昂 &#x1f390;CSDN新晋作者 &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏✨收录专栏&#xff1a;C语言每日一练✨其他专栏&#xff1a;代码小游戏C语言初阶&#x1f91d;希望作者的文章能对你有…