设计模式在电商业务下的实践——状态模式

专栏

持续更新中。

背景

以订单业务为例,存在多种业务操作,订单创建,订单支付,发货,收货等等。而这些操作对应了不同的订单状态,只有在指定的订单状态才能进行指定的订单业务,例如如下订单状态机:

image.png

假如一开始只能在待发货状态下买家才能申请售后退款,得出如下代码

初始代码

订单状态枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码@Getter
@AllArgsConstructor
public enum OrderStateEnum {
WAIT_PAY(0, "待支付"),
WAIT_DELIVER(1, "待发货"),
WAIT_RECEIVE(2, "待收货"),
REFUNDING(3, "退款中"),

FINISH(10, "已完成"),
REFUNDED(11, "已退款"),
;

private Integer code;
private String desc;

public static OrderStateEnum getEnumByCode(Integer code) {
for (OrderStateEnum stateEnum : values()) {
if (stateEnum.getCode().equals(code)) {
return stateEnum;
}
}
throw new RuntimeException("code非法");
}
}

业务处理service类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
java复制代码import java.util.Objects;

@Service
public class OrderService {

@Autowired
private OrderRepository orderRepository;

//模拟获取订单号,这里简单做,一般都是全局唯一的分布式id
private static Long orderSn = 1L;
public static String generateOrderSn() {
return String.valueOf(orderSn++);
}

/**
* 创建订单
* @param buyerId
* @param skuId
* @return
*/
public String create(Long buyerId, Long skuId) {
Order order = new Order();
order.setOrderSn(generateOrderSn());
order.setBuyerId(buyerId);
order.setSkuId(skuId);
order.setStatus(OrderStateEnum.WAIT_PAY.getCode());
orderRepository.insert(order);
return order.getOrderSn();
}

/**
* 发起支付
* 订单发货
* 订单收货
* 与订单退款写法类似,暂时忽略...
*/

/**
* 售后申请
* @param orderSn
*/
void refund(String orderSn) {
Order order = orderRepository.get(orderSn);
//判断是否是待收货状态
if (!Objects.equals(order.getStatus(), OrderStateEnum.WAIT_DELIVER.getCode())) {
throw new RuntimeException("该状态下不支持该操作");
}
OrderStateEnum newState = OrderStateEnum.REFUNDING;
//操作收货,并更新数据库
order.setStatus(newState.getCode());
orderRepository.update(order);
}
}

迭代代码

随着业务的迭代,不止待发货状态可以申请售后了,待收货状态也可以申请售后,即状态机改为:

image.png

那refund方法中相应的状态判断也要做出改变,甚至处理流程很可能是不一样的,这里简单考虑假设一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
java复制代码import java.util.Objects;

@Service
public class OrderService {

@Autowired
private OrderRepository orderRepository;

//模拟获取订单号,这里简单做,一般都是全局唯一的分布式id
private static Long orderSn = 1L;
public static String generateOrderSn() {
return String.valueOf(orderSn++);
}

/**
* 创建订单
* @param buyerId
* @param skuId
* @return
*/
public String create(Long buyerId, Long skuId) {
Order order = new Order();
order.setOrderSn(generateOrderSn());
order.setBuyerId(buyerId);
order.setSkuId(skuId);
order.setStatus(OrderStateEnum.WAIT_PAY.getCode());
orderRepository.insert(order);
return order.getOrderSn();
}

/**
* 发起支付
* 订单发货
* 订单收货
* 与订单退款写法类似,暂时忽略...
*/

/**
* 售后申请
* @param orderSn
*/
void refund(String orderSn) {
Order order = orderRepository.get(orderSn);
//判断是否是待收货状态
if (!Objects.equals(order.getStatus(), OrderStateEnum.WAIT_DELIVER.getCode())
&& !Objects.equals(order.getStatus(), OrderStateEnum.WAIT_RECEIVE.getCode())) {
throw new RuntimeException("该状态下不支持该操作");
}
OrderStateEnum newState = OrderStateEnum.REFUNDING;
//操作收货,并更新数据库
order.setStatus(newState.getCode());
orderRepository.update(order);
}
}

可以看出,这里我们违背了开闭原则,直接改了之前已经严格测试的代码,导致后续必须进行回归测试。虽然目前例子中的情况看上去还不算太糟,但是随着业务的迭代,状态机只会越来越复杂,如果每次增加新的逻辑都得改动原来的代码,上线风险性会很高,而且代码可读性也会越来越差(if-else不宜太多)。为此我们尝试用状态模式来优化该订单业务的代码逻辑编写。

定义

状态(State)模式的定义:对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。我们通过看每一个状态对象的实现,可以清晰了解该状态下可以执行的操作已经流转到下一个状态的集合。

模式的结构

状态模式包含以下主要角色。

  • 环境类(Context)角色:也称为上下文,它定义了客户端需要的接口,内部维护一个当前状态,并负责具体状态的切换。
  • 抽象状态(State)角色:定义一个接口,用以封装环境对象中的特定状态所对应的行为,可以有一个或多个行为。
  • 具体状态(Concrete State)角色:实现抽象状态所对应的行为,并且在需要的情况下进行状态切换。

UML图

状态模式的结构图

模式基本实现

上下文类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public class Context {
private State state;

public Context(State state) {
this.state = state;
}

public State getState() {
return state;
}

public void setState(State state) {
this.state = state;
}

public void handle() {
state.handle(this);
}
}

抽象状态类

1
2
3
4
java复制代码public abstract class State {

public abstract void handle(Context context);
}

具体状态类A

1
2
3
4
5
6
7
java复制代码public class AState extends State{

public void handle(Context context) {
System.out.println(this.getClass().getSimpleName() + "将流转到BState");
context.setState(new BState());
}
}

具体状态类B

1
2
3
4
5
6
7
java复制代码public class BState extends State{

public void handle(Context context) {
System.out.println(this.getClass().getSimpleName() + "将流转到AState");
context.setState(new AState());
}
}

测试Client类

1
2
3
4
5
6
7
8
java复制代码public class ClientTest {

public static void main(String[] args) {
Context context = new Context(new AState());
context.handle();
context.handle();
}
}

执行的结果展示,第一次执行handle是由AState执行的,执行完后状态流转到B,第二次执行handle是由BState执行的,执行完后状态流转到A

1
2
复制代码AState将流转到BState
BState将流转到AState

上面的基本代码中有一个问题,那就是AState和BState每次切换状态的时候都new了一个新的实例,其实没有必要,这里可以把实例保存下来,例如保存Context中,整个程序执行的生命周期中所有线程共享这些状态实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码public class ShareContext {

private static Map<String, State> shareStateMap = new HashMap<>();

private State state;

static {
shareStateMap.put(AState.class.getSimpleName(), new AState());
shareStateMap.put(BState.class.getSimpleName(), new BState());
}

public ShareContext() {}

public State getState() {
return state;
}

public void setState(State state) {
this.state = state;
}

//读取状态
public static State getState(String key) {
return shareStateMap.get(key);
}

public void handle() {
state.handle(this);
}
}

具体状态A的实现变成,

1
2
3
4
5
6
java复制代码public class AState extends State{
public void handle(ShareContext context) {
System.out.println(this.getClass().getSimpleName() + "将流转到BState");
context.setState(ShareContext.getState("AState"));
}
}

具体状态B和测试Client类同理,new一个State的操作都改为从map中获取。

优化订单状态流转

模式基本代码

以上面的模板代码为例可轻易写出优化订单状态流转的基本代码,但是现在一般我们都是在SpringBoot的框架下编写代码了,所以这里给出状态模式在SpringBoot下的代码实现。

模式与SpringBoot结合代码

完整代码见:…

定义订单状态枚举

与之前一致

定义抽象状态类

一开始默认所有的方法都是不可操作的,每个状态下实现自己可以操作的方法,这样状态流转也一目了然

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
java复制代码public abstract class AbstractOrderState {

public abstract Enum type();

/**
* 发起支付
*
* @param context
* @param order
*/
public void pay(OrderStateContext context, Order order) {
throw new RuntimeException("该状态下不支持该操作");
}

/**
* 订单发货
*
* @param context
* @param order
*/
public void deliver(OrderStateContext context, Order order) {
throw new RuntimeException("该状态下不支持该操作");
}

/**
* 订单收货
*
* @param context
* @param order
*/
public void receive(OrderStateContext context, Order order) {
throw new RuntimeException("该状态下不支持该操作");
}

/**
* 售后申请
*
* @param context
* @param order
*/
public void applyRefund(OrderStateContext context, Order order) {
throw new RuntimeException("该状态下不支持该操作");
}

/**
* 退款完成
*
* @param context
* @param order
*/
public void finishRefund(OrderStateContext context, Order order) {
throw new RuntimeException("该状态下不支持该操作");
}
}

定义具体状态类

以待发货状态为例,按状态机描述,需要实现发货方法和申请售后方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
java复制代码@Component
public class WaitDeliverOrderState extends AbstractOrderState {

@Autowired
private OrderRepository orderRepository;

@Override
public Enum type() {
return OrderStateEnum.WAIT_DELIVER;
}

/**
* 发货
* @param context
* @param order
*/
public void deliver(OrderStateContext context, Order order) {
OrderStateEnum newState = OrderStateEnum.WAIT_RECEIVE;
//操作发货,并更新数据库
order.setStatus(newState.getCode());
orderRepository.update(order);
//更新上下文状态
context.setOrderState(OrderStateFactory.getState(newState));
System.out.println("订单号:"+ order.getOrderSn() + " 发货成功!状态流转至:" + newState.getDesc());
}

/**
* 申请售后
* @param context
* @param order
*/
public void applyRefund(OrderStateContext context, Order order) {
OrderStateEnum newState = OrderStateEnum.REFUNDING;
//操作发货,并更新数据库
order.setStatus(newState.getCode());
orderRepository.update(order);
//更新上下文状态
context.setOrderState(OrderStateFactory.getState(newState));
System.out.println("订单号:"+ order.getOrderSn() + " 申请售后!状态流转至:" + newState.getDesc());
}
}

定义上下文类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
java复制代码public class OrderStateContext {

private AbstractOrderState orderState;

public OrderStateContext() {}

public AbstractOrderState getOrderState() {
return orderState;
}

public void setOrderState(AbstractOrderState orderState) {
this.orderState = orderState;
}

/**
* 发起支付
*
* @param order
*/
void pay(Order order) {
orderState.pay(this, order);
}

/**
* 订单发货
*
* @param order
*/
void deliver(Order order) {
orderState.deliver(this, order);
}

/**
* 订单收货
*
* @param order
*/
void receive(Order order) {
orderState.receive(this, order);
}

/**
* 申请售后
*
* @param order
*/
void applyRefund(Order order) {
orderState.applyRefund(this, order);
}

/**
* 退款完成
*
* @param order
*/
void finishRefund(Order order) {
orderState.finishRefund(this, order);
}
}

封装状态实例工厂

封装状态实例工厂,可达到共享具体状态实例的效果,避免浪费内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Component
public class OrderStateFactory implements ApplicationContextAware {

private static final Map<Enum, AbstractOrderState> stateMap = new HashMap<>(OrderStateEnum.values().length);

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map<String, AbstractOrderState> beans = applicationContext.getBeansOfType(AbstractOrderState.class);
beans.values().forEach(item -> stateMap.put(item.type(), item));
}

public static AbstractOrderState getState(Enum orderStateEnum) {
return stateMap.get(orderStateEnum);
}
}

代码测试

这里模拟写一个OrderService,同样对应了不同操作,只是这里做了简化,一般情况下如支付操作前需要校验参数,支付后可能会需要发消息通知下游等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
java复制代码@Service
public class OrderService {

@Autowired
private OrderRepository orderRepository;

/**
* 创建订单,和之前一样
*/
public String create(Long buyerId, Long skuId) {
//...
}

/**
* 发起支付
*
* @param orderSn
*/
public void pay(String orderSn) {
Order order = orderRepository.get(orderSn);
OrderStateContext context = new OrderStateContext();
AbstractOrderState currentState = OrderStateFactory.getState(OrderStateEnum.getEnumByCode(order.getStatus()));
context.setOrderState(currentState);
context.pay(order);
}

/**
* 订单发货
*
* @param orderSn
*/
public void deliver(String orderSn) {
Order order = orderRepository.get(orderSn);
OrderStateContext context = new OrderStateContext();
AbstractOrderState currentState = OrderStateFactory.getState(OrderStateEnum.getEnumByCode(order.getStatus()));
context.setOrderState(currentState);
context.deliver(order);
}

/**
* 订单收货
* 申请售后
* 退款完成
* 与上述代码基本一致,完整代码见github
*/
}

Client测试,模拟外部按钮操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class })
public class StateClientTest {

public static void main(String[] args) {
SpringApplication.run(StateClientTest.class, args);
OrderService orderService = SpringContextUtil.getBean(OrderService.class);

//1.创建一笔订单
String orderSn = orderService.create(1L, 1L);

//2.执行支付操作
orderService.pay(orderSn);

//3.执行发货操作
orderService.deliver(orderSn);

//4.执行收货操作
orderService.receive(orderSn);

//5.尝试执行下支付操作,会失败
orderService.pay(orderSn);
}
}

执行结果

image.png

优缺点

优点

  1. 每一个具体状态中能直观看出当前状态下能执行的操作以及会流向的状态
  2. 通过定义新的子类很容易地增加新的状态和转换,较好的适应了开闭原则

缺点

  1. 当状态过多时可能系统中的类会变得很多
  2. 当状态过多时操作也会变多,导致抽象状态类和上下文context中的方法定义可能会变得很多,这里其实可以做下分层,操作分为正向和逆向,把取消和售后的操作都定义到逆向中

总结

当状态确定比较少并且后续也不会扩展时,其实一般不一定需要使用状态模式来过度设计,少许的if-else看起来也很清晰。
另外,状态模式对于状态流转之后,会直接进行一部分操作,比如上述代码中会更新数据库,有时候其实不太好,因为一般一个订单操作中对于数据库的更新肯定不止一个表,为了保证事务,是否都放在具体状态的方法中去更新有时候就很头疼。还有一种状态管理的实现方式是不直接做操作,只有一个getNextState方法, 传入当前状态和动作,返回流转到的下一个状态,也就是有限状态机,关于数据库以及其他的操作都放在外部实现,后续会单独写一篇文章介绍。


参考

状态模式(详解版)

解构电商产品——订单系统(一)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%