实现一个简易版的DDD脚手架,并给出落地的示例。
前言
在前面的《一文带你学习DDD,全是干货!》文章中,里面讲述了一个Demo,虽然有DDD的思想,但是感觉整体很乱,每一层都没有做好隔离,所以我参考小米内部的DDD脚手架,对这个Demo进行了重构,也就诞生了我这个版本,代码已经上传到GitHub中,大家可以自取:https://github.com/lml200701158/ddd-framework
git clone git@github.com:lml200701158/ddd-framework.git
项目介绍
- 主要是围绕用户、角色和两者的关系,构建权限分配领域模型。
- 采用DDD 4层架构,包括用户接口层、应用层、领域层和基础服务层。
- 数据通过VO、DTO、DO、PO转换,进行分层隔离。
- 采用SpringBoot + MyBatis Plus框架,存储用MySQL。
项目目录
项目划分为用户接口层、应用层、领域层和基础服务层,每一层的代码结构都非常清晰,包括每一层VO、DTO、DO、PO的数据定义。对于每一层的公共代码,比如常量、接口等,都抽离到ddd-common中。
./ddd-application // 应用层 ├── pom.xml └── src └── main └── java └── com └── ddd └── applicaiton ├── converter │ └── UserApplicationConverter.java // 类型转换器 └── impl └── AuthrizeApplicationServiceImpl.java // 业务逻辑 ./ddd-common ├── ddd-common // 通用类库 │ ├── pom.xml │ └── src │ └── main │ └── java │ └── com │ └── ddd │ └── common │ ├── exception // 异常 │ │ ├── ServiceException.java │ │ └── ValidationException.java │ ├── result // 返回结果集 │ │ ├── BaseResult.javar │ │ ├── Page.java │ │ ├── PageResult.java │ │ └── Result.java │ └── util // 通用工具 │ ├── GsonUtil.java │ └── ValidationUtil.java ├── ddd-common-application // 业务层通用模块 │ ├── pom.xml │ └── src │ └── main │ └── java │ └── com │ └── ddd │ └── applicaiton │ ├── dto // DTO │ │ ├── RoleInfoDTO.java │ │ └── UserRoleDTO.java │ └── servic // 业务接口 │ └── AuthrizeApplicationService.java ├── ddd-common-domain │ ├── pom.xml │ └── src │ └── main │ └── java │ └── com │ └── ddd │ └── domain │ ├── event // 领域事件 │ │ ├── BaseDomainEvent.java │ │ └── DomainEventPublisher.java │ └── service // 领域接口 │ └── AuthorizeDomainService.java └── ddd-common-infra ├── pom.xml └── src └── main └── java └── com └── ddd └── infra ├── domain // DO │ └── AuthorizeDO.java ├── dto │ ├── AddressDTO.java │ ├── RoleDTO.java │ ├── UnitDTO.java │ └── UserRoleDTO.java └── repository ├── UserRepository.java // 领域仓库 └── mybatis └── entity // PO ├── BaseUuidEntity.java ├── RolePO.java ├── UserPO.java └── UserRolePO.java ./ddd-domian // 领域层 ├── pom.xml └── src └── main └── java └── com └── ddd └── domain ├── event // 领域事件 │ ├── DomainEventPublisherImpl.java │ ├── UserCreateEvent.java │ ├── UserDeleteEvent.java │ └── UserUpdateEvent.java └── impl // 领域逻辑 └── AuthorizeDomainServiceImpl.java ./ddd-infra // 基础服务层 ├── pom.xml └── src └── main └── java └── com └── ddd └── infra ├── config │ └── InfraCoreConfig.java // 扫描Mapper文件 └── repository ├── converter │ └── UserConverter.java // 类型转换器 ├── impl │ └── UserRepositoryImpl.java └── mapper ├── RoleMapper.java ├── UserMapper.java └── UserRoleMapper.java ./ddd-interface ├── ddd-api // 用户接口层 │ ├── pom.xml │ └── src │ └── main │ ├── java │ │ └── com │ │ └── ddd │ │ └── api │ │ ├── DDDFrameworkApiApplication.java // 启动入口 │ │ ├── converter │ │ │ └── AuthorizeConverter.java // 类型转换器 │ │ ├── model │ │ │ ├── req // 入参 req │ │ │ │ ├── AuthorizeCreateReq.java │ │ │ │ └── AuthorizeUpdateReq.java │ │ │ └── vo // 输出 VO │ │ │ └── UserAuthorizeVO.java │ │ └── web // API │ │ └── AuthorizeController.java │ └── resources // 系统配置 │ ├── application.yml │ └── resources // Sql文件 │ └── init.sql └── ddd-task └── pom.xml ./pom.xml
项目解读
数据库
包括3张表,分别为用户、角色和用户角色表,一个用户可以拥有多个角色,一个角色可以分配给多个用户。
create table t_user ( id bigint auto_increment comment '主键' primary key, user_name varchar(64) null comment '用户名', password varchar(255) null comment '密码', real_name varchar(64) null comment '真实姓名', phone bigint null comment '手机号', province varchar(64) null comment '用户名', city varchar(64) null comment '用户名', county varchar(64) null comment '用户名', unit_id bigint null comment '单位id', unit_name varchar(64) null comment '单位名称', gmt_create datetime default CURRENT_TIMESTAMP not null comment '创建时间', gmt_modified datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '修改时间', deleted bigint default 0 not null comment '是否删除,非0为已删除' )comment '用户表' collate = utf8_bin; create table t_role ( id bigint auto_increment comment '主键' primary key, name varchar(256) not null comment '名称', code varchar(64) null comment '角色code', gmt_create datetime default CURRENT_TIMESTAMP not null comment '创建时间', gmt_modified datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '修改时间', deleted bigint default 0 not null comment '是否已删除' )comment '角色表' charset = utf8; create table t_user_role ( id bigint auto_increment comment '主键id' primary key, user_id bigint not null comment '用户id', role_id bigint not null comment '角色id', gmt_create datetime default CURRENT_TIMESTAMP not null comment '创建时间', gmt_modified datetime default CURRENT_TIMESTAMP not null comment '修改时间', deleted bigint default 0 not null comment '是否已删除' )comment '用户角色关联表' charset = utf8;
基础服务层
仓储(资源库)介于领域模型和数据模型之间,主要用于聚合的持久化和检索。它隔离了领域模型和数据模型,以便我们关注于领域模型而不需要考虑如何进行持久化。
比如保存用户,需要将用户和角色一起保存,也就是创建用户的同时,需要新建用户的角色权限,这个可以直接全部放到仓储中:
public AuthorizeDO save(AuthorizeDO user) { UserPO userPo = userConverter.toUserPo(user); if(Objects.isNull(user.getUserId())){ userMapper.insert(userPo); user.setUserId(userPo.getId()); } else { userMapper.updateById(userPo); userRoleMapper.delete(Wrappers.<UserRolePO>lambdaQuery() .eq(UserRolePO::getUserId, user.getUserId())); } List<UserRolePO> userRolePos = userConverter.toUserRolePo(user); userRolePos.forEach(userRoleMapper::insert); return this.query(user.getUserId()); }
仓储对外暴露的接口如下:
// 用户领域仓储 public interface UserRepository { // 删除 void delete(Long userId); // 查询 AuthorizeDO query(Long userId); // 保存 AuthorizeDO save(AuthorizeDO user); }
基础服务层不仅仅包括资源库,与第三方的调用,都需要放到该层,Demo中没有该示例,我们可以看一个小米内部具体的实际项目,他把第三方的调用放到了remote目录中:
领域层
聚合&聚合根
我们有用户和角色两个实体,可以将用户、角色和两者关系进行聚合,然后用户就是聚合根,聚合之后的属性,我们称之为“权限”。
对于地址Address,目前是作为字段属性存储到DB中,如果对地址无需进行检索,可以把地址作为“值对象”进行存储,即把地址序列化为Json存,存储到DB的一个字段中。
public class AuthorizeDO { // 用户ID private Long userId; // 用户名 private String userName; // 真实姓名 private String realName; // 手机号 private String phone; // 密码 private String password; // 用户单位 private UnitDTO unit; // 用户地址 private AddressDTO address; // 用户角色 private List<RoleDTO> roles; }
领域服务
Demo中的领域服务比较薄,通过单位ID后去获取单位名称,构建单位信息:
@Service public class AuthorizeDomainServiceImpl implements AuthorizeDomainService { @Override // 设置单位信息 public void associatedUnit(AuthorizeDO authorizeDO) { String unitName = "武汉小米";// TODO: 通过第三方获取 authorizeDO.getUnit().setUnitName(unitName); } }
我们其实可以把领域服务再进一步抽象,可以抽象出领域能力,通过这些领域能力去构建应用层逻辑,比如账号相关的领域能力可以包括授权领域能力、身份认证领域能力等,这样每个领域能力相对独立,就不会全部揉到一个文件中,下面是实际项目的领域层截图:
领域事件
领域事件 = 事件发布 + 事件存储 + 事件分发 + 事件处理。
这个Demo中,对领域事件的处理非常简单,还是一个应用内部的领域事件,就是每次执行一次具体的操作时,把行为记录下来。Demo中没有记录事件的库表,事件的分发还是同步的方式,所以Demo中的领域事件还不完善,后面我会再继续完善Demo中的领域事件,通过Java消息机制实现解耦,甚至可以借助消息队列,实现异步。
/** * 领域事件基类 * * @author louzai * @since 2021/11/22 */ @Getter @Setter @NoArgsConstructor public abstract class BaseDomainEvent<T> implements Serializable { private static final long serialVersionUID = 1465328245048581896L; /** * 发生时间 */ private LocalDateTime occurredOn; /** * 领域事件数据 */ private T data; public BaseDomainEvent(T data) { this.data = data; this.occurredOn = LocalDateTime.now(); } } /** * 用户新增领域事件 * * @author louzai * @since 2021/11/20 */ public class UserCreateEvent extends BaseDomainEvent<AuthorizeDO> { public UserCreateEvent(AuthorizeDO user) { super(user); } }
/** * 领域事件发布实现类 * * @author louzai * @since 2021/11/20 */ @Component @Slf4j public class DomainEventPublisherImpl implements DomainEventPublisher { @Autowired private ApplicationEventPublisher applicationEventPublisher; @Override public void publishEvent(BaseDomainEvent event) { log.debug("发布事件,event:{}", GsonUtil.gsonToString(event)); applicationEventPublisher.publishEvent(event); } }
应用层
应用层就非常好理解了,只负责简单的逻辑编排,比如创建用户授权:
@Transactional(rollbackFor = Exception.class) public void createUserAuthorize(UserRoleDTO userRoleDTO){ // DTO转为DO AuthorizeDO authorizeDO = userApplicationConverter.toAuthorizeDo(userRoleDTO); // 关联单位单位信息 authorizeDomainService.associatedUnit(authorizeDO); // 存储用户 AuthorizeDO saveAuthorizeDO = userRepository.save(authorizeDO); // 发布用户新建的领域事件 domainEventPublisher.publishEvent(new UserCreateEvent(saveAuthorizeDO)); }
查询用户授权信息:
@Override public UserRoleDTO queryUserAuthorize(Long userId) { // 查询用户授权领域数据 AuthorizeDO authorizeDO = userRepository.query(userId); if (Objects.isNull(authorizeDO)) { throw ValidationException.of("UserId is not exist.", null); } // DO转DTO return userApplicationConverter.toAuthorizeDTO(authorizeDO); }
细心的同学可以发现,我们应用层和领域层,通过DTO和DO进行数据转换。
用户接口层
最后就是提供API接口:
@GetMapping("/query") public Result<UserAuthorizeVO> query(@RequestParam("userId") Long userId){ UserRoleDTO userRoleDTO = authrizeApplicationService.queryUserAuthorize(userId); Result<UserAuthorizeVO> result = new Result<>(); result.setData(authorizeConverter.toVO(userRoleDTO)); result.setCode(BaseResult.CODE_SUCCESS); return result; } @PostMapping("/save") public Result<Object> create(@RequestBody AuthorizeCreateReq authorizeCreateReq){ authrizeApplicationService.createUserAuthorize(authorizeConverter.toDTO(authorizeCreateReq)); return Result.ok(BaseResult.INSERT_SUCCESS); }
数据的交互,包括入参、DTO和VO,都需要对数据进行转换。
项目运行
- 新建库表:通过文件"ddd-interface/ddd-api/src/main/resources/init.sql"新建库表。
- 修改SQL配置:修改"ddd-interface/ddd-api/src/main/resources/application.yml"的数据库配置。
- 启动服务:直接启动服务即可。
- 测试用例:
- 请求URL:http://127.0.0.1:8087/api/user/save
- Post body:{"userName":"louzai","realName":"楼","phone":13123676844,"password":"***","unitId":2,"province":"湖北省","city":"鄂州市","county":"葛店开发区","roles":[{"roleId":2}]}
结语
这段时间主要是学习DDD如何落地,也一直想写个DDD的Demo,感觉这次学习周期稍微有点长。下一篇文章会将之前文章的重点内容,包括近期对DDD的学习,以及一些自己的理解,再出一篇“理论到实践”相关的文章,算是对自己近一个多月学习的总结。