专栏
- 设计模式在电商业务下的实践——策略模式
- 设计模式在电商业务下的实践——状态模式
- 设计模式在电商业务下的实践——责任链模式
- 设计模式在电商业务下的实践——模版方法模式
- 设计模式在电商业务下的实践——适配器模式
- ……
持续更新中。
背景
以订单业务为例,存在多种业务操作,订单创建,订单支付,发货,收货等等。而这些操作对应了不同的订单状态,只有在指定的订单状态才能进行指定的订单业务,例如如下订单状态机:
假如一开始只能在待发货状态下买家才能申请售后退款,得出如下代码
初始代码
订单状态枚举
1 | java复制代码@Getter |
业务处理service类
1 | java复制代码import java.util.Objects; |
迭代代码
随着业务的迭代,不止待发货状态可以申请售后了,待收货状态也可以申请售后,即状态机改为:
那refund方法中相应的状态判断也要做出改变,甚至处理流程很可能是不一样的,这里简单考虑假设一样。
1 | java复制代码import java.util.Objects; |
可以看出,这里我们违背了开闭原则,直接改了之前已经严格测试的代码,导致后续必须进行回归测试。虽然目前例子中的情况看上去还不算太糟,但是随着业务的迭代,状态机只会越来越复杂,如果每次增加新的逻辑都得改动原来的代码,上线风险性会很高,而且代码可读性也会越来越差(if-else不宜太多)。为此我们尝试用状态模式来优化该订单业务的代码逻辑编写。
定义
状态(State)模式的定义:对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。我们通过看每一个状态对象的实现,可以清晰了解该状态下可以执行的操作已经流转到下一个状态的集合。
模式的结构
状态模式包含以下主要角色。
- 环境类(Context)角色:也称为上下文,它定义了客户端需要的接口,内部维护一个当前状态,并负责具体状态的切换。
- 抽象状态(State)角色:定义一个接口,用以封装环境对象中的特定状态所对应的行为,可以有一个或多个行为。
- 具体状态(Concrete State)角色:实现抽象状态所对应的行为,并且在需要的情况下进行状态切换。
UML图
模式基本实现
上下文类
1 | java复制代码public class Context { |
抽象状态类
1 | java复制代码public abstract class State { |
具体状态类A
1 | java复制代码public class AState extends State{ |
具体状态类B
1 | java复制代码public class BState extends State{ |
测试Client类
1 | java复制代码public class ClientTest { |
执行的结果展示,第一次执行handle是由AState执行的,执行完后状态流转到B,第二次执行handle是由BState执行的,执行完后状态流转到A
1 | 复制代码AState将流转到BState |
上面的基本代码中有一个问题,那就是AState和BState每次切换状态的时候都new了一个新的实例,其实没有必要,这里可以把实例保存下来,例如保存Context中,整个程序执行的生命周期中所有线程共享这些状态实例
1 | java复制代码public class ShareContext { |
具体状态A的实现变成,
1 | java复制代码public class AState extends State{ |
具体状态B和测试Client类同理,new一个State的操作都改为从map中获取。
优化订单状态流转
模式基本代码
以上面的模板代码为例可轻易写出优化订单状态流转的基本代码,但是现在一般我们都是在SpringBoot的框架下编写代码了,所以这里给出状态模式在SpringBoot下的代码实现。
模式与SpringBoot结合代码
完整代码见:…
定义订单状态枚举
与之前一致
定义抽象状态类
一开始默认所有的方法都是不可操作的,每个状态下实现自己可以操作的方法,这样状态流转也一目了然
1 | java复制代码public abstract class AbstractOrderState { |
定义具体状态类
以待发货状态为例,按状态机描述,需要实现发货方法和申请售后方法
1 | java复制代码@Component |
定义上下文类
1 | java复制代码public class OrderStateContext { |
封装状态实例工厂
封装状态实例工厂,可达到共享具体状态实例的效果,避免浪费内存
1 | java复制代码@Component |
代码测试
这里模拟写一个OrderService,同样对应了不同操作,只是这里做了简化,一般情况下如支付操作前需要校验参数,支付后可能会需要发消息通知下游等等。
1 | java复制代码@Service |
Client测试,模拟外部按钮操作
1 | java复制代码@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class }) |
执行结果
优缺点
优点
- 每一个具体状态中能直观看出当前状态下能执行的操作以及会流向的状态
- 通过定义新的子类很容易地增加新的状态和转换,较好的适应了开闭原则
缺点
- 当状态过多时可能系统中的类会变得很多
- 当状态过多时操作也会变多,导致抽象状态类和上下文context中的方法定义可能会变得很多,这里其实可以做下分层,操作分为正向和逆向,把取消和售后的操作都定义到逆向中
总结
当状态确定比较少并且后续也不会扩展时,其实一般不一定需要使用状态模式来过度设计,少许的if-else看起来也很清晰。
另外,状态模式对于状态流转之后,会直接进行一部分操作,比如上述代码中会更新数据库,有时候其实不太好,因为一般一个订单操作中对于数据库的更新肯定不止一个表,为了保证事务,是否都放在具体状态的方法中去更新有时候就很头疼。还有一种状态管理的实现方式是不直接做操作,只有一个getNextState方法, 传入当前状态和动作,返回流转到的下一个状态,也就是有限状态机,关于数据库以及其他的操作都放在外部实现,后续会单独写一篇文章介绍。
参考
本文转载自: 掘金