当前位置: 首页 > news >正文

领域驱动设计(DDD)【18】之实现聚合的不变规则和持久化

文章目录

  • 实现不变规则
    • 第一种做法
    • 第二种做法
  • 聚合的持久化
  • 聚合修改所面临的问题
  • 标记领域对象的修改状态
  • 总结

  • 聚合的目的是确保不变规则。下面研究如何为聚合实现不变规则。仓库(Repository)是以聚合为单位进行持久化的

实现不变规则

第一种做法

规则编号模块规则描述举例影响的主要功能
R025组织管理试用期的员工才能被转正小王已经是正式员工,不能被转正转正员工
R026组织管理已经终止的员工不能再次终止-终止员工
R027组织管理同一技能,不能录入两次-添加员工,修改员工
R028组织管理工作经验的时间段不能重叠-添加员工,修改员工
  • 首先,两个规则都是业务规则,因此必须在领域层来实现。其次,由于聚合根(Emp),已拥有实现业务规则所需要的数据,所
    以直接在聚合根里实现业务规则,而不是领域服务里。
// 员工状态枚举类
public enum EmpStatus {PROBATION("试用期", 1),REGULAR("正式", 2),TERMINATED("离职", 3);private final String description; // 描述private final int code; // 状态码EmpStatus(String description, int code) {this.description = description;this.code = code;}// 获取描述public String getDescription() {return description;}// 获取状态码public int getCode() {return code;}// 根据状态码获取枚举实例public static EmpStatus getByCode(int code) {return EmpStatus.getByCode(code);}@Overridepublic String toString() {return this.description;}
}
  • 员工
@Getter
public class Emp extends AuditableEntity {//othersprivate EmpStatus empStatus;//转正void becomeRegular() {// 调用业务规则: 试用期的员工才能被转正onlyProbationCanBecomeRegular();empStatus = REGULAR;}//终止void terminate() {// 调用业务规则: 已经终止的员工不能再次终止shouldNotTerminateAgain();empStatus = EmpStatus.TERMINATED;}// 实现业务规则private void onlyProbationCanBecomeRegular() {if (empStatus != PROBATION) {throw new BusinessException("试用期员工才能转正!");}}private void shouldNotTerminateAgain() {if (empStatus ==EmpStatus.TERMINATED ) {throw new BusinessException("已经终止的员工不能再次终止!");}}
}
  • 关于技能和工作经验的不变规则
规则编号模块规则描述举例影响的主要功能
R027组织管理同一技能,不能录入两次-添加员工,修改员工
R028组织管理工作经验的时间段不能重叠-添加员工,修改员工

第二种做法

// application.orgmng.empservice;
@Service
public class EmpService {private final EmpRepository empRepository;private final EmpAssembler assembler;@Autowiredpublic EmpService(EmpRepository empRepository, EmpAssembler assembler) {this.empRepository = empRepository;this.assembler = assembler;}@Transactionalpublic EmpResponse addEmp(CreateEmpRequest request, User user) {Emp emp = assembler.fromCreateRequest(request, user);empRepository.save(emp);return assembler.toResponse(emp);}
}
// application.orgmng.empservice;
// imports...@Component
public class EmpAssembler {EmpHandler handler; // Emp的领域服务OrgValidator orgValidator;@Autowiredpublic EmpAssembler(EmpHandler handler, OrgValidator orgValidator) {this.handler = handler;this.orgValidator = orgValidator;}// 由 DTO 生成领域对象Emp fromCreateRequest(CreateEmpRequest request, User user) {//校验参数validateCreateRequest(request);// 生成员工号String empNum = handler.generateNum();Emp result = new Emp(request.getTenantId(), user.getId());result.setNum(empNum).setIdNum(request.getIdNum()).setDob(request.getDob()).setOrgId(request.getOrgId()).setGender(Gender.ofCode(request.getGenderCode()));request.getSkills().forEach(s -> result.addSkill(s.getSkillTypeId(), SkillLevel.ofCode(s.getLevelCode()), s.getDuration(), user.getId()));request.getExperiences().forEach(e -> result.addExperience(e.getStartDate(), e.getEndDate(), e.getCompany(), user.getId()));return result;}void validateCreateRequest(CreateEmpRequest request) {//业务规则:组织应该有效orgValidator.orgShouldValid(request.getTenantId(), request.getOrgId());}// 将领域对象转换成 DTOEmpResponse toResponse(Emp emp) {// ...}
}
  • Assembler 和上个迭代的 Builder 作用类似,都用来创建领域对象。assembler 用到在应用层定义的DTO(CreateEmpRequest),所以只能放在应用层,不能放到领域层,否则就会破坏层间依赖。

  • Assembler 位于应用层,入口参数可以是应用层定义的 DTO。使用 asembler 的优点是代码比较简洁;代价是,从理论上来说,有时领域逻辑可能稍有泄漏。对于“组织应该有效”这条业务规则,尽管规则的实现仍然在领域层,但却是从应用层调用的。

  • Assembler 的命名只是一种常见的习惯,目的是和领域层的工厂相区别。Assembler 中的逻辑也可以都写在应用服务(EmpService)里,从而取消单独的 assembler。不过,使用 assembler 可以避免庞大的应用服务类,使代码更加整洁。像 assembler 这样对 service 起辅助作用的类,一般统称为 Helper。
  • 工厂的参数不能是应用层定义的 DTO。这个规则可以推广到整个领域层。也就是领域层中所有对象,包括领域对象、领域服务、工厂、仓库,对外暴露的方法的输入和输出参数,都只能是领域对象、基本类型,或者领域层内部定义的 DTO。

聚合的持久化

  • DAO 是针对单个表的,而 Repository 是针对整个聚合的。
// adapter.driving.persistence.orgmng;
// imports ...@Repository
public class EmpRepositoryJdbc implements EmpRepository {final JdbcTemplate jdbc;// SimpleJdbcInsert 是 Spring JDBC 提供的插入数据表的机制final SimpleJdbcInsert empInsert;final SimpleJdbcInsert skillInsert;final SimpleJdbcInsert insertWorkExperience;final SimpleJdbcInsert empPostInsert;@Autowiredpublic EmpRepositoryJdbc(JdbcTemplate jdbc) {this.jdbc = jdbc;this.empInsert = new SimpleJdbcInsert(jdbc).withTableName("emp").usingGeneratedKeyColumns("id");// 初始化其他几个 SimpleJdbcInsrt ...}@Overridepublic void save(Emp emp) {insertEmp(emp);  // 插入 emp 表//插入 skill 表emp.getSkills().forEach(s ->insertSkill(s, emp.getId()));//插入 work_experience 表emp.getExperiences().forEach(e ->insertWorkExperience(e, emp.getId()));//插入 emp_post表emp.getEmpPosts().forEach(p ->insertEmpPost(p, emp.getId()));}private void insertEmp(Emp emp) {Map<String, Object> parms = Map.of("tenant_id", emp.getTenantId(), "org_id", emp.getOrgId(), "num", emp.getNum(), "id_num", emp.getIdNum(), "name", emp.getName(), "gender", emp.getGender().code(), "dob", emp.getDob(), "status", emp.getStatus().code(), "created_at", emp.getCreatedAt(), "created_by", emp.getCreatedBy());Number createdId = empInsert.executeAndReturnKey(parms);//通过反射为私有 id 属性赋值forceSet(emp, "id", createdId.longValue());}private void insertWorkExperience(WorkExperience experience, Long empId) {// 类似 insertEmp...}private void insertSkill(Skill skill, Long empId) {// 类似 insertEmp...}private void insertEmpPost(EmpPost empPost, Long empId) {// 类似 insertEmp...}// 其他方法 ...
}

聚合修改所面临的问题

  • 考虑 修改员工 的功能。对于把聚合作为整体保存到数据库而言,修改比添加要复杂一些。比如说有一个员工“张三”,出生日期是1990年1月1日。他在相应的emp表里有一条记录。张三有三条技能,分别是Java、Golang和“项目管理”。所以他在skill表里也有3条记录。
    在这里插入图片描述

  • 假如对张三这个员工聚合进行修改:张三的出生日期输入错了,现在要由1990年1月1日改为1985年1月1日;Java技能的年期由10年改为15年;删掉Golang技能;增加JavaScript技能。
    在这里插入图片描述

  • 从数据库的角度,员工表要 update 一条记录;技能表分别 update、 insert 和 delete 一条记录,还有一条记录不变。虽然对聚合整体而言是“修改”,但具体到聚合内部的各个对象和相应的数据表来说,却不一定都是 “update”。

标记领域对象的修改状态

  • 处理这种复杂情况,可以有不同的方法。用的方法是,在每个实体中增加一个“修改状态”,在程序中合适的地方把状态设置正确,然后在 EmpRepository 里根据状态进行相应的处理。
// common.framework.domain;
public enum ChangingStatus {NEW,            // 新增UNCHANGED,      // 不变UPDATED,        // 更改DELETED         // 删除
}

枚举表示了 4 种状态:

  • 新增:表示新建的对象,数据库还没有,需要向数据表插入记录。
  • 不变:表示从数据库里取出的对象,数据没有变化,因此不需要任何数据库操作。
  • 更改:表示从数据库里取出的对象,数据发生了变化,需要在数据表里更改记录。
  • 删除:表示从数据库里取出的对象,需要在数据表里删除记录。

总结

关于不变规则的实现,有两个要点需要注意。

  • 第一,如果规则的验证不需要访问数据库,那么首先应该考虑在领域对象里实现,而不是在领域服务里实现。
  • 第二,关于技能和工作经验的两条规则,必须从整个聚合层面才能验证,所以无法在Skill和WorkExperience两个类内部实现,只能在聚合根(Emp)里实现,这也是聚合存在的价值。

  • 在持久化方面,我们用仓库(EmpRepository)来把聚合保存到数据库,仓库是针对聚合整体的,而不是针对单独的表的。聚合和它的仓库有一一对应关系。此外,为了对修改过的聚合进行持久化,我实体增加了“修改状态”(ChangingStatus)属性。

相关文章:

  • 从0到100:房产中介小程序开发笔记(中)
  • day44/60
  • uniapp消息推送
  • Python搭建HTTP服务,如何用内网穿透快速远程访问?
  • 【策划所需编程知识】
  • 83、高级特性-自定义starter细节
  • IBW 2025: CertiK首席商务官出席,探讨AI与Web3融合带来的安全挑战
  • win7实现永恒之蓝ms17_010漏洞之445端口
  • Learning PostgresSQL读书笔记: 第9章 Partitioning
  • .小故事.
  • 腾讯云市场目前饱和度
  • 通达信 稳定盈利多维度趋势分析系统
  • Outlook总是提示登录微软,怎么办?
  • Google机器学习实践指南(逻辑回归损失函数)
  • 《P1471 方差》
  • C++11 <chrono> 库特性:从入门到精通
  • 深入解析RNN模型:应用、结构与构建实战
  • swift 对象转Json
  • ArcGIS Pro利用擦除工具,矢量要素消除另一矢量部分区域
  • QT 国际化 翻译 总结