重学 Java 设计模式:实战状态模式「模拟系统营销活动,状态流程审核发布上线场景」
一、前言
写好代码三个关键点
如果把写代码想象成家里的软装,你肯定会想到家里需要有一个非常不错格局最好是南北通透的,买回来的家具最好是品牌保证质量的,之后呢是大小合适,不能摆放完了看着别扭。那么把这一过程抽象成写代码就是需要三个核心的关键点;架构
(房间的格局)、命名
(品牌和质量)、注释
(尺寸大小说明书),只有这三个点都做好才能完成出一套赏心悦目的家。
平原走码🐎易放难收
上学期间你写了多少代码?上班一年你能写多少代码?回家自己学习写了多少代码?个人素养的技术栈地基都是一块一块砖码出来的,写的越广越深,根基就越牢固。当根基牢固了以后在再上层建设就变得迎刃而解了,也更容易建设了。往往最难的就是一层一层阶段的突破,突破就像破壳一样,也像夯实地基,短时间看不到成绩,也看不出高度。但以后谁能走的稳,就靠着默默的沉淀。
技术传承的重要性
可能是现在时间节奏太快,一个需求下来恨不得当天就上线(这个需求很简单,怎么实现我不管,明天上线!
),导致团队的人都很慌
、很急
、很累
、很崩溃
,最终反反复复的人员更替,项目在这个过程中也交接了N次,文档不全、代码混乱、错综复杂,谁在后面接手也都只能修修补补,就像烂尾楼。这个没有传承、没有沉淀的项目,很难跟随业务的发展。最终!根基不牢,一地鸡毛。
二、开发环境
- JDK 1.8
- Idea + Maven
- 涉及工程三个,可以通过关注公众号:
bugstack虫洞栈
,回复源码下载
获取(打开获取的链接,找到序号18)
工程 | 描述 |
---|---|
itstack-demo-design-19-00 | 场景模拟工程;模拟营销活动操作服务(查询、审核) |
itstack-demo-design-19-01 | 使用一坨代码实现业务需求 |
itstack-demo-design-19-02 | 通过设计模式优化改造代码,产生对比性从而学习 |
三、状态模式介绍
- 图片来自:https://refactoringguru.cn/design-patterns/state
状态模式描述的是一个行为下的多种状态变更,比如我们最常见的一个网站的页面,在你登录与不登录下展示的内容是略有差异的(不登录不能展示个人信息
),而这种登录
与不登录
就是我们通过改变状态,而让整个行为发生了变化。
至少80后、90后的小伙伴基本都用过这种磁带放音机(可能没有这个好看
),它的上面是一排按钮,当放入磁带后,通过上面的按钮就可以让放音机播放磁带上的内容(listen to 英语听力考试
),而且有些按钮是互斥的,当在某个状态下才可以按另外的按钮(这在设计模式里也是一个关键的点
)。
四、案例场景模拟
在本案例中我们模拟营销活动审核状态流转场景(一个活动的上线是多个层级审核上线的)
在上图中也可以看到我们的流程节点中包括了各个状态到下一个状态扭转的关联条件,比如;审核通过才能到活动中,而不能从编辑中直接到活动中,而这些状态的转变就是我们要完成的场景处理。
大部分程序员基本都开发过类似的业务场景,需要对活动或者一些配置需要审核后才能对外发布,而这个审核的过程往往会随着系统的重要程度而设立多级控制,来保证一个活动可以安全上线,避免造成资损。
当然有时候会用到一些审批流的过程配置,也是非常方便开发类似的流程的,也可以在配置中设定某个节点的审批人员。但这不是我们主要体现的点,在本案例中我们主要是模拟学习对一个活动的多个状态节点的审核控制。
1. 场景模拟工程
itstack-demo-design-19-00
└── src└── main└── java└── org.itstack.demo.design├── ActivityInfo.java├── Status.java└── ActivityService.java
- 在这个模拟工程里我们提供了三个类,包括;状态枚举(
Status
)、活动对象(ActivityInfo
)、活动服务(ActivityService
),三个服务类。 - 接下来我们就分别介绍三个类包括的内容。
2. 代码实现
2.1 基本活动信息
public class ActivityInfo {private String activityId; // 活动IDprivate String activityName; // 活动名称private Enum<Status> status; // 活动状态private Date beginTime; // 开始时间private Date endTime; // 结束时间// ...get/set
}
- 一些基本的活动信息;活动ID、活动名称、活动状态、开始时间、结束时间。
2.2 活动枚举状态
public enum Status {// 1创建编辑、2待审核、3审核通过(任务扫描成活动中)、4审核拒绝(可以撤审到编辑状态)、5活动中、6活动关闭、7活动开启(任务扫描成活动中)Editing, Check, Pass, Refuse, Doing, Close, Open}
- 活动的枚举;1创建编辑、2待审核、3审核通过(任务扫描成活动中)、4审核拒绝(可以撤审到编辑状态)、5活动中、6活动关闭、7活动开启(任务扫描成活动中)
2.3 活动服务接口
public class ActivityService {private static Map<String, Enum<Status>> statusMap = new ConcurrentHashMap<String, Enum<Status>>();public static void init(String activityId, Enum<Status> status) {// 模拟查询活动信息ActivityInfo activityInfo = new ActivityInfo();activityInfo.setActivityId(activityId);activityInfo.setActivityName("早起学习打卡领奖活动");activityInfo.setStatus(status);activityInfo.setBeginTime(new Date());activityInfo.setEndTime(new Date());statusMap.put(activityId, status);}/*** 查询活动信息** @param activityId 活动ID* @return 查询结果*/public static ActivityInfo queryActivityInfo(String activityId) {// 模拟查询活动信息ActivityInfo activityInfo = new ActivityInfo();activityInfo.setActivityId(activityId);activityInfo.setActivityName("早起学习打卡领奖活动");activityInfo.setStatus(statusMap.get(activityId));activityInfo.setBeginTime(new Date());activityInfo.setEndTime(new Date());return activityInfo;}/*** 查询活动状态** @param activityId 活动ID* @return 查询结果*/public static Enum<Status> queryActivityStatus(String activityId) {return statusMap.get(activityId);}/*** 执行状态变更** @param activityId 活动ID* @param beforeStatus 变更前状态* @param afterStatus 变更后状态 b*/public static synchronized void execStatus(String activityId, Enum<Status> beforeStatus, Enum<Status> afterStatus) {if (!beforeStatus.equals(statusMap.get(activityId))) return;statusMap.put(activityId, afterStatus);}}
- 在这个静态类中提供了活动的查询和状态变更接口;
queryActivityInfo
、queryActivityStatus
、execStatus
。 - 同时使用Map的结构来记录活动ID和状态变化信息,另外还有init方法来初始化活动数据。实际的开发中这类信息基本都是从
数据库
或者Redis
中获取。
五、用一坨坨代码实现
这里我们先使用最粗暴的方式来实现功能
对于这样各种状态的变更,最让我们直接想到的就是使用if
和else
进行判断处理。每一个状态可以流转到下一个什么状态,都可以使用嵌套的if
实现。
1. 工程结构
itstack-demo-design-19-01
└── src└── main└── java└── org.itstack.demo.design├── ActivityExecStatusController.java└── Result.java
- 整个实现的工程结构比较简单,只包括了两个类;
ActivityExecStatusController
、Result
,一个是处理流程状态,另外一个是返回的对象。
2. 代码实现
public class ActivityExecStatusController {/*** 活动状态变更* 1. 编辑中 -> 提审、关闭* 2. 审核通过 -> 拒绝、关闭、活动中* 3. 审核拒绝 -> 撤审、关闭* 4. 活动中 -> 关闭* 5. 活动关闭 -> 开启* 6. 活动开启 -> 关闭** @param activityId 活动ID* @param beforeStatus 变更前状态* @param afterStatus 变更后状态* @return 返回结果*/public Result execStatus(String activityId, Enum<Status> beforeStatus, Enum<Status> afterStatus) {// 1. 编辑中 -> 提审、关闭if (Status.Editing.equals(beforeStatus)) {if (Status.Check.equals(afterStatus) || Status.Close.equals(afterStatus)) {ActivityService.execStatus(activityId, beforeStatus, afterStatus);return new Result("0000", "变更状态成功");} else {return new Result("0001", "变更状态拒绝");}}// 2. 审核通过 -> 拒绝、关闭、活动中if (Status.Pass.equals(beforeStatus)) {if (Status.Refuse.equals(afterStatus) || Status.Doing.equals(afterStatus) || Status.Close.equals(afterStatus)) {ActivityService.execStatus(activityId, beforeStatus, afterStatus);return new Result("0000", "变更状态成功");} else {return new Result("0001", "变更状态拒绝");}}// 3. 审核拒绝 -> 撤审、关闭if (Status.Refuse.equals(beforeStatus)) {if (Status.Editing.equals(afterStatus) || Status.Close.equals(afterStatus)) {ActivityService.execStatus(activityId, beforeStatus, afterStatus);return new Result("0000", "变更状态成功");} else {return new Result("0001", "变更状态拒绝");}}// 4. 活动中 -> 关闭if (Status.Doing.equals(beforeStatus)) {if (Status.Close.equals(afterStatus)) {ActivityService.execStatus(activityId, beforeStatus, afterStatus);return new Result("0000", "变更状态成功");} else {return new Result("0001", "变更状态拒绝");}}// 5. 活动关闭 -> 开启if (Status.Close.equals(beforeStatus)) {if (Status.Open.equals(afterStatus)) {ActivityService.execStatus(activityId, beforeStatus, afterStatus);return new Result("0000", "变更状态成功");} else {return new Result("0001", "变更状态拒绝");}}// 6. 活动开启 -> 关闭if (Status.Open.equals(beforeStatus)) {if (Status.Close.equals(afterStatus)) {ActivityService.execStatus(activityId, beforeStatus, afterStatus);return new Result("0000", "变更状态成功");} else {return new Result("0001", "变更状态拒绝");}}return new Result("0001", "非可处理的活动状态变更");}}
- 这里我们只需要看一下代码实现的结构即可。从上到下是一整篇的
ifelse
,基本这也是大部分初级程序员的开发方式。 - 这样的面向过程式开发方式,对于不需要改动代码,也不需要二次迭代的,还是可以使用的(
但基本不可能不迭代
)。而且随着状态和需求变化,会越来越难以维护,后面的人也不好看懂并且很容易填充其他的流程进去。越来越乱就是从点滴开始的
3. 测试验证
3.1 编写测试类
@Test
public void test() {// 初始化数据String activityId = "100001";ActivityService.init(activityId, Status.Editing); ActivityExecStatusController activityExecStatusController = new ActivityExecStatusController();Result resultRefuse = activityExecStatusController.execStatus(activityId, Status.Editing, Status.Refuse); logger.info("测试结果(编辑中To审核拒绝):{}", JSON.toJSONString(resultRefuse)); Result resultCheck = activityExecStatusController.execStatus(activityId, Status.Editing, Status.Check);logger.info("测试结果(编辑中To提交审核):{}", JSON.toJSONString(resultCheck));
}
- 我们的测试代码包括了两个功能的验证,一个是从
编辑中
到审核拒绝
,另外一个是从编辑中到提交审核
。 - 因为从我们的场景流程中可以看到,编辑中的活动是不能直接到
审核拒绝
的,这中间还需要提审
。
3.2 测试结果
23:24:30.774 [main] INFO org.itstack.demo.design.test.ApiTest - 测试结果(编辑中To审核拒绝):{"code":"0001","info":"变更状态拒绝"}
23:24:30.778 [main] INFO org.itstack.demo.design.test.ApiTest - 测试结果(编辑中To提交审核):{"code":"0000","info":"变更状态成功"}Process finished with exit code 0
- 从测试结果和我们的状态流程的流转中可以看到,是符合测试结果预期的。除了不好维护外,这样的开发过程还是蛮快的,但不建议这么搞!
六、状态模式重构代码
接下来使用状态模式来进行代码优化,也算是一次很小的重构。
重构的重点往往是处理掉ifelse
,而想处理掉ifelse
基本离不开接口与抽象类,另外还需要重新改造代码结构。
1. 工程结构
itstack-demo-design-19-02
└── src└── main└── java└── org.itstack.demo.design├── event│ ├── CheckState.java│ └── CloseState.java│ └── DoingState.java│ └── EditingState.java│ └── OpenState.java│ └── PassState.java│ └── RefuseState.java├── Result.java├── State.java└── StateHandler.java
状态模式模型结构
- 以上是状态模式的整个工程结构模型,State是一个抽象类,定义了各种操作接口(
提审、审核、拒审等
)。 - 右侧的不同颜色状态与我们场景模拟中的颜色保持一致,是各种状态流程流转的实现操作。这里的实现有一个关键点就是每一种状态到下一个状态,都分配到各个实现方法中控制,也就不需要
if
语言进行判断了。 - 最后是
StateHandler
对状态流程的统一处理,里面提供Map
结构的各项服务接口调用,也就避免了使用if
判断各项状态转变的流程。
2. 代码实现
2.1 定义状态抽象类
public abstract class State {/*** 活动提审** @param activityId 活动ID* @param currentStatus 当前状态* @return 执行结果*/public abstract Result arraignment(String activityId, Enum<Status> currentStatus);/*** 审核通过** @param activityId 活动ID* @param currentStatus 当前状态* @return 执行结果*/public abstract Result checkPass(String activityId, Enum<Status> currentStatus);/*** 审核拒绝** @param activityId 活动ID* @param currentStatus 当前状态* @return 执行结果*/public abstract Result checkRefuse(String activityId, Enum<Status> currentStatus);/*** 撤审撤销** @param activityId 活动ID* @param currentStatus 当前状态* @return 执行结果*/public abstract Result checkRevoke(String activityId, Enum<Status> currentStatus);/*** 活动关闭** @param activityId 活动ID* @param currentStatus 当前状态* @return 执行结果*/public abstract Result close(String activityId, Enum<Status> currentStatus);/*** 活动开启** @param activityId 活动ID* @param currentStatus 当前状态* @return 执行结果*/public abstract Result open(String activityId, Enum<Status> currentStatus);/*** 活动执行** @param activityId 活动ID* @param currentStatus 当前状态* @return 执行结果*/public abstract Result doing(String activityId, Enum<Status> currentStatus);}
- 在整个接口中提供了各项状态流转服务的接口,例如;活动提审、审核通过、审核拒绝、撤审撤销等7个方法。
- 在这些方法中所有的入参都是一样的,activityId(
活动ID
)、currentStatus(当前状态
),只有他们的具体实现是不同的。
2.2 部分状态流转实现
编辑
public class EditingState extends State {public Result arraignment(String activityId, Enum<Status> currentStatus) {ActivityService.execStatus(activityId, currentStatus, Status.Check);return new Result("0000", "活动提审成功");}public Result checkPass(String activityId, Enum<Status> currentStatus) {return new Result("0001", "编辑中不可审核通过");}public Result checkRefuse(String activityId, Enum<Status> currentStatus) {return new Result("0001", "编辑中不可审核拒绝");}@Overridepublic Result checkRevoke(String activityId, Enum<Status> currentStatus) {return new Result("0001", "编辑中不可撤销审核");}public Result close(String activityId, Enum<Status> currentStatus) {ActivityService.execStatus(activityId, currentStatus, Status.Close);return new Result("0000", "活动关闭成功");}public Result open(String activityId, Enum<Status> currentStatus) {return new Result("0001", "非关闭活动不可开启");}public Result doing(String activityId, Enum<Status> currentStatus) {return new Result("0001", "编辑中活动不可执行活动中变更");}}
提审
public class CheckState extends State {public Result arraignment(String activityId, Enum<Status> currentStatus) {return new Result("0001", "待审核状态不可重复提审");}public Result checkPass(String activityId, Enum<Status> currentStatus) {ActivityService.execStatus(activityId, currentStatus, Status.Pass);return new Result("0000", "活动审核通过完成");}public Result checkRefuse(String activityId, Enum<Status> currentStatus) {ActivityService.execStatus(activityId, currentStatus, Status.Refuse);return new Result("0000", "活动审核拒绝完成");}@Overridepublic Result checkRevoke(String activityId, Enum<Status> currentStatus) {ActivityService.execStatus(activityId, currentStatus, Status.Editing);return new Result("0000", "活动审核撤销回到编辑中");}public Result close(String activityId, Enum<Status> currentStatus) {ActivityService.execStatus(activityId, currentStatus, Status.Close);return new Result("0000", "活动审核关闭完成");}public Result open(String activityId, Enum<Status> currentStatus) {return new Result("0001", "非关闭活动不可开启");}public Result doing(String activityId, Enum<Status> currentStatus) {return new Result("0001", "待审核活动不可执行活动中变更");}}
- 这里提供了两个具体实现类的内容,编辑状态和提审状态。
- 例如在这两个实现类中,
checkRefuse
这个方法对于不同的类中有不同的实现,也就是不同状态下能做的下一步流转操作已经可以在每一个方法中具体控制了。 - 其他5个类的操作是类似的具体就不在这里演示了,大部分都是重复代码。可以通过源码进行学习理解。
2.3 状态处理服务
public class StateHandler {private Map<Enum<Status>, State> stateMap = new ConcurrentHashMap<Enum<Status>, State>();public StateHandler() {stateMap.put(Status.Check, new CheckState()); // 待审核stateMap.put(Status.Close, new CloseState()); // 已关闭stateMap.put(Status.Doing, new DoingState()); // 活动中stateMap.put(Status.Editing, new EditingState()); // 编辑中stateMap.put(Status.Open, new OpenState()); // 已开启stateMap.put(Status.Pass, new PassState()); // 审核通过stateMap.put(Status.Refuse, new RefuseState()); // 审核拒绝}public Result arraignment(String activityId, Enum<Status> currentStatus) {return stateMap.get(currentStatus).arraignment(activityId, currentStatus);}public Result checkPass(String activityId, Enum<Status> currentStatus) {return stateMap.get(currentStatus).checkPass(activityId, currentStatus);}public Result checkRefuse(String activityId, Enum<Status> currentStatus) {return stateMap.get(currentStatus).checkRefuse(activityId, currentStatus);}public Result checkRevoke(String activityId, Enum<Status> currentStatus) {return stateMap.get(currentStatus).checkRevoke(activityId, currentStatus);}public Result close(String activityId, Enum<Status> currentStatus) {return stateMap.get(currentStatus).close(activityId, currentStatus);}public Result open(String activityId, Enum<Status> currentStatus) {return stateMap.get(currentStatus).open(activityId, currentStatus);}public Result doing(String activityId, Enum<Status> currentStatus) {return stateMap.get(currentStatus).doing(activityId, currentStatus);}}
- 这是对状态服务的统一控制中心,可以看到在构造函数中提供了所有状态和实现的具体关联,放到Map数据结构中。
- 同时提供了不同名称的接口操作类,让外部调用方可以更加容易的使用此项功能接口,而不需要像在
itstack-demo-design-19-01
例子中还得传两个状态来判断。
3. 测试验证
3.1 编写测试类(Editing2Arraignment)
@Test
public void test_Editing2Arraignment() {String activityId = "100001";ActivityService.init(activityId, Status.Editing);StateHandler stateHandler = new StateHandler();Result result = stateHandler.arraignment(activityId, Status.Editing);logger.info("测试结果(编辑中To提审活动):{}", JSON.toJSONString(result));logger.info("活动信息:{} 状态:{}", JSON.toJSONString(ActivityService.queryActivityInfo(activityId)), JSON.toJSONString(ActivityService.queryActivityInfo(activityId).getStatus()));
}
测试结果
23:59:20.883 [main] INFO org.itstack.demo.design.test.ApiTest - 测试结果(编辑中To提审活动):{"code":"0000","info":"活动提审成功"}
23:59:20.907 [main] INFO org.itstack.demo.design.test.ApiTest - 活动信息:{"activityId":"100001","activityName":"早起学习打卡领奖活动","beginTime":1593694760892,"endTime":1593694760892,"status":"Check"} 状态:"Check"Process finished with exit code 0
- 测试编辑中To提审活动,的状态流转。
3.2 编写测试类(Editing2Open)
@Test
public void test_Editing2Open() {String activityId = "100001";ActivityService.init(activityId, Status.Editing);StateHandler stateHandler = new StateHandler();Result result = stateHandler.open(activityId, Status.Editing);logger.info("测试结果(编辑中To开启活动):{}", JSON.toJSONString(result));logger.info("活动信息:{} 状态:{}", JSON.toJSONString(ActivityService.queryActivityInfo(activityId)), JSON.toJSONString(ActivityService.queryActivityInfo(activityId).getStatus()));
}
测试结果
23:59:36.904 [main] INFO org.itstack.demo.design.test.ApiTest - 测试结果(编辑中To开启活动):{"code":"0001","info":"非关闭活动不可开启"}
23:59:36.914 [main] INFO org.itstack.demo.design.test.ApiTest - 活动信息:{"activityId":"100001","activityName":"早起学习打卡领奖活动","beginTime":1593694776907,"endTime":1593694776907,"status":"Editing"} 状态:"Editing"Process finished with exit code 0
- 测试编辑中To开启活动,的状态流转。
3.3 编写测试类(Refuse2Doing)
@Test
public void test_Refuse2Doing() {String activityId = "100001";ActivityService.init(activityId, Status.Refuse);StateHandler stateHandler = new StateHandler();Result result = stateHandler.doing(activityId, Status.Refuse);logger.info("测试结果(拒绝To活动中):{}", JSON.toJSONString(result));logger.info("活动信息:{} 状态:{}", JSON.toJSONString(ActivityService.queryActivityInfo(activityId)), JSON.toJSONString(ActivityService.queryActivityInfo(activityId).getStatus()));
}
测试结果
23:59:46.339 [main] INFO org.itstack.demo.design.test.ApiTest - 测试结果(拒绝To活动中):{"code":"0001","info":"审核拒绝不可执行活动为进行中"}
23:59:46.352 [main] INFO org.itstack.demo.design.test.ApiTest - 活动信息:{"activityId":"100001","activityName":"早起学习打卡领奖活动","beginTime":1593694786342,"endTime":1593694786342,"status":"Refuse"} 状态:"Refuse"Process finished with exit code 0
- 测试拒绝To活动中,的状态流转。
3.4 编写测试类(Refuse2Revoke)
@Test
public void test_Refuse2Revoke() {String activityId = "100001";ActivityService.init(activityId, Status.Refuse);StateHandler stateHandler = new StateHandler();Result result = stateHandler.checkRevoke(activityId, Status.Refuse);logger.info("测试结果(拒绝To撤审):{}", JSON.toJSONString(result));logger.info("活动信息:{} 状态:{}", JSON.toJSONString(ActivityService.queryActivityInfo(activityId)), JSON.toJSONString(ActivityService.queryActivityInfo(activityId).getStatus()));
}
测试结果
23:59:50.197 [main] INFO org.itstack.demo.design.test.ApiTest - 测试结果(拒绝To撤审):{"code":"0000","info":"撤销审核完成"}
23:59:50.208 [main] INFO org.itstack.demo.design.test.ApiTest - 活动信息:{"activityId":"100001","activityName":"早起学习打卡领奖活动","beginTime":1593694810201,"endTime":1593694810201,"status":"Editing"} 状态:"Editing"Process finished with exit code 0
-
测试测试结果(拒绝To撤审),的状态流转。
-
综上以上四个测试类分别模拟了不同状态之间的
有效流转
和拒绝流转
,不同的状态服务处理不同的服务内容。
七、总结
- 从以上的两种方式对一个需求的实现中可以看到,在第二种使用设计模式处理后已经没有了
ifelse
,代码的结构也更加清晰易于扩展。这就是设计模式的好处,可以非常强大的改变原有代码的结构,让以后的扩展和维护都变得容易些。 - 在实现结构的编码方式上可以看到这不再是面向过程的编程,而是面向对象的结构。并且这样的设计模式满足了
单一职责
和开闭原则
,当你只有满足这样的结构下才会发现代码的扩展是容易的,也就是增加和修改功能不会影响整体的变化。 - 但如果状态和各项流转较多像本文的案例中,就会产生较多的实现类。因此可能也会让代码的实现上带来了时间成本,因为如果遇到这样的场景可以按需评估投入回报率。主要点在于看是否经常修改、是否可以做成组件化、抽离业务与非业务功能。