重学 Java 设计模式:实战状态模式「模拟系统营销活动,状态流程审核发布上线场景」

作者:小傅哥
博客:https://bugstack.cn - 原创系列专题文章
沉淀、分享、成长,让自己和他人都能有所收获!😄
一、前言
写好代码三个关键点
如果把写代码想象成家里的软装,你肯定会想到家里需要有一个非常不错格局最好是南北通透的,买回来的家具最好是品牌保证质量的,之后呢是大小合适,不能摆放完了看着别扭。那么把这一过程抽象成写代码就是需要三个核心的关键点;架构(房间的格局)、命名(品牌和质量)、注释(尺寸大小说明书),只有这三个点都做好才能完成出一套赏心悦目的家。
平原走码🐎易放难收
上学期间你写了多少代码?上班一年你能写多少代码?回家自己学习写了多少代码?个人素养的技术栈地基都是一块一块砖码出来的,写的越广越深,根基就越牢固。当根基牢固了以后在再上层建设就变得迎刃而解了,也更容易建设了。往往最难的就是一层一层阶段的突破,突破就像破壳一样,也像夯实地基,短时间看不到成绩,也看不出高度。但以后谁能走的稳,就靠着默默的沉淀。
技术传承的重要性
可能是现在时间节奏太快,一个需求下来恨不得当天就上线(这个需求很简单,怎么实现我不管,明天上线!),导致团队的人都很慌、很急、很累、很崩溃,最终反反复复的人员更替,项目在这个过程中也交接了N次,文档不全、代码混乱、错综复杂,谁在后面接手也都只能修修补补,就像烂尾楼。这个没有传承、没有沉淀的项目,很难跟随业务的发展。最终!根基不牢,一地鸡毛。
二、开发环境
- JDK 1.8 
- Idea + Maven 
- 涉及工程三个,可以通过关注公众号:bugstack虫洞栈,回复 - 源码下载获取(打开获取的链接,找到序号18)

三、状态模式介绍

状态模式描述的是一个行为下的多种状态变更,比如我们最常见的一个网站的页面,在你登录与不登录下展示的内容是略有差异的(不登录不能展示个人信息),而这种登录与不登录就是我们通过改变状态,而让整个行为发生了变化。

至少80后、90后的小伙伴基本都用过这种磁带放音机(可能没有这个好看),它的上面是一排按钮,当放入磁带后,通过上面的按钮就可以让放音机播放磁带上的内容(listen to 英语听力考试),而且有些按钮是互斥的,当在某个状态下才可以按另外的按钮(这在设计模式里也是一个关键的点)。
四、案例场景模拟

在本案例中我们模拟营销活动审核状态流转场景(一个活动的上线是多个层级审核上线的)
在上图中也可以看到我们的流程节点中包括了各个状态到下一个状态扭转的关联条件,比如;审核通过才能到活动中,而不能从编辑中直接到活动中,而这些状态的转变就是我们要完成的场景处理。
大部分程序员基本都开发过类似的业务场景,需要对活动或者一些配置需要审核后才能对外发布,而这个审核的过程往往会随着系统的重要程度而设立多级控制,来保证一个活动可以安全上线,避免造成资损。
当然有时候会用到一些审批流的过程配置,也是非常方便开发类似的流程的,也可以在配置中设定某个节点的审批人员。但这不是我们主要体现的点,在本案例中我们主要是模拟学习对一个活动的多个状态节点的审核控制。
1. 场景模拟工程
- 在这个模拟工程里我们提供了三个类,包括;状态枚举( - Status)、活动对象(- ActivityInfo)、活动服务(- ActivityService),三个服务类。
- 接下来我们就分别介绍三个类包括的内容。 
2. 代码实现
2.1 基本活动信息
- 一些基本的活动信息;活动ID、活动名称、活动状态、开始时间、结束时间。 
2.2 活动枚举状态
- 活动的枚举;1创建编辑、2待审核、3审核通过(任务扫描成活动中)、4审核拒绝(可以撤审到编辑状态)、5活动中、6活动关闭、7活动开启(任务扫描成活动中) 
2.3 活动服务接口
- 在这个静态类中提供了活动的查询和状态变更接口; - queryActivityInfo、- queryActivityStatus、- execStatus。
- 同时使用Map的结构来记录活动ID和状态变化信息,另外还有init方法来初始化活动数据。实际的开发中这类信息基本都是从 - 数据库或者- Redis中获取。
五、用一坨坨代码实现
这里我们先使用最粗暴的方式来实现功能
对于这样各种状态的变更,最让我们直接想到的就是使用if和else进行判断处理。每一个状态可以流转到下一个什么状态,都可以使用嵌套的if实现。
1. 工程结构
- 整个实现的工程结构比较简单,只包括了两个类; - ActivityExecStatusController、- Result,一个是处理流程状态,另外一个是返回的对象。
2. 代码实现
- 这里我们只需要看一下代码实现的结构即可。从上到下是一整篇的 - ifelse,基本这也是大部分初级程序员的开发方式。
- 这样的面向过程式开发方式,对于不需要改动代码,也不需要二次迭代的,还是可以使用的( - 但基本不可能不迭代)。而且随着状态和需求变化,会越来越难以维护,后面的人也不好看懂并且很容易填充其他的流程进去。- 越来越乱就是从点滴开始的
3. 测试验证
3.1 编写测试类
- 我们的测试代码包括了两个功能的验证,一个是从 - 编辑中到- 审核拒绝,另外一个是从编辑中到- 提交审核。
- 因为从我们的场景流程中可以看到,编辑中的活动是不能直接到 - 审核拒绝的,这中间还需要- 提审。
3.2 测试结果
- 从测试结果和我们的状态流程的流转中可以看到,是符合测试结果预期的。除了不好维护外,这样的开发过程还是蛮快的,但不建议这么搞! 
六、状态模式重构代码
接下来使用状态模式来进行代码优化,也算是一次很小的重构。
重构的重点往往是处理掉ifelse,而想处理掉ifelse基本离不开接口与*抽象类*,另外还需要重新改造代码结构。
1. 工程结构
状态模式模型结构

- 以上是状态模式的整个工程结构模型,State是一个抽象类,定义了各种操作接口( - 提审、审核、拒审等)。
- 右侧的不同颜色状态与我们场景模拟中的颜色保持一致,是各种状态流程流转的实现操作。这里的实现有一个关键点就是每一种状态到下一个状态,都分配到各个实现方法中控制,也就不需要 - if语言进行判断了。
- 最后是 - StateHandler对状态流程的统一处理,里面提供- Map结构的各项服务接口调用,也就避免了使用- if判断各项状态转变的流程。
2. 代码实现
2.1 定义状态抽象类
- 在整个接口中提供了各项状态流转服务的接口,例如;活动提审、审核通过、审核拒绝、撤审撤销等7个方法。 
- 在这些方法中所有的入参都是一样的,activityId( - 活动ID)、currentStatus(- 当前状态),只有他们的具体实现是不同的。
2.2 部分状态流转实现
编辑
提审
- 这里提供了两个具体实现类的内容,编辑状态和提审状态。 
- 例如在这两个实现类中, - checkRefuse这个方法对于不同的类中有不同的实现,也就是不同状态下能做的下一步流转操作已经可以在每一个方法中具体控制了。
- 其他5个类的操作是类似的具体就不在这里演示了,大部分都是重复代码。可以通过源码进行学习理解。 
2.3 状态处理服务
- 这是对状态服务的统一控制中心,可以看到在构造函数中提供了所有状态和实现的具体关联,放到Map数据结构中。 
- 同时提供了不同名称的接口操作类,让外部调用方可以更加容易的使用此项功能接口,而不需要像在 - itstack-demo-design-19-01例子中还得传两个状态来判断。
3. 测试验证
3.1 编写测试类(Editing2Arraignment)
测试结果
- 测试编辑中To提审活动,的状态流转。 
3.2 编写测试类(Editing2Open)
测试结果
- 测试编辑中To开启活动,的状态流转。 
3.3 编写测试类(Refuse2Doing)
测试结果
- 测试拒绝To活动中,的状态流转。 
3.4 编写测试类(Refuse2Revoke)
测试结果
- 测试测试结果(拒绝To撤审),的状态流转。 
- 综上以上四个测试类分别模拟了不同状态之间的 - 有效流转和- 拒绝流转,不同的状态服务处理不同的服务内容。
七、总结
- 从以上的两种方式对一个需求的实现中可以看到,在第二种使用设计模式处理后已经没有了 - ifelse,代码的结构也更加清晰易于扩展。这就是设计模式的好处,可以非常强大的改变原有代码的结构,让以后的扩展和维护都变得容易些。
- 在实现结构的编码方式上可以看到这不再是面向过程的编程,而是面向对象的结构。并且这样的设计模式满足了 - 单一职责和- 开闭原则,当你只有满足这样的结构下才会发现代码的扩展是容易的,也就是增加和修改功能不会影响整体的变化。
- 但如果状态和各项流转较多像本文的案例中,就会产生较多的实现类。因此可能也会让代码的实现上带来了时间成本,因为如果遇到这样的场景可以按需评估投入回报率。主要点在于看是否经常修改、是否可以做成组件化、抽离业务与非业务功能。 
八、推荐阅读
版权声明: 本文为 InfoQ 作者【小傅哥】的原创文章。
原文链接:【http://xie.infoq.cn/article/17a7bdb0a9dc39d1261c1b612】。文章转载请联系作者。
 
  
  
  
  
  
  
  
  
  
    
评论