You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

DDD下如何构建正确类层次并维护业务不变量?

DDD聚合设计与业务不变量维护方案

问题背景

现有实体:Faculty(学院)、Course(课程)、Student(学生)、Teacher(教师),关联规则与业务不变量如下:

  • 关联关系:学院包含课程,课程不能脱离学院和教师存在;一位教师可教授多门课程,一位学生可选修多门课程,一门课程可有多名学生。
  • 业务不变量:
    1. 一门课程最多容纳30名学生
    2. 一位学生最多选修10门课程
    3. 一个学院最多包含20门课程
    4. 一位教师所教授的所有课程中的学生总数不能超过100人(学生与教师通过课程间接关联)

原实现违反DDD聚合规范:教师持有课程引用(但课程属于学院聚合)、课程持有学生实体(跨聚合直接引用而非ID),需要调整方案以符合DDD原则并覆盖所有业务不变量。


调整方案

一、明确聚合根划分

按照DDD聚合设计的一致性边界原则,划分三个独立聚合:

  • Faculty聚合:聚合根为Faculty,包含Course实体(课程依赖学院,无法独立存在),负责维护学院与课程的关联及对应不变量。
  • Student聚合:聚合根为Student,包含CourseEnrollment值对象(记录学生选修的课程ID),维护学生选课数量限制。
  • Teacher聚合:聚合根为Teacher,仅维护自身基础信息,通过课程的TeacherId与课程关联,不直接持有课程引用。

二、各聚合内部不变量维护

1. Faculty聚合(Faculty + Course)

课程不再持有学生实体,改为存储学生ID集合,避免跨聚合直接引用;学院负责创建课程并维护自身课程数量限制,课程维护自身学生数量限制。

public class Faculty : Entity
{
    public Guid Id { get; private set; }
    public string Name { get; private set; }
    private readonly List<Course> _courses = new();
    public IReadOnlyCollection<Course> Courses => _courses.AsReadOnly();
    private const int MaxCourses = 20;

    public Faculty(string name)
    {
        Id = Guid.NewGuid();
        Name = name;
    }

    public Course AddCourse(string courseName, Guid teacherId)
    {
        if (_courses.Count >= MaxCourses)
            throw new InvalidOperationException("学院最多只能开设20门课程");

        var course = new Course(courseName, Id, teacherId);
        _courses.Add(course);
        // 发布课程分配给教师的领域事件(可选,用于后续关联校验)
        AddDomainEvent(new CourseAssignedToTeacherEvent(course.Id, teacherId));
        return course;
    }
}

public class Course : Entity
{
    public Guid Id { get; private set; }
    public string Name { get; private set; }
    public Guid FacultyId { get; private set; }
    public Guid TeacherId { get; private set; }
    private readonly List<Guid> _studentIds = new();
    public IReadOnlyCollection<Guid> StudentIds => _studentIds.AsReadOnly();
    private const int MaxStudents = 30;

    public Course(string name, Guid facultyId, Guid teacherId)
    {
        Id = Guid.NewGuid();
        Name = name;
        FacultyId = facultyId;
        TeacherId = teacherId;
    }

    public void EnrollStudent(Guid studentId)
    {
        if (_studentIds.Contains(studentId))
            throw new InvalidOperationException("学生已选修该课程");
        if (_studentIds.Count >= MaxStudents)
            throw new InvalidOperationException("课程最多只能容纳30名学生");

        _studentIds.Add(studentId);
        // 发布学生选课成功事件,触发教师学生总数校验
        AddDomainEvent(new StudentEnrolledInCourseEvent(Id, TeacherId, studentId));
    }
}

2. Student聚合

学生通过值对象CourseEnrollment存储选修的课程ID,维护自身选课数量限制,选课操作发布领域事件通知课程处理。

public class Student : Entity
{
    public Guid Id { get; private set; }
    public string Name { get; private set; }
    private readonly List<CourseEnrollment> _enrollments = new();
    public IReadOnlyCollection<CourseEnrollment> Enrollments => _enrollments.AsReadOnly();
    private const int MaxCourses = 10;

    public Student(string name)
    {
        Id = Guid.NewGuid();
        Name = name;
    }

    public void EnrollInCourse(Guid courseId)
    {
        if (_enrollments.Any(e => e.CourseId == courseId))
            throw new InvalidOperationException("学生已选修该课程");
        if (_enrollments.Count >= MaxCourses)
            throw new InvalidOperationException("学生最多只能选修10门课程");

        _enrollments.Add(new CourseEnrollment(courseId));
        // 发布学生选课请求事件,通知课程执行 enrollment 操作
        AddDomainEvent(new StudentRequestedEnrollmentEvent(Id, courseId));
    }
}

// 作为值对象,无需独立ID
public record CourseEnrollment(Guid CourseId);

3. Teacher聚合

教师仅维护自身基础信息,不持有课程引用,通过课程的TeacherId实现跨聚合关联。

public class Teacher : Entity
{
    public Guid Id { get; private set; }
    public string Name { get; private set; }

    public Teacher(string name)
    {
        Id = Guid.NewGuid();
        Name = name;
    }
}

三、跨聚合不变量处理(教师100名学生限制)

跨聚合的不变量无法在单个聚合内部维护,需通过领域服务结合领域事件实现:

1. 领域事件定义

// 学生选课成功事件
public class StudentEnrolledInCourseEvent : IDomainEvent
{
    public Guid CourseId { get; }
    public Guid TeacherId { get; }
    public Guid StudentId { get; }

    public StudentEnrolledInCourseEvent(Guid courseId, Guid teacherId, Guid studentId)
    {
        CourseId = courseId;
        TeacherId = teacherId;
        StudentId = studentId;
    }
}

2. 领域服务实现

领域服务协调多个聚合,通过仓储获取数据并验证跨聚合规则,这里依赖IFacultyRepository获取教师关联的所有课程:

public class TeacherStudentLimitService
{
    private readonly IFacultyRepository _facultyRepository;
    private const int MaxTotalStudents = 100;

    public TeacherStudentLimitService(IFacultyRepository facultyRepository)
    {
        _facultyRepository = facultyRepository;
    }

    public async Task ValidateTeacherStudentLimitAsync(Guid teacherId)
    {
        // 查询该教师教授的所有课程
        var teacherCourses = await _facultyRepository.GetCoursesByTeacherIdAsync(teacherId);
        // 收集所有选课学生ID并去重
        var uniqueStudentIds = teacherCourses.SelectMany(c => c.StudentIds).Distinct();
        
        if (uniqueStudentIds.Count() > MaxTotalStudents)
            throw new InvalidOperationException("教师所教授课程的学生总数不能超过100人");
    }
}

3. 领域事件处理器

在学生选课成功的事件处理器中调用领域服务完成校验:

public class StudentEnrolledInCourseEventHandler : IDomainEventHandler<StudentEnrolledInCourseEvent>
{
    private readonly TeacherStudentLimitService _limitService;

    public StudentEnrolledInCourseEventHandler(TeacherStudentLimitService limitService)
    {
        _limitService = limitService;
    }

    public async Task Handle(StudentEnrolledInCourseEvent notification, CancellationToken cancellationToken)
    {
        await _limitService.ValidateTeacherStudentLimitAsync(notification.TeacherId);
    }
}

四、仓储设计

仅为聚合根提供仓储,支持跨聚合查询:

  • IFacultyRepository:负责Faculty聚合的持久化,需提供GetCoursesByTeacherIdAsync方法,查询所有属于该教师的课程(通过数据库查询Course表中TeacherId匹配的记录)。
  • IStudentRepository:负责Student聚合的持久化。
  • ITeacherRepository:负责Teacher聚合的持久化。

五、业务流程说明

  1. 创建课程:通过Faculty.AddCourse方法传入教师ID,创建课程并发布课程分配事件。
  2. 学生选课:Student.EnrollInCourse验证自身选课数量,发布选课请求事件。
  3. 处理选课请求:事件处理器通过IFacultyRepository找到对应课程,调用Course.EnrollStudent验证课程学生数量,发布选课成功事件。
  4. 校验教师学生总数:选课成功事件处理器调用领域服务,查询教师所有课程的学生ID并去重统计,若超过100则抛出异常。

内容的提问来源于stack exchange,提问作者Darer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 15:29:51