前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >COLA-statemachine在多级审核业务中的实践

COLA-statemachine在多级审核业务中的实践

作者头像
benym
发布2023-10-18 15:00:52
8160
发布2023-10-18 15:00:52
举报
文章被收录于专栏:后端知识体系后端知识体系

# 背景

在实际的项目开发中,开发者经常会遇见类似多级审核之类的开发需求,比如某个文件审核,需要经过申请->直系领导审核->总经理审核等多个步骤。如果是一次动作触发整个审核过程,开发者可能会想到使用责任链模式来进行开发。但如果多级审核的间隔时间长,审核触发的条件不一样,责任链模式会不太能够解耦这项需求。如果采用平铺直叙式开发,无疑会将审核状态转移过程散落在系统间各个位置,前后两个状态之间的关系没有直观进行维护,同时状态转移时的条件、执行的方式和状态之间的逻辑关系很容易让开发者写出“面条代码”。在项目开发初期可能还好,随着需求的增量变化,平铺直叙式开发将使得状态转移逻辑和业务逻辑高度混合,且每增加一级节点审核,就要新增对应的审核状态及状态转移的逻辑,长此以往变得难以阅读和维护。所以,在这种情况下使用状态机这样建模方式就显得尤为必要。

# 状态机概述

在计算机领域谈及状态机一般有有限状态机(FSM finite state machine)无限状态机(ISM Infinite state machine),由于无限状态机只是理论存在,所以应用中均使用有限状态机。

有限状态机是一种抽象的计算模型,其核心思想在于系统在不同状态下对于输入会产生不同的响应,并且可以根据输入和当前状态转移到新的状态。

状态机通常由状态(State)事件(Event)动作(Action)三个基本元素构成。其中动作不是必须的,可以只根据事件进行状态转移。

对于开发者视角的状态机通常还会增加转移条件(Condtion)的概念,此时状态机模型变更为

其中转移条件也是可选的。

# 状态机选型

对于开源状态机框架的选型和多种实现方式不是本文讨论的重点,详情可查看状态机引擎在vivo营销自动化中的深度实践 (opens new window)

引用文章中的一张图概况开源状态机框架现状

本文选用的为COLA-Statemachine

# 基本实现

本文涉及的MVP代码地址github (opens new window)

以小朋友要出去玩需要经过爸爸同意、妈妈同意这样的场景为例。

# 实体建模

状态(State)可以建模为

  • 已申请、爸爸同意、妈妈同意、爸爸不同意、妈妈不同意、已完成

事件(Event)可以建模为

  • 同意、不同意、已完成

状态转译成代码可以用枚举类表示

代码语言:javascript
复制
public enum AuditState {

    /**
     * 已申请
     */
    APPLY("APPLY", "已申请"),
    /**
     * 爸爸同意
     */
    DAD_PASS("DAD_PASS", "爸爸同意"),
    /**
     * 妈妈同意
     */
    MOM_PASS("MOM_PASS", "妈妈同意"),
    /**
     * 爸爸不同意
     */
    DAD_REJ("DAD_REJ", "爸爸不同意"),
    /**
     * 妈妈不同意
     */
    MOM_REJ("MOM_REJ", "妈妈不同意"),
    /**
     * 已完成
     */
    DONE("DONE", "已完成");

    private static final Map<String, AuditState> CODE_MAP = new ConcurrentHashMap<>();

    static {
        for (AuditState auditState : EnumSet.allOf(AuditState.class)) {
            CODE_MAP.put(auditState.getCode(), auditState);
        }
    }

    public static AuditState getEnumsByCode(String code) {
        return CODE_MAP.get(code);
    }

    /**
     * code
     */
    private String code;

    /**
     * desc
     */
    private String desc;

    AuditState(String code, String desc) {
        this.code = code;
        this.desc = desc;
    }
    // 省略get/set
}

事件转译成代码为

代码语言:javascript
复制
public enum AuditEvent {

    /**
     * 同意
     */
    PASS(0,"同意"),

    /**
     * 不同意
     */
    REJECT(1,"不同意"),

    /**
     * 已完成
     */
    DONE(2,"已完成");

    /**
     * code
     */
    private Integer code;

    /**
     * desc
     */
    private String desc;

    private static final Map<Integer, AuditEvent> CODE_MAP = new ConcurrentHashMap<>();

    static {
        for (AuditEvent auditEvent : EnumSet.allOf(AuditEvent.class)) {
            CODE_MAP.put(auditEvent.getCode(), auditEvent);
        }
    }

    public static AuditEvent getEnumsByCode(Integer code) {
        return CODE_MAP.get(code);
    }

    AuditEvent(Integer code, String desc) {
        this.code = code;
        this.desc = desc;
    }
    // 省略get/set
}

在使用COLA状态机时,还要求开发者传递Context参数,可用于后续的ConditionAction等,根据我们的场景,只需要知道该审核单的id和当前接口审核的事件AuditEvent即可,所以Context可以建模为

代码语言:javascript
复制
@Data
public class AuditContext {

    /**
     * id
     */
    private Long id;

    /**
     * 事件
     */
    private Integer auditEvent;
}

# Spring体系下的可扩展状态机

完成实体建模后就是状态机的构建了,通常来说我们应该结合SpringBoot体系达成系统内多个状态机的自动识别和自动获取。

# 构建状态机

建立一个基本的策略接口

代码语言:javascript
复制
public interface StateMachineStrategy {

    String getMachineType();
}

状态机枚举

代码语言:javascript
复制
public enum StateMachineEnum {

    /**
     * 测试状态机
     */
    TEST_MACHINE("testMachine","测试状态机");

    /**
     * code
     */
    private String code;

    /**
     * desc
     */
    private String desc;

    StateMachineEnum(String code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }
}

状态机实现

代码语言:javascript
复制
@Component
public class AuditMachine implements StateMachineStrategy {

    @Autowired
    private ConditionService conditionService;

    @Autowired
    private ActionService actionService;

    @Override
    public String getMachineType() {
        return StateMachineEnum.TEST_MACHINE.getCode();
    }

    /**
     * | From(开始状态) | To(抵达状态) | Event(事件) | When(条件)            | Perform(执行动作)  |
     * | -------------- | ------------ | ----------- | --------------------- | ------------------ |
     * | 已申请      | 爸爸同意 | 审核通过    | passOrRejectCondition | passOrRejectAction |
     * | 爸爸同意    | 妈妈同意 | 审核通过    | passOrRejectCondition | passOrRejectAction |
     * | 已申请     | 爸爸不同意 | 审核驳回    | passOrRejectCondition | passOrRejectAction |
     * | 爸爸同意   | 妈妈不同意 | 审核驳回    | passOrRejectCondition | passOrRejectAction |
     * | 已申请    | 已完成状态    | 已完成        | doneCondition        | doneAction        |
     * | 爸爸同意  | 已完成状态    | 已完成        | doneCondition        | doneAction        |
     * | 妈妈同意  | 已完成状态    | 已完成        | doneCondition        | doneAction        |
     *
     * @return StateMachine stateMachine
     */
    @Bean
    public StateMachine<AuditState, AuditEvent, AuditContext> stateMachine() {
        StateMachineBuilder<AuditState, AuditEvent, AuditContext> builder = StateMachineBuilderFactory.create();
        // 已申请->爸爸同意
        builder.externalTransition().from(AuditState.APPLY).to(AuditState.DAD_PASS)
                .on(AuditEvent.PASS)
                .when(conditionService.passOrRejectCondition())
                .perform(actionService.passOrRejectAction());
        // 已申请->爸爸不同意
        builder.externalTransition().from(AuditState.APPLY).to(AuditState.DAD_REJ)
                .on(AuditEvent.REJECT)
                .when(conditionService.passOrRejectCondition())
                .perform(actionService.passOrRejectAction());
        // 爸爸同意->妈妈同意
        builder.externalTransition().from(AuditState.DAD_PASS).to(AuditState.MOM_PASS)
                .on(AuditEvent.PASS)
                .when(conditionService.passOrRejectCondition())
                .perform(actionService.passOrRejectAction());
        // 爸爸同意->妈妈不同意
        builder.externalTransition().from(AuditState.DAD_PASS).to(AuditState.MOM_REJ)
                .on(AuditEvent.REJECT)
                .when(conditionService.passOrRejectCondition())
                .perform(actionService.passOrRejectAction());
        // 已申请->已完成
        // 爸爸同意->已完成
        // 妈妈同意->已完成
        builder.externalTransitions().fromAmong(AuditState.APPLY, AuditState.DAD_PASS, AuditState.MOM_PASS)
                .to(AuditState.DONE)
                .on(AuditEvent.DONE)
                .when(conditionService.doneCondition())
                .perform(actionService.doneAction());
        return builder.build(getMachineType());
    }
}

从实现类可见状态、事件、条件、动作,在代码中是非常清晰的,且维护在一个类中。

此时的状态机DSL图如下(通过接口获取)

状态机引擎类

代码语言:javascript
复制
@Component
public class StateMachineEngine implements InitializingBean {
    
    private Map<String, StateMachineStrategy> stateMachineMap;
    
    @Autowired
    private ApplicationContext applicationContext;

    /**
     * 根据枚举获取状态机实例key
     * 
     * @param stateMachineEnum stateMachineEnum
     * @return String
     */
    private String getMachine(StateMachineEnum stateMachineEnum) {
        return stateMachineMap.get(stateMachineEnum.getCode()).getMachineType();
    }

    /**
     * 根据枚举获取状态机示例,并根据当前状态、事件、上下文,进行状态流转
     *
     * @param stateMachineEnum stateMachineEnum
     * @param auditState auditState
     * @param auditEvent auditEvent
     * @param auditContext auditContext
     * @return AuditState
     */
    public AuditState fire(StateMachineEnum stateMachineEnum, AuditState auditState,
                           AuditEvent auditEvent, AuditContext auditContext) {
        StateMachine<AuditState, AuditEvent, AuditContext> stateMachine = StateMachineFactory.get(getMachine(stateMachineEnum));
        return stateMachine.fireEvent(auditState, auditEvent, auditContext);
    }

    /**
     * 根据枚举获取状态机示例的状态DSL UML图
     *
     * @param stateMachineEnum stateMachineEnum
     * @return String
     */
    public String generateUml(StateMachineEnum stateMachineEnum){
        StateMachine<AuditState, AuditEvent, AuditContext> stateMachine = StateMachineFactory.get(getMachine(stateMachineEnum));
        return stateMachine.generatePlantUML();
    }

    /**
     * 获取所有实现了接口的状态机
     */
    @Override
    public void afterPropertiesSet() {
        Map<String, StateMachineStrategy> beansOfType = applicationContext.getBeansOfType(StateMachineStrategy.class);
        stateMachineMap = Optional.of(beansOfType)
                .map(beansOfTypeMap -> beansOfTypeMap.values().stream()
                        .filter(stateMachineHandler -> StringUtils.hasLength(stateMachineHandler.getMachineType()))
                        .collect(Collectors.toMap(StateMachineStrategy::getMachineType, Function.identity())))
                .orElse(new HashMap<>(8));
    }
}

其中上文内的ConditionServiceActionService分别表示转移条件的Service服务和动作的Service服务

COLA中定义了CondtionAction接口

本文的Condition实现为

代码语言:javascript
复制
public interface ConditionService {

    /**
     * 通用通过/驳回条件
     * 覆盖审核正向流程,以及驳回流程
     * 已申请->爸爸同意->妈妈统一
     * 已申请->爸爸不同意
     * 爸爸同意->妈妈不同意
     *
     * @return Condition
     */
    Condition<AuditContext> passOrRejectCondition();

    /**
     * 已完成条件
     *
     * @return Condition
     */
    Condition<AuditContext> doneCondition();
}
代码语言:javascript
复制
@Component
public class ConditionServiceImpl implements ConditionService {
    @Override
    public Condition<AuditContext> passOrRejectCondition() {
        return context -> {
            System.out.println(1);
            return true;
        };
    }

    @Override
    public Condition<AuditContext> doneCondition() {
        return context -> {
            System.out.println(1);
            return true;
        };
    }
}

这里简单起见并没有在Condition上做特殊条件判断,如果需要拦截返回false便不会执行后续Action

本文的Action实现为

代码语言:javascript
复制
public interface ActionService {

    /**
     * 通用审核通过/驳回执行动作
     * 覆盖审核正向流程,以及驳回流程
     * 已申请->爸爸同意->妈妈同意
     * 已申请->爸爸不同意
     * 爸爸同意->妈妈不同意
     *
     * @return Action
     */
    Action<AuditState, AuditEvent, AuditContext> passOrRejectAction();

    /**
     * 已完成执行动作
     *
     * @return Action
     */
    Action<AuditState, AuditEvent, AuditContext> doneAction();
}
代码语言:javascript
复制
@Component
public class ActionServiceImpl implements ActionService {

    private static final Logger LOGGER = LoggerFactory.getLogger(ActionServiceImpl.class);

    @Autowired
    private AuditDao auditDao;

    @Override
    public Action<AuditState, AuditEvent, AuditContext> passOrRejectAction() {
        return (from, to, event, context) -> {
            LOGGER.info("passOrRejectAction from {}, to {}, on event {}, id:{}", from, to, event, context.getId());
            auditDao.updateAuditStatus(to.getCode(), context.getId());
        };
    }

    @Override
    public Action<AuditState, AuditEvent, AuditContext> doneAction() {
        return (from, to, event, context) -> {
            LOGGER.info("doneAction from {}, to {}, on event {}, id:{}", from, to, event, context.getId());
            auditDao.updateAuditStatus(to.getCode(), context.getId());
        };
    }
}

上述代码均采用匿名函数的写法,其实等价于

代码语言:javascript
复制
@Override
public Action<AuditState, AuditEvent, AuditContext> passOrRejectAction() {
    Action<AuditState, AuditEvent, AuditContext> action = new Action<AuditState, AuditEvent, AuditContext>() {
        @Override
        public void execute(AuditState from, AuditState to, AuditEvent event, AuditContext context) {

        }
    };
    return action;
}

Action中只是进行根据id更新状态,这一操作

# Controller层
代码语言:javascript
复制
@RestController
@RequestMapping("/test")
@Slf4j
public class TestController {

    @Autowired
    private AuditService auditService;

    @PostMapping("/audit")
    public void audit(@RequestBody @Validated AuditParam auditParam){
        AuditContext auditContext = new AuditContext();
        BeanUtils.copyProperties(auditParam, auditContext);
        auditService.audit(auditContext);
    }

    @GetMapping("/uml")
    public String uml(){
        return auditService.uml();
    }
}

提供2个接口,一个接口用于触发多级审核状态机,一个接口用于获取状态机内置的UML图

其中AuditParam

代码语言:javascript
复制
@Data
public class AuditParam {

    /**
     * id
     */
    private Long id;

    /**
     * 事件
     */
    private Integer auditEvent;
}
# Service层
代码语言:javascript
复制
@Service
@Slf4j
public class AuditServiceImpl implements AuditService {

    @Autowired
    private AuditDao auditDao;

    @Autowired
    private StateMachineEngine stateMachineEngine;

    @Override
    public void audit(AuditContext auditContext) {
        Long id = auditContext.getId();
        AuditDTO auditDTO = auditDao.selectById(id);
        String auditState = auditDTO.getAuditState();
        Integer auditEvent = auditContext.getAuditEvent();
        // 获取当前状态和事件
        AuditState nowState = AuditState.getEnumsByCode(auditState);
        AuditEvent nowEvent = AuditEvent.getEnumsByCode(auditEvent);
        // 执行状态机
        stateMachineEngine.fire(StateMachineEnum.TEST_MACHINE, nowState, nowEvent, auditContext);
    }

    @Override
    public String uml() {
        return stateMachineEngine.generateUml(StateMachineEnum.TEST_MACHINE);
    }
}

Service层首先根据id查询该审核单在数据库中的状态,通过当前状态和事件获取状态机需要的StateEvent,同时构建Context,通过前面构造的状态机引擎执行fire方法进行状态转化。

# 请求模拟

数据库中初始有一条已申请数据,id1状态APPLY

Postman发如下请求时,代表对于id1的数据,进行一次审核,且审核事件为0(同意)

此时控制台打印为

表示正常执行Action

数据库状态变更为

# Q/A

提示

Q: 当状态机内没有定义某个状态转移时,比如此时数据库状态为DONE,请求带上审核事件为同意的参数进来,状态机会发生什么?

笔记

A: 由状态机框架内部处理,此时状态机什么都不会发生,也不会抛出异常

提示

Q: 在状态机的Action和Condition方法上加AOP注解有效吗

笔记

A: 无效,Action和Condition由框架内部直接调用,框架并未交给Spring管理,所以无法产生代理对象执行增强。具体经验可查看COLA-statemachine事务失效踩坑 (opens new window)

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-08-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • # 背景
  • # 状态机概述
  • # 状态机选型
  • # 基本实现
    • # 实体建模
      • # Spring体系下的可扩展状态机
        • # 构建状态机
        • # Controller层
        • # Service层
      • # 请求模拟
        • # Q/A
        相关产品与服务
        腾讯企点营销
        腾讯企点原厂自研重磅级SaaS营销产品,是数字营销领域的行业领军品牌。全渠道数字化平台,提供给市场营销人员更高效企业市场部的营销管理工作台,实现全场景获客,全周期线索管理,全旅程自动化营销,全链路数据洞察。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档