本文最后更新于:2021年2月9日 下午
本文以运营活动状态转换为例,结合 Spring 演示状态模式的实践应用。
更新
2021 年 02 月 09 日
评论中匿名评论提到可能出现并发问题,审视代码发现 ActivityState 类持有了全局变量 ActivityContext,在多线程情况下,会造成数据共享导致线程安全问题,解决办法很简单,使用 ThreadLocal 对象包装 ActivityContext 即可,需要注意用完需要进行释放,暂时没想到其他更好的方法解决。
感谢评论中哥们提出的问题,有空加 QQ 群 967808880 一块交流交流。
类型:行为型模式
意图:允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。
主要解决:一个对象存在多个状态,每个状态行为不同,状态可以相互转换。
使用场景:1、行为随状态改变而改变的场景。 2、减少 switch..case 以及 if…else
设计模式系列文章目录
角色
State:抽象状态角色,负责对象状态定义,并且封装环境角色以实现状态切换。
Context:环境角色,定义客户端需要的接口,并且负责具体状态的切换。
ConcreteState:具体状态角色,当前状态要做的事情,以及当前状态如何转换其他状态。
UML
实战
本文以运营活动状态为例,结合 Spring 演示状态模式的实践应用。
运营活动创建初始状态为草稿状态,编辑好活动之后,运营会后台启用活动,此时活动状态为已启用;
当达到活动开始时间时,定时任务会将活动状态置为进行中;
当达到活动结束时间时,定时任务会将活动状态置为已结束。
进行中的活动也可能会因为某些原因需要手动停用,此时活动状态置为已停用。
状态之间有着严格的前置校验,比如草稿状态可以继续保存为草稿,也可以进行启动,但不能直接切换为进行中,可以直接编辑切换回草稿箱状态;比如已停用的状态只有在启用之后才能被置为进行中。
活动状态的切换约束如下图:
新状态→ 当前状态↓ |
草稿箱 |
已启用 |
进行中 |
已停用 |
已结束 |
草稿箱 |
✅ |
✅ |
❌ |
❌ |
❌ |
已启用 |
✅ |
❌ |
✅ |
✅ |
❌ |
进行中 |
❌ |
❌ |
❌ |
✅ |
✅ |
已停用 |
❌ |
✅ |
❌ |
❌ |
❌ |
已结束 |
❌ |
❌ |
❌ |
❌ |
❌ |
如果不采取状态模式,可能写出的代码就是不断使用 if 判断前置状态是否符合规则,当增加了新的状态,需要改动判断的地方,从而可能引入了 Bug。
本文示例 UML 图
示例代码
定义抽象状态角色
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
| public abstract class ActivityState { protected ThreadLocal<ActivityContext> activityContext = new ThreadLocal<>();
public void setActivityContext(ActivityContext activityContext) { this.activityContext.set(activityContext); }
public abstract Integer type();
protected boolean isSameStatus(Activity activity) { return type().equals(activity.getStatus()); }
public abstract boolean saveDraft(Activity activity);
public abstract boolean enable(Activity activity);
public abstract boolean start(Activity activity);
public abstract boolean disable(Activity activity);
public abstract boolean finish(Activity activity); public void clear() { activityContext.remove(); } }
|
定义环境角色
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
| public class ActivityContext { private ActivityState activityState;
public void setActivityState(ActivityState activityState) { this.activityState = activityState; this.activityState.setActivityContext(this); }
public boolean saveDraft(Activity activity) { return this.activityState.saveDraft(activity); }
public boolean enable(Activity activity) { return this.activityState.enable(activity); }
public boolean start(Activity activity) { return this.activityState.start(activity); }
public boolean disable(Activity activity) { return this.activityState.disable(activity); }
public boolean finish(Activity activity) { return this.activityState.finish(activity); }
}
|
定义具体状态角色
因为本文示例具体状态角色有很多,因此只列举一个开启状态角色举例参考,更多代码可以参考本文对应的 GitHub 示例代码
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
| @Component public class ActivityEnableState extends ActivityState {
@Resource private ActivityDraftState activityDraftState; @Resource private ActivityStartState activityStartState; @Resource private ActivityDisableState activityDisableState;
@Override public Integer type() { return ActivityStateEnum.ENABLE.getCode(); }
@Override public boolean saveDraft(Activity activity) { ActivityContext activityContext = this.activityContext.get(); activityContext.setActivityState(activityDraftState); return activityContext.saveDraft(activity); }
@Override public boolean enable(Activity activity) { if (isSameStatus(activity)) { return false; } activity.setStatus(type()); return true; }
@Override public boolean start(Activity activity) { ActivityContext activityContext = this.activityContext.get(); activityContext.setActivityState(activityDraftState); return activityContext.start(activity); }
@Override public boolean disable(Activity activity) { ActivityContext activityContext = this.activityContext.get(); activityContext.setActivityState(activityDraftState); return activityContext.disable(activity); }
@Override public boolean finish(Activity activity) { return false; } }
|
封装具体状态实例工厂
状态角色应该是单例的,结合 Spring 与工厂模式对实例进行封装,方便根据数据库的 status 值获取对应的状态角色实例。
| @Component public class ActivityStateFactory implements ApplicationContextAware { public static final Map<Integer, ActivityState> STATE_MAP = new HashMap<>(ActivityStateEnum.values().length);
@Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { Map<String, ActivityState> beans = applicationContext.getBeansOfType(ActivityState.class); beans.values().forEach(item -> STATE_MAP.put(item.type(), item)); } }
|
测试
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
| @RunWith(SpringRunner.class) @SpringBootTest(classes = App.class) public class BaseTest {
@Test public void test1() { Activity activity = new Activity() .setId(1L) .setName("测试活动") .setStatus(ActivityStateEnum.DRAFT.getCode()) .setCreateTime(LocalDateTime.now());
ActivityState activityState = ActivityStateFactory.STATE_MAP.get(activity.getStatus()); ActivityContext context = new ActivityContext(); context.setActivityState(activityState);
System.out.println("保存草稿: " + (context.saveDraft(activity) ? "成功" : "失败")); System.out.println("更新活动状态为已启用: " + (context.enable(activity) ? "成功" : "失败")); System.out.println("更新活动状态为进行中: " + (context.start(activity) ? "成功" : "失败")); System.out.println("更新活动状态为已停用: " + (context.disable(activity) ? "成功" : "失败")); System.out.println("更新活动状态为已启用: " + (context.enable(activity) ? "成功" : "失败")); System.out.println("更新活动状态为进行中: " + (context.start(activity) ? "成功" : "失败")); System.out.println("更新活动状态为已结束: " + (context.finish(activity) ? "成功" : "失败")); System.out.println("更新活动状态为进行中: " + (context.start(activity) ? "成功" : "失败")); activityState.clear(); } }
|
结果输出:
| 保存草稿: 成功 更新活动状态为已启用: 成功 更新活动状态为进行中: 成功 更新活动状态为已停用: 成功 更新活动状态为已启用: 成功 更新活动状态为进行中: 成功 更新活动状态为已结束: 成功 更新活动状态为进行中: 失败
|
可以看到状态切换路径:草稿-> 草稿-> 已启用-> 进行中-> 已停用-> 已启用-> 进行中-> 已结束-> 进行中,前面都是正确切换,但是已结束无法切换为进行中状态,从而验证了状态模式的应用。
总结
看上一篇策略模式的文章中的 UML,和本文的 UML 是相同的。那么他们的区别是什么呢?
策略模式是提供了可相互替换的算法,根据客户端选择一种算法指定一种行为;
状态模式则包含了对象状态,根据对象状态不同,行为也不一样,即状态决定行为,将行为对应的逻辑封装到具体状态类中,在环境类中消除逻辑判断,且具体实现不可相互替换。
状态模式中,客户端角色与状态对象不需要进行交互,所有的交互都委托给环境角色进行。
源码下载
参考