DDD下如何构建正确类层次并维护业务不变量?
DDD聚合设计与业务不变量维护方案
问题背景
现有实体:Faculty(学院)、Course(课程)、Student(学生)、Teacher(教师),关联规则与业务不变量如下:
- 关联关系:学院包含课程,课程不能脱离学院和教师存在;一位教师可教授多门课程,一位学生可选修多门课程,一门课程可有多名学生。
- 业务不变量:
- 一门课程最多容纳30名学生
- 一位学生最多选修10门课程
- 一个学院最多包含20门课程
- 一位教师所教授的所有课程中的学生总数不能超过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聚合的持久化。
五、业务流程说明
- 创建课程:通过
Faculty.AddCourse方法传入教师ID,创建课程并发布课程分配事件。 - 学生选课:
Student.EnrollInCourse验证自身选课数量,发布选课请求事件。 - 处理选课请求:事件处理器通过
IFacultyRepository找到对应课程,调用Course.EnrollStudent验证课程学生数量,发布选课成功事件。 - 校验教师学生总数:选课成功事件处理器调用领域服务,查询教师所有课程的学生ID并去重统计,若超过100则抛出异常。
内容的提问来源于stack exchange,提问作者Darer
相关产品推荐
相关产品推荐

