Spring状态机应用项目实现优雅设计 -- 知识铺
mall学习教程官网:macrozheng.com
作者:cafebabe
来源:juejin.cn/post/6844904098676867086
状态模式在生活场景中也是比较常见的。比如我们平时网购的订单状态变化,还有平时坐电梯,电梯状态的变化。
在软件开发过程中,对于某一项的操作,可能存在不同的情况。通常处理多情况问题最直接的办法就是使用if…else或者switch…case条件语句进行判断。这种做法对于复杂状态的判断天然存在弊端:判断条件语句过于臃肿,可读性较差,不具备扩展性,维度难度也很大。如果转换一下思维,将这些不同状态独立起来用各种不同的类进行表示,系统处理哪种情况,直接使用相应的状态类进行处理,消除条件判断语句,代码也更加具有层次感,且具备良好的扩展能力。
状态模式(State Pattern)也成为状态机模式(State Machine Pattern),是允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。状态模式中类的行为是由状态决定的,不同的状态下有不同的行为。其意图是让一个对象在其内部改变的时候,其行为也随之改变。状态模式的核心就是状态与行为绑定,不同的状态对应不同的行为。
一、状态模式的应用场景
状态模式适用于以下几种场景:
-
行为随状态改变而改变场景;
-
一个操作中含有庞大的多分支机构,并且这些分支取决于对象的状态。
状态模式主要包含三种角色:
-
环境类角色(Context):定义客户端需要的接囗,内部维护一个当前状态实例,并负责具体状态的切换;
-
抽象状态角色(State):定义该状态下的行为,可以有一个或多个行为;
-
具体状态角色(ConcreteState):具体实现该状态对应的行为并且在需要的肩况下进行状态切换。
这或许是一个对你有用的开源项目,mall项目是一套基于 SpringBoot3 + JDK 17 + Vue 实现的电商系统(Github标星60K),采用Docker容器化部署,后端支持多模块和微服务架构。包括前台商城项目和后台管理系统,能支持完整的订单流程!涵盖商品、订单、购物车、权限、优惠券、会员、支付等功能!
项目演示:
1.1 状态模式在业务场景中的应用
我们在某社区阅读文章的时候,如果觉得某篇文章写得好,就会转发、收藏并且评论。如果用户处于登录情况下,我们就可以做评论、转发、收藏这些行为。否则需要跳转到登录页面,登录之后才能执行先前的动作。那么这里涉及到的状态有两种:已登录和未登录,行为有三种:评论、转发、收藏。下面使用代码来实现这些逻辑,首先创建抽象状态角色类UserState:
public abstract class UserState {
private AppContext appContext;
public void setAppContext(AppContext appContext) {
this.appContext = appContext;
}
public abstract void forward();
public abstract void collect();
public abstract void comment(String comment);
}
创建登录状态LoginState类:
public class LoginState extends UserState {
@Override
public void forward() {
System.out.println("转发成功!");
}
@Override
public void collect() {
System.out.println("收藏成功!");
}
@Override
public void comment(String comment) {
System.out.println("评论成功,内容是:" + comment);
}
}
接着创建未登录状态UnLoginState类:
public class UnLoginState extends UserState {
@Override
public void forward() {
forward2Login();
this.appContext.forward();
}
@Override
public void collect() {
forward2Login();
this.appContext.collect();
}
@Override
public void comment(String comment) {
forward2Login();
this.appContext.comment(comment);
}
private void forward2Login() {
System.out.println("跳转到登录页面!");
this.appContext.setState(this.appContext.LOGIN_STATE);
}
}
创建上下文角色AppContext类:
public class AppContext {
public static final UserState LOGIN_STATE = new LoginState();
public static final UserState UNLOGIN_STATE = new UnLoginState();
private UserState currentState = UNLOGIN_STATE;
{
UNLOGIN_STATE.setAppContext(this);
LOGIN_STATE.setAppContext(this);
}
public void setState(UserState state) {
this.currentState = state;
this.currentState.setAppContext(this);
}
public UserState getState() {
return this.currentState;
}
public void forward() {
this.currentState.forward();
}
public void collect() {
this.currentState.collect();
}
public void comment(String comment) {
this.currentState.comment(comment);
}
}
测试main方法:
public static void main(String[] args) {
AppContext context = new AppContext();
context.forward();
context.collect();
context.comment("说的太好了,双手双脚给个赞👍");
}
运行结果:
1.2 利用状态机实现订单状态流转控制
状态机是状态模式的一种应用,相当于上下文角色的一个升级版本。在工作流或游戏等各种系统中有大量使用,比如各种工作流引擎,它几乎是状态机的子集和实现,封装状态的变化规则。Spring提供了一个很好的解决方案。Spring的组件名称就叫StateMachine(状态机)。状态机帮助开发者简化状态控制的开发过程,让状态机结构更加层次化。下面来用Spring状态机模拟一个订单状态流转的过程。
1、pom依赖
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-core</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>
2、创建订单实体类
public class Order {
private int id;
private OrderStatus status;
public void setStatus(OrderStatus status) {
this.status = status;
}
public OrderStatus getStatus() {
return status;
}
public void setId(int id) {
this.id = id;
}
public int getId() {
return id;
}
@Override
public String toString() {
return "订单号:" + id + ", 订单状态:" + status;
}
}
3、创建订单状态枚举类和状态转换枚举类
/**
* 订单状态
*/
public enum OrderStatus {
// 待支付,待发货,待收货,订单结束
WAIT_PAYMENT, WAIT_DELIVER, WAIT_RECEIVE, FINISH;
}
/**
* 订单状态改变事件
*/
public enum OrderStatusChangeEvent {
// 支付,发货,确认收货
PAYED, DELIVERY, RECEIVED;
}
4、添加状态流转配置
/**
* 订单状态机配置
*/
@Configuration
@EnableStateMachine(name = "orderStateMachine")
public class OrderStateMachineConfig extends StateMachineConfigurerAdapter<OrderStatus, OrderStatusChangeEvent> {
/**
* 配置状态
* @param states
* @throws Exception
*/
public void configure(StateMachineStateConfigurer<OrderStatus, OrderStatusChangeEvent> states) throws Exception {
states
.withStates()
.initial(OrderStatus.WAIT_PAYMENT)
.states(EnumSet.allOf(OrderStatus.class));
}
/**
* 配置状态转换事件关系
* @param transitions
* @throws Exception
*/
public void configure(StateMachineTransitionConfigurer<OrderStatus, OrderStatusChangeEvent> transitions) throws Exception {
transitions
.withExternal().source(OrderStatus.WAIT_PAYMENT).target(OrderStatus.WAIT_DELIVER).event(OrderStatusChangeEvent.PAYED)
.and()
.withExternal().source(OrderStatus.WAIT_DELIVER).target(OrderStatus.WAIT_RECEIVE).event(OrderStatusChangeEvent.DELIVERY)
.and()
.withExternal().source(OrderStatus.WAIT_RECEIVE).target(OrderStatus.FINISH).event(OrderStatusChangeEvent.RECEIVED);
}
/**
* 持久化配置
* 实际使用中,可以配合redis等,进行持久化操作
* @return
*/
@Bean
public DefaultStateMachinePersister persister(){
return new DefaultStateMachinePersister<>(new StateMachinePersist<Object, Object, Order>() {
@Override
public void write(StateMachineContext<Object, Object> context, Order order) throws Exception {
//此处并没有进行持久化操作
}
@Override
public StateMachineContext<Object, Object> read(Order order) throws Exception {
//此处直接获取order中的状态,其实并没有进行持久化读取操作
return new DefaultStateMachineContext(order.getStatus(), null, null, null);
}
});
}
}
5、添加订单状态监听器
@Component("orderStateListener")
@WithStateMachine(name = "orderStateMachine")
public class OrderStateListenerImpl{
@OnTransition(source = "WAIT_PAYMENT", target = "WAIT_DELIVER")
public boolean payTransition(Message<OrderStatusChangeEvent> message) {
Order order = (Order) message.getHeaders().get("order");
order.setStatus(OrderStatus.WAIT_DELIVER);
System.out.println("支付,状态机反馈信息:" + message.getHeaders().toString());
return true;
}
@OnTransition(source = "WAIT_DELIVER", target = "WAIT_RECEIVE")
public boolean deliverTransition(Message<OrderStatusChangeEvent> message) {
Order order = (Order) message.getHeaders().get("order");
order.setStatus(OrderStatus.WAIT_RECEIVE);
System.out.println("发货,状态机反馈信息:" + message.getHeaders().toString());
return true;
}
@OnTransition(source = "WAIT_RECEIVE", target = "FINISH")
public boolean receiveTransition(Message<OrderStatusChangeEvent> message){
Order order = (Order) message.getHeaders().get("order");
order.setStatus(OrderStatus.FINISH);
System.out.println("收货,状态机反馈信息:" + message.getHeaders().toString());
return true;
}
}
6、创建IOrderService接口
public interface IOrderService {
//创建新订单
Order create();
//发起支付
Order pay(int id);
//订单发货
Order deliver(int id);
//订单收货
Order receive(int id);
//获取所有订单信息
Map<Integer, Order> getOrders();
}
7、在Service中添加业务逻辑
@Service("orderService")
public class OrderServiceImpl implements IOrderService {
@Autowired
private StateMachine<OrderStatus, OrderStatusChangeEvent> orderStateMachine;
@Autowired
private StateMachinePersister<OrderStatus, OrderStatusChangeEvent, Order> persister;
private int id = 1;
private Map<Integer, Order> orders = new HashMap<>();
public Order create() {
Order order = new Order();
order.setStatus(OrderStatus.WAIT_PAYMENT);
order.setId(id++);
orders.put(order.getId(), order);
return order;
}
public Order pay(int id) {
Order order = orders.get(id);
System.out.println("线程名称:" + Thread.currentThread().getName() + " 尝试支付,订单号:" + id);
Message message = MessageBuilder.withPayload(OrderStatusChangeEvent.PAYED).setHeader("order", order).build();
if (!sendEvent(message, order)) {
System.out.println("线程名称:" + Thread.currentThread().getName() + " 支付失败, 状态异常,订单号:" + id);
}
return orders.get(id);
}
public Order deliver(int id) {
Order order = orders.get(id);
System.out.println("线程名称:" + Thread.currentThread().getName() + " 尝试发货,订单号:" + id);
if (!sendEvent(MessageBuilder.withPayload(OrderStatusChangeEvent.DELIVERY).setHeader("order", order).build(), orders.get(id))) {
System.out.println("线程名称:" + Thread.currentThread().getName() + " 发货失败,状态异常,订单号:" + id);
}
return orders.get(id);
}
public Order receive(int id) {
Order order = orders.get(id);
System.out.println("线程名称:" + Thread.currentThread().getName() + " 尝试收货,订单号:" + id);
if (!sendEvent(MessageBuilder.withPayload(OrderStatusChangeEvent.RECEIVED).setHeader("order", order).build(), orders.get(id))) {
System.out.println("线程名称:" + Thread.currentThread().getName() + " 收货失败,状态异常,订单号:" + id);
}
return orders.get(id);
}
public Map<Integer, Order> getOrders() {
return orders;
}
/**
* 发送订单状态转换事件
*
* @param message
* @param order
* @return
*/
private synchronized boolean sendEvent(Message<OrderStatusChangeEvent> message, Order order) {
boolean result = false;
try {
orderStateMachine.start();
//尝试恢复状态机状态
persister.restore(orderStateMachine, order);
//添加延迟用于线程安全测试
Thread.sleep(1000);
result = orderStateMachine.sendEvent(message);
//持久化状态机状态
persister.persist(orderStateMachine, order);
} catch (Exception e) {
e.printStackTrace();
} finally {
orderStateMachine.stop();
}
return result;
}
}
测试main方法:
public static void main(String[] args) {
Thread.currentThread().setName("主线程");
ConfigurableApplicationContext context = SpringApplication.run(Test.class,args);
IOrderService orderService = (IOrderService)context.getBean("orderService");
orderService.create();
orderService.create();
orderService.pay(1);
new Thread("客户线程"){
@Override
public void run() {
orderService.deliver(1);
orderService.receive(1);
}
}.start();
orderService.pay(2);
orderService.deliver(2);
orderService.receive(2);
System.out.println("全部订单状态:" + orderService.getOrders());
}
二、状态模式中的源码体现
状态模式的具体应用在源码中非常少见,在源码中一般只是提供一种通用的解决方案。如果一定要找,当然也是能找到的。经历千辛万苦,持续烧脑,下面我们来看一个在JSF源码中的Lifecycle类。JSF也算是一个比较经典的前端框架,那么没用过的小伙伴也没关系,我们这是只是分析一下其设计思想。在JSF中它所有页面的处理分为6个阶段,被定义在了Phaseld类中用不同的常量来表示生命周期阶段,源码如下:
public class PhaseId implements Comparable {
private final int ordinal;
private String phaseName;
private static int nextOrdinal = 0;
private static final String ANY_PHASE_NAME = "ANY";
public static final PhaseId ANY_PHASE = new PhaseId("ANY");
private static final String RESTORE_VIEW_NAME = "RESTORE_VIEW";
public static final PhaseId RESTORE_VIEW = new PhaseId("RESTORE_VIEW");
private static final String APPLY_REQUEST_VALUES_NAME = "APPLY_REQUEST_VALUES";
public static final PhaseId APPLY_REQUEST_VALUES = new PhaseId("APPLY_REQUEST_VALUES");
private static final String PROCESS_VALIDATIONS_NAME = "PROCESS_VALIDATIONS";
public static final PhaseId PROCESS_VALIDATIONS = new PhaseId("PROCESS_VALIDATIONS");
private static final String UPDATE_MODEL_VALUES_NAME = "UPDATE_MODEL_VALUES";
public static final PhaseId UPDATE_MODEL_VALUES = new PhaseId("UPDATE_MODEL_VALUES");
private static final String INVOKE_APPLICATION_NAME = "INVOKE_APPLICATION";
public static final PhaseId INVOKE_APPLICATION = new PhaseId("INVOKE_APPLICATION");
private static final String RENDER_RESPONSE_NAME = "RENDER_RESPONSE";
public static final PhaseId RENDER_RESPONSE = new PhaseId("RENDER_RESPONSE");
private static final PhaseId[] values;
public static final List VALUES;
private PhaseId(String newPhaseName) {
this.ordinal = nextOrdinal++;
this.phaseName = null;
this.phaseName = newPhaseName;
}
public int compareTo(Object other) {
return this.ordinal - ((PhaseId)other).ordinal;
}
public int getOrdinal() {
return this.ordinal;
}
public String toString() {
return null == this.phaseName ? String.valueOf(this.ordinal) : this.phaseName + ' ' + this.ordinal;
}
static {
values = new PhaseId[]{ANY_PHASE, RESTORE_VIEW, APPLY_REQUEST_VALUES, PROCESS_VALIDATIONS, UPDATE_MODEL_VALUES, INVOKE_APPLICATION, RENDER_RESPONSE};
VALUES = Collections.unmodifiableList(Arrays.asList(values));
}
}
那么这些状态的切换都在Lifecycle的execute()方、去中进行。其中会传一个参数FacesContext对象,最终所有的状态都被FacesContext保存。在此呢,我们就不做继续深入的分析。
三、状态模式的相关模式
3.1 状态模式与责任链模式
状态模式和责任链模式都能消除if分支过多的问题。但某些情况下,状态模式中的状态可以理解为责任,那么这种情况下,两种模式都可以使用。
从定义来看,状态模式强调的是一个对象内在状态的改变,而责任链模式强调的是外部节点对象间的改变。
从其代码实现上来看,他们间最大的区别就是状态模式各个状态对象知道自己下一个要进入的状态对象而责任链模式并不清楚其下一个节点处理对象,因为链式组装由客户端负责。
3.2 状态模式与策略模式
状态模式和策略模式的UML类图架构几乎完全一样,但他们的应用场景是不一样的。策略模式多种算法行为择其一都能满足,彼此之间是独立的用户可自行更换策略算法,而状态模式各个状态间是存在相互关系的,彼此之间在一定条件下存在自动切换状态效果,且用户无法指定状态,只能设置初始状态。
四、状态模式的优缺点
优点:
-
结构清晰:将状态独立为类,消除了冗余的if…else或switch…case语句,使代码更加简洁,提高系统可维护性;
-
将状态转换显示化通常的对象内部都是使用数值类型来定义状态,状态的切换是通过賦值进行表现,不够直观,而使用状态类,在切换状态时,是以不同的类进行表示,转换目的更加明确;
-
状态类职责明确且具备扩展性。
缺点:
-
类膨胀:如果一个事物具备很多状态,则会造成状态类太多;
-
状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱;
-
状态模式对开闭原则的支持并不太好,对于可以切换状态的状态模式增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。
Github上标星60K
的电商实战项目mall,全套 视频教程(2023最新版) 已更新完毕!全套教程约40小时,共113期
,通过这套教程你可以拥有一个涵盖主流Java技术栈的完整项目经验
,同时提高自己独立开发一个项目的能力
,下面是项目的整体架构图,感兴趣的小伙伴可以点击链接 mall视频教程 加入学习。
整套 视频教程 的内容还是非常完善的,涵盖了mall项目最佳学习路线、整体框架搭建、业务与技术实现全方位解析、线上Docker环境部署、微服务项目学习等内容,你也可以点击链接 mall视频教程 了解更多内容。
推荐阅读
- 原文作者:知识铺
- 原文链接:https://index.zshipu.com/geek001/post/20240710/Spring%E7%8A%B6%E6%80%81%E6%9C%BA%E5%BA%94%E7%94%A8%E9%A1%B9%E7%9B%AE%E5%AE%9E%E7%8E%B0%E4%BC%98%E9%9B%85%E8%AE%BE%E8%AE%A1--%E7%9F%A5%E8%AF%86%E9%93%BA/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。
- 免责声明:本页面内容均来源于站内编辑发布,部分信息来源互联网,并不意味着本站赞同其观点或者证实其内容的真实性,如涉及版权等问题,请立即联系客服进行更改或删除,保证您的合法权益。转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。也可以邮件至 sblig@126.com