Spring Boot 2系列(五十一):状态设计模式实现简单的工作流

做软件开发的,经常会碰到流程性的业务需求,最常见的是工作流类型的需求。例如审批流程,下单支付流程。每个节点的每个状态分别由有相应权限的角色人处理。

实现流程性业务需求,通常会使用工作流引擎或使用状态流的方式来实现。常见的开源的工作流引擎有 ActivityFlowable

目前工作有涉及到工作流,管理层还准备往 BPM 转,研发有多个小团队,有用工作流引擎 ActivityFlowable,也有放弃工作流引擎转而使用状态流,并不统一。

工作需求驱动目学习目标,若不使用工作流引擎,基本上就是基于状态迁移的方式来实现(状态流),会引入状态设计模式,责任链设计模式,状态机相关概念,这也是本篇要描述的目标。

工作流

工作流技术起源自办公自动化领域的研究,在 OA 办公系统中最为常见。

工作流是一系列相互衔接、结构化的、预无定义、可自动进行,可视化的活动集。工作流由步骤、完成步骤所需的资源(例如人员或机器)以及步骤之间的交互组成。

一个工作流描述了一组任务(或活动)及它们的相互顺序关系,描述了一项工作的起点和终点,前进方向,可能存在的决策点,期望结果及潜在的替代步骤。

工作流的目标是实现过程管理的 自动化、智能化整合化,提高业务工作效率。

工作流相关概念可参考 工作流入门(基础概念篇)

状态模式

状态模式:是对象根据它内部的状态(属性)迁移,而改变自身的行为。详细可参考 设计模式:状态模式(State Pattern)

工作流中任务所处生命周期的节点称为 任务状态,任务的状态是可以迁移的以便进入下一个步骤(即根据状态改变而改变任务内部的行为),这与设计模式中的 状态模式 是契合的。

工作流的任务状态必属于三种基本类型:未开始、进行中、已完成

状态流实现审批

状态流实现完全是状态设计模式的实现,可以说是等价的。

理解状态流必须先理解状态模式的结构,核心是状态上下文持有具体状态对象,具体状态对象实现了抽象状态的处理事件的方法(行为),状态上下文中通过具体状态对象调用其处理事件的(行为)方法(!!!怎么好绕的感觉)。

备注:个人理解,用状态流来实现真实业务的工作流是不合适的,状态流适用于系统内部完全自动化的状态迁移的处理,而真实的业务工作流是在某个任务节点需要人为手动来完成再进入下一个节点的。

下面使用状态流来模拟一个请假审批流程,需求:员工请假,先项目经理批,如果请求超过3天,还需部门经理批,只要有不同意,则结束流程。

  1. 状态抽象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /**
    * 状态抽象接口
    */
    public interface IState {

    /**
    * 执行状态处理
    * @param stateContext
    */
    void handle(StateContext stateContext);
    }
  2. 具体抽象,可以忽略,创建是为了表示可以自定义

    1
    2
    3
    4
    5
    /**
    * 具体抽象接口
    */
    public interface TakeLeaveState extends IState {
    }
  3. 状态具体实现

    项目经理的审批状态具体实现:

    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
    /**
    * 具体状态
    * 项目经理审核
    */
    public class ProjectManagerState implements TakeLeaveState {
    private static final Logger logger = LogManager.getLogger(ProjectManagerState.class);

    private TakeLeaveRep takeLeaveRep;

    public ProjectManagerState() {
    }

    public ProjectManagerState(TakeLeaveRep takeLeaveRep) {
    this.takeLeaveRep = takeLeaveRep;
    }

    @Override
    public void handle(StateContext stateContext) {
    TakeLeave takeLeave = (TakeLeave) stateContext.getBusinessObj();
    System.out.println("项目经理审核中......");
    System.out.println(takeLeave.getUsername() + "申请从" + takeLeave.getStartDate() + "开始请假" + takeLeave.getLeaveDays() + "天,请项目经理审核:");
    if ("Y".equals(takeLeave.getCurrentState())) {
    System.out.println("项目经理审批:" + "同意");
    if (takeLeave.getLeaveDays() > 3) {
    System.out.println("请假超过3天,转交部门经理审批......");
    //请假大于3天转交上级部门经理审批
    stateContext.setState(new DepManagerState(takeLeaveRep));
    stateContext.doWork();
    } else {
    //3天及以内,同意后结束
    stateContext.setState(new AuditOverState(takeLeaveRep));
    stateContext.doWork();
    }
    } else {
    System.out.println("项目经理审批:" + "不同意");
    //不同意,强求束
    stateContext.setState(new AuditOverState(takeLeaveRep));
    stateContext.doWork();
    }
    }
    }

    部门经理的审批状态具体实现:

    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
    /**
    * 具体状态
    * 部门经理审核
    */
    public class DepManagerState implements TakeLeaveState {
    private static Logger logger = LogManager.getLogger(DepManagerState.class);

    private TakeLeaveRep takeLeaveRep;

    public DepManagerState(TakeLeaveRep takeLeaveRep) {
    this.takeLeaveRep = takeLeaveRep;
    }

    @Override
    public void handle(StateContext stateContext) {
    TakeLeave takeLeave = (TakeLeave) stateContext.getBusinessObj();
    if ("Y".equals(takeLeave.getCurrentState())) {
    System.out.println("部门经理审批:同意");
    //同意
    stateContext.setState(new AuditOverState(takeLeaveRep));
    stateContext.doWork();
    } else {
    System.out.println("部门经理审批:不同意");
    //不同意,结束
    stateContext.setState(new AuditOverState(takeLeaveRep));
    stateContext.doWork();
    }
    }
    }

    结束状态具体实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    /**
    * 审核结束状态
    */
    public class AuditOverState implements TakeLeaveState {
    private static Logger logger = LogManager.getLogger(DepManagerState.class);

    private TakeLeaveRep takeLeaveRep;

    public AuditOverState(TakeLeaveRep takeLeaveRep) {
    this.takeLeaveRep = takeLeaveRep;
    }

    @Override
    public void handle(StateContext stateContext) {
    TakeLeave takeLeave = (TakeLeave) stateContext.getBusinessObj();
    takeLeave.setCurrentState("审核结束");
    System.out.println("审核结束:" + takeLeave.getCurrentState());
    }
    }
  4. 状态上下文

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    /**
    * 状态上下文
    */
    @Data
    public class StateContext {

    private IState state;

    private Object businessObj;

    public void doWork(){
    this.state.handle(this);
    }
    }
  5. 请假单实体类

    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
    /**
    * 请假单实体
    */
    @Data
    @ToString
    @Entity(name = "take_leave")
    public class TakeLeave {

    /**
    * 主键ID
    */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    /**
    * 请假人
    */
    private String username;
    /**
    * 请假开始日期
    */
    private String startDate;
    /**
    * 请假天数
    */
    private Integer leaveDays;

    /**
    * 当前状态
    */
    private String currentState;

    /**
    * 状态记录
    */
    @Transient
    private List<TakeLeaveStateRecord> leaveStateRecords;
    }
  6. 请假单状态记录

    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
    /**
    * 状态记录表
    */
    @Data
    @Accessors(chain = true)
    @Entity(name = "take_leave_state_record")
    public class TakeLeaveStateRecord {

    /**
    * 主键ID
    */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer Id;

    /**
    * 关联ID
    */
    private Integer takeLeaveId;
    /**
    * 状态
    */
    private String state;
    /**
    * 审批人ID
    */
    private String auditId;
    /**
    * 审批人姓名
    */
    private String auditor;
    /**
    * 审批时间
    */
    private Date auditDate;
    }
  7. Controller 方法接收员工提交的请假单

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @RestController
    @RequestMapping("/takeLeave")
    public class TakeLeaveController {

    @Autowired
    private TakeLeaveService takeLeaveService;

    @PostMapping("/save")
    public TakeLeave takeLeave(TakeLeave takeLeave){
    takeLeaveService.save(takeLeave);
    return takeLeave;
    }
    }
  8. 提交保存触发状态流

    下面注入了 TakeLeaveRep 用于数据落库。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Service
    public class TakeLeaveServiceImpl implements TakeLeaveService {

    @Autowired
    private TakeLeaveRep takeLeaveRep;

    @Override
    public void save(TakeLeave takeLeave) {
    TakeLeaveStateContext context = new TakeLeaveStateContext();
    context.setBusinessObj(takeLeave);
    //配置上下文的初始状态,以后就随流程而动态变化(状态驱动)
    context.setState(new ProjectManagerState(takeLeaveRep));
    context.doWork();
    }
    }

    这种单纯的状态模式实是:在一开始传入初始状态后,整个状态流就全自动执行,无法人为干预。

  9. 提交请假表单

    而实际的业务操作是需要每个角色打开申请表单,点击是否同意按钮,再提交保存,请假单实体应该有一个当前节点的状态对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    post http://www.localhost:8080/takeLeave/save
    {
    "username":"张三",
    "startDate":"2019-12-05",
    "leaveDays":2,
    "currentState":"Y"
    }

    结果输出:
    张三申请从2019-12-05开始请假4天,请项目经理审核。
    项目经理审核中......
    项目经理审批:同意
    请假超过3天,转交部门经理审批......
    部门经理审批:同意
    审核结束:审核结束
    张三申请从2019-12-05开始请假2天,请项目经理审核。
    项目经理审核中......
    项目经理审批:同意
    审核结束:审核结束

    上面示例相当于再一次理解状态设计模式,并不适用于实际的业务流程设计。

状态模式实现变种

此变种模式需要每个状态显式调用,需要传入事件,当前状态执行完同时设置下一状态对象,而不是系统内部自动调用。

需求:空调摇控器控制空调的状态,初始是关闭状态。状态迁移如下:

关闭 送风 制冷
关闭 事件:按电源按钮
送风 事件:按送风按钮 事件:按制冷按钮
制冷 事件:按制冷按钮 事件:按送风按钮
  1. 抽象状态接口

    1
    2
    3
    4
    5
    6
    /**
    * 抽象状态接口
    */
    public interface IState {
    void handle(StateContext stateContext, OperateEvent event);
    }
  2. 具体状态对象

    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
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    /**
    * 关闭中状态
    */
    public class OffState implements IState {
    @Override
    public void handle(StateContext context, OperateEvent event) {
    switch (event){
    case CLICK_POWER:
    //进入下一个状态
    context.setState(new FanOnlyState());
    doStartFan();
    break;
    }
    }

    private void doStartFan() {
    System.out.println("开始送风");
    }
    }

    /**
    * 送风中状态
    */
    public class FanOnlyState implements IState {
    @Override
    public void handle(StateContext context, OperateEvent event) {
    switch (event) {
    case CLICK_POWER:
    //下一状态:送风
    context.setState(new OffState());
    doStopFan();
    break;
    case CLICK_COOL:
    //下一状态:制冷
    context.setState(new CoolState());
    doStartCool();
    break;
    }
    }

    private void doStopFan() {
    System.out.println("关闭送风");
    }
    private void doStartCool() {
    System.out.println("开始制冷");
    }
    }

    /**
    * 制冷中状态
    */
    public class CoolState implements IState {

    @Override
    public void handle(StateContext stateContext, OperateEvent event) {
    switch (event) {
    case CLICK_POWER:
    stateContext.setState(new OffState());
    doStopCool();
    break;
    case CLICK_COOL:
    stateContext.setState(new FanOnlyState());
    doStartFan();
    break;
    }
    }

    private void doStartFan() {
    System.out.println("开始送风");
    }
    private void doStopCool(){
    System.out.println("关闭制冷");
    }
    }
  3. 状态上下文

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    /**
    * 状态上下文
    */
    @Data
    @Accessors(chain = true)
    public class StateContext {

    private IState state;

    private Object businessObj;

    public StateContext() {
    }

    public StateContext(IState state) {
    this.state = state;
    }

    public void doWork(OperateEvent event) {
    this.state.handle(this, event);
    }
    }
  4. 事件枚举

    1
    2
    3
    4
    5
    6
    /**
    * 事件枚举
    */
    public enum OperateEvent {
    CLICK_POWER, CLICK_COOL,
    }
  5. 客户端提交

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class MainTest {
    public static void main(String[] args) {
    //初始状态
    IState initState = new OffState();
    StateContext context = new StateContext(initState);
    context.doWork(OperateEvent.CLICK_POWER);//开始送风
    context.doWork(OperateEvent.CLICK_COOL);//开始制冷
    context.doWork(OperateEvent.CLICK_COOL);//开始送风
    context.doWork(OperateEvent.CLICK_POWER);//关闭送风
    }
    }

其它参考

  1. 工作流基础概念
  2. Windows Workflow:状态机工作流
  3. 从工作流状态机实践中总结状态模式使用心得
  4. JBPM学习实现一个简单的工作流
  5. 工作流引擎的设计与实现

Spring Boot 2系列(五十一):状态设计模式实现简单的工作流

http://blog.gxitsky.com/2019/12/15/SpringBoot-51-workflow-state/

作者

光星

发布于

2019-12-15

更新于

2022-06-17

许可协议

评论