- 聚合的目的是确保不变规则。下面研究如何为聚合实现不变规则。仓库(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);
}
@Override
public String toString() {
return this.description;
}
}
- 员工
@Getter
public class Emp extends AuditableEntity {
//others
private 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;
@Autowired
public EmpService(EmpRepository empRepository
, EmpAssembler assembler) {
this.empRepository = empRepository;
this.assembler = assembler;
}
@Transactional
public 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;
@Autowired
public 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());
}
// 将领域对象转换成 DTO
EmpResponse 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;
@Autowired
public EmpRepositoryJdbc(JdbcTemplate jdbc) {
this.jdbc = jdbc;
this.empInsert = new SimpleJdbcInsert(jdbc)
.withTableName("emp")
.usingGeneratedKeyColumns("id");
// 初始化其他几个 SimpleJdbcInsrt ...
}
@Override
public 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)属性。