luckydraw-ddd 06-活动领域的配置和状态
领域开发-活动领域的配置和状态
学习目的
【1】活动流转状态梳理
【2】活动领域构建
参考分支 | 210911_xfg_activity |
开发分支 | dev_220119_01_activity |
1.需求分析
开发活动领域部分功能,包括:活动创建、活动状态变更。主要以 domain 领域层下添加 activity 为主,并在对应的 service 中添加 deploy(创建活动)、partake(领取活动,待开发)、stateflow(状态流转) 三个模块。以及调整仓储服务实现到基础层
2.项目结构设计
开发说明
【1】DDD模型适配:原有工程实现在lottery-domain中定义repository实现仓储服务,按照DDD模型改造:在lottery-domain
领域层定义仓储接口,在lottery-infrastructure
基础层实现仓储接口
【2】活动状态流转分析
【3】活动状态流转实现(对比传统实现和状态模式
构建)
DDD模型适配
原有工程实现在lottery-domain中定义repository实现仓储服务,按照DDD模型改造:在lottery-domain
领域层定义仓储接口,在lottery-infrastructure
基础层实现仓储接口,以award
领域说明(其余构建思路类似):
原有工程实现:
lottery-domain
└── src
└── main
└── java
└── cn.itedus.lottery.domain.award
├── model
│ ├── req # 请求参数
│ ├── res # 响应结果
│ ├── vo # 视图层参数封装
├── repository
│ ├── impl
│ │ └── AwardRepository # 仓储服务实现
│ └── IAwardRepository # 仓储服务定义
└── service # 领域服务定义&实现
lottery-infrastructure
└── src
└── main
└── java
└── cn.itedus.lottery.infrastructure.award
├── dao # 数据操作层接口定义
├── po # 数据库实体定义
改造后工程实现:
lottery-domain
└── src
└── main
└── java
└── cn.itedus.lottery.domain.award
├── model
│ ├── req # 请求参数
│ ├── res # 响应结果
│ ├── vo # 视图层参数封装
├── repository
│ └── IAwardRepository # 仓储服务定义
└── service # 领域服务定义&实现
lottery-infrastructure 引入lottery-domain,实现仓储服务相关内容
└── src
└── main
└── java
└── cn.itedus.lottery.infrastructure.award
├── dao # 数据操作层接口定义
├── po # 数据库实体定义
├── repository
│ └── AwardRepository # 仓储服务实现
构建过程
调整说明
【1】lottery-infrastructure引入lottery-domain依赖,lottery-domain取消原有对lottery-infrastructure的依赖(避免循环依赖),更新完成刷新maven
【2】将lottery-domain原有的仓储服务实现迁移到lottery-infrastructure对应领域的repository空间
lottery-domain->repository/impl迁移至lottery-infrastructure->repository
【3】lottery-domain activity领域构建
引入activity相关的内容,涉及内容说明如下所示
lottery-domain中activity领域构建(model、repository仓储接口定义、service服务领域构建)
lottery-infrastructure基础层中仓储构建(dao接口定义、repository仓储接口实现)
lottery-common中Constant常量枚举构建(引入活动状态)
lottery-interfaces(resources:dao对应mapper文件资源引入)
问题说明
【1】循环依赖引入问题
java: Annotation processing is not supported for module cycles. Please ensure that all modules from cycle [lottery-infrastructure,lottery-domain] are excluded from annotation processing
此处需要注意的一点是,由于之前的工程中将repository的实现定义在lottery-domain中,而repository的实现构建依托于dao层的调用,因此在lottery-domain中引用了lottery-infrastructure模块,而在本次调整中将仓储服务的实现转移到lottery-infrastructure中(dao和repository实现在同一个模块)在lottery-infrastructure中引入lottery-domain模块(引用repository仓储服务定义),如果不注意取消掉原有“lottery-domain中引用的lottery-infrastructure模块”,就会出现循环依赖的问题,为了解决这一问题可以从两个方面考虑:
- 一是明确划分职责,尽量避免循环依赖问题(把循环依赖扼杀在摇篮里)
由上述调整分析可知,lottery-domain中的仓储服务实现已经迁移到lottery-infrastructure,说明lottery-domain中只需要根据业务定义相应的接口即可(不涉及dao相关调用的具体实现),因此lottery-domain
中并不需要引用lottery-infrastructure
,直接在pom.xml去除相关依赖即可
- 二是解决循环依赖问题(如果实在是不可避免多模块工程之间的相互调用问题)
参考了一些思路,可有如下解决方案:
借助build-helper-maven-plugin插件进行规避
例如:A依赖B,B依赖C,C依赖A的情况。
这个插件提供了一种规避措施,即临时地将工程A、B、C合并成一个中间工程,编译出临时的模块D。然后A、B、C再分别依赖临时模块D进行编译)
但这种方式只是一种规避措施,并没有从根本上解决工程间依赖关系混乱的问题
重构(从工程结构上进行根治)
平移:例如A、B相互依赖,则将B依赖A的代码平移到工程B中,则B不需要依赖依赖A,从而消除循环依赖
下移:例如A、B互相依赖,且A、B依赖于C,则可将A、B中相互依赖的部分代码迁移到C中,让A、B只依赖于C,从而消除循环依赖(这种思路和上述build-helper-maven-plugin的思路是类似的,只不过一个从编译过程中解决循环依赖,一个是从实体项目结构过程中解决依赖)
活动领域构建
活动发布(活动创建)
在activity领域中构建deploy
实现活动发布相关内容(主要是活动创建)
lottery-domain
└── src
└── main
└── java
└── cn.itedus.lottery.domain.activity
├── model
├── repository
│ └── IActivityRepository # 仓储服务定义
├── service # 领域服务定义&实现
│ ├── deploy # 活动发布
├── impl # 活动发布
│ └── IActivityDeployImpl # 领域服务实现
└── IActivityDeploy # 领域服务定义
lottery-infrastructure
└── src
└── main
└── java
└── cn.itedus.lottery.infrastructure.activity
├── dao # 数据操作层接口定义
├── po # 数据库实体定义
活动的创建操作主要内容包括:添加活动配置、添加奖品配置、添加策略配置、添加策略明细配置,这些都是在同一个注解事务配置下进行处理 @Transactional(rollbackFor = Exception.class)
# cn.itedus.lottery.domain.activity.service.deploy.impl.ActivityDeployImpl
public class ActivityDeployImpl implements IActivityDeploy {
private Logger logger = LoggerFactory.getLogger(ActivityDeployImpl.class);
@Resource
private IActivityRepository activityRepository;
@Transactional(rollbackFor = Exception.class)
@Override
public void createActivity(ActivityConfigReq req) {
logger.info("创建活动配置开始,activityId:{}", req.getActivityId());
ActivityConfigRich activityConfigRich = req.getActivityConfigRich();
try {
// 添加活动配置
ActivityVO activity = activityConfigRich.getActivity();
activityRepository.addActivity(activity);
// 添加奖品配置
List<AwardVO> awardList = activityConfigRich.getAwardList();
activityRepository.addAward(awardList);
// 添加策略配置
StrategyVO strategy = activityConfigRich.getStrategy();
activityRepository.addStrategy(strategy);
// 添加策略明细配置
List<StrategyDetailVO> strategyDetailList = activityConfigRich.getStrategy().getStrategyDetailList();
activityRepository.addStrategyDetailList(strategyDetailList);
logger.info("创建活动配置完成,activityId:{}", req.getActivityId());
} catch (DuplicateKeyException e) {
logger.error("创建活动配置失败,唯一索引冲突 activityId:{} reqJson:{}", req.getActivityId(), JSON.toJSONString(req), e);
throw e;
}
}
@Override
public void updateActivity(ActivityConfigReq req) {
// TODO: 非核心功能后续补充
}
}
活动状态流转(状态变更:状态模式)
状态模式:类的行为是基于它的状态改变的,这种类型的设计模式属于行为型模式。它描述的是一个行为下的多种状态变更,比如我们最常见的一个网站的页面,在你登录与不登录下展示的内容是略有差异的(不登录不能展示个人信息),而这种登录与不登录就是我们通过改变状态,而让整个行为发生了变化
【1】活动状态流转分析
踩坑日记(待学习补充)
针对流程管理类相关设计,可能涉及很多记录状态的流转、变更,一开始分析可能会有点懵,包括自己在一开始接触这种状态流转概念的时候,经常会被每个状态可能是由什么状态转过来的、又可以转变为什么状态搞的晕头转向,但接触过流程管理相关系统的开发,了解相关流程引擎的工作原理和思路,在针对一些流程类业务开发的时候,要先抓住业务流程类开发的重点是“流转处理”,而记录的流转则是由状态节点一个个串联起来的,因此在梳理流程状态的时候可以尝试以下思路(以lottery活动流转为例进行说明)
1.先梳理业务流程,然后分析涉及的流程状态节点
查看、编辑、提审、撤审、通过、拒绝、关闭、开启、执行
2.分析流程状态节点可以执行的操作(当前的节点状态的流程可以执行的下一步操作是什么(可以变更的target状态是什么))
此处不要将“当前状态节点可能是由什么状态转过来“纳入分析,因为这会让自己处于混沌状态,也是流程开发的一个小误区。当我们确定一下流程开发的步骤(业务流程),其相应的节点状态也确定下来,因此只需要根据流程节点状态的流转走便能形成”回路“,将重点侧重于”当前状态可以执行什么操作、变成下一个targetStatus“
反过来想,之所以一开始会考虑某个节点状态是由什么转过来的,也是基于业务校验的一个考虑,担心存在不符合流转规则的数据,但如果能够在流转的过程中去控制(”校验流转状态变更,从而限制入口“),这个问题也就不复存在(流转规则制定、流转过程校验)
而流转规则的指定则需结合实际业务考虑,例如针对一些复杂的业务,某个节点状态又可根据不同的情况限制相应的操作
此处则需区分“状态流转”和“业务功能限制”,“状态流转”只需考虑当前节点能否流转到下一节点,而“业务功能限制”则需考虑当前节点的上一节点是什么,可以执行什么操作(可限制功能访问甚至是限制下一节点的流转),简单举例说明
3.根据每个流转状态节点,制定相应的方法供状态变更
以lottery为例,说明如下所示:依据流程分析每个状态节点的状态和流转,只考虑当前节点状态的出入分析。针对某个状态节点只考虑“出”的情况,即由当前节点和可以执行什么操作(变更为指定状态),“入”的情况则可在其他状态中体现
- 编辑态
- 提审态
- 撤审态
- 通过态
- 拒绝态
- 关闭态
- 开启态
- 执行态
基于上述分析,再重新以流程节点流转的理思路去再尝试去勾勒整个节点流转图即可
【2】活动状态流转实现
抽象出状态抽象类AbstractState
定义活动状态xxxState继承AbstractState,并配置StateConfig装载状态配置
抽离IStateHandler定义处理接口,使用StateHandlerImpl 对外提供接口服务
开发思路梳理
1.定义状态(将活动过程中涉及到的状态一一列举)
活动涉及状态:查看、编辑、提审、撤审、通过、拒绝、关闭、开启、执行
- 针对每个状态而言:可以只考虑“出”的情况
前台根据当前活动状态可变更为什么指定状态,从而提供相应的按钮控制)
后台则根据当前活动状态进行校验:校验当前活动状态是否可以变更为指定状态,从而执行变更操作
2.状态流转控制(从当前活动状态到指定状态)
定义活动状态抽象类:根据活动状态,定义每个活动状态对应操作方法
定义活动状态处理服务类:根据活动当前状态,执行要变更的状态操作
lottery-domain
└── src
└── main
└── java
└── cn.itedus.lottery.domain.activity
├── model
├── repository
│ └── IActivityRepository
└── service
├── deploy
├── partake [待开发]
└── stateflow
├── event
│ ├── ArraignmentState.java
│ ├── CloseState.java
│ ├── DoingState.java
│ ├── EditingState.java
│ ├── OpenState.java
│ ├── PassState.java
│ └── RefuseState.java
├── impl
│ └── StateHandlerImpl.java
├── AbstractState.java
├── IStateHandler.java
└── StateConfig.java
实现
1.定义状态抽象类AbstractState,提供各项状态流转服务的接口(提审、通过、拒绝、撤审、开启、关闭、活动中等)
@Component
public class AbstractState {
@Resource
protected IActivityRepository activityRepository;
/**
* 流转操作
* activityId:活动ID
* currentState:活动状态枚举 对应当前活动状态
*/
public abstract Result oper(Long activityId, Enum<Constants.ActivityState> currentState);
}
2.定义活动状态xxxState继承AbstractState,并配置StateConfig装载状态配置
xxxState定义:继承AbstractState 重载相关实现方法(根据当前状态,限定流转是否可执行)
@Component
public class xxxState extends AbstractState {
@Override
public Result oper(Long activityId, Enum<Constants.ActivityState> currentState) {
// 1.流转状态校验:根据当前状态进行判断或者其他限制判断是否放行
// 2.如果校验正常则执行流转操作,处理业务逻辑
return Result.buildResult(Constants.ResponseCode.SUCCESS, "xxx");
}
}
StateConfig流转状态配置类定义:装载流转状态相关
public class StateConfig {
@Resource
private xxxState xxxState;
protected Map<Enum<Constants.ActivityState>, AbstractState> stateGroup = new ConcurrentHashMap<>();
@PostConstruct
public void init() {
// 将指定状态装载入map
stateGroup.put(Constants.ActivityState.xxxState, xxxState);
}
}
3.抽离IStateHandler定义处理接口,使用StateHandlerImpl 对外提供接口服务
IStateHandler定义:抽离业务处理接口
public interface IStateHandler {
// 定义相关处理方法
Result handle(Long activityId, Enum<Constants.ActivityState> currentStatus);
}
StateHandlerImpl 定义:状态流转实现(对外提供接口)
@Service
public class StateHandlerImpl extends StateConfig implements IStateHandler {
// 重载处理方法实现,根据当前传入的状态获取相应的处理服务操作相应的流转变更
@Override
public Result handle(Long activityId, Enum<Constants.ActivityState> currentStatus) {
return stateGroup.get(currentStatus).oper(activityId, currentStatus);
}
}
活动领域构建测试
活动发布测试
# 单元测试,运行后检查日志和数据表Activity、Award、Strategy、StrategyDetail
# lottery-interface:cn.itedus.lottery.test.domain.ActivityTest
活动流转测试
# 根据不同状态验证状态流转,检查正常流转和拒绝的情况
活动状态:1编辑、2提审、3撤审、4通过、5运行(审核通过后worker扫描状态)、6拒绝、7关闭、8开启
测试数据活动初始化状态为1-编辑状态,该状态可执行“提审”状态,随后根据变化按顺序分析状态流转即可
3.问题思考
前置状态控制:
针对并发场景应用,对应同一条记录,不同运营人员操作具有现有顺序且审核状态结果不同,因此需要通过前置状态校验限制状态变更操作。例如对于并发场景考虑下,需校验前置状态控制流转,避免出现异常问题
TODO:状态设计模式概念、对比flowable等流程引擎中的流转概念