领域驱动设计(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)属性。