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

发布于:2025-06-28 ⋅ 阅读:(19) ⋅ 点赞:(0)

  • 聚合的目的是确保不变规则。下面研究如何为聚合实现不变规则。仓库(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)属性。