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

DDD中多对多关系的不变量建模问题咨询

学校排课系统DDD多对多关系不变量建模问题

我正在学习DDD,在多对多关系中对不变量建模时遇到了困难。假设要建模一个学校排课系统,存在以下不变量:

  • Student不能重复选修同一门Course
  • Student最多只能选修5门Course
  • Course的选课学生数不能超过其最大容量

这是Course与Student之间的多对多关系,建模时难以同时满足这些不变量。从逻辑上看,Course和Student应作为独立的聚合根,同时Enrollment(选课记录)概念很重要,我将其设为Course聚合下的实体。

最初的代码实现

class Student {
  public id: number
  public age: number
  public email: string
}

class Course {
  public id: number
  public studentLimit: number
  public studentIds: List<number>

  public enrollStudent(studentId: number) {
    if (studentIds.length >= studentLimit || studentIds.find(id => id == studentId)) {
        throw new BadRequestException()
    }
    studentIds.push(studentId)
  }
}

该实现可满足不变量1和3,但为满足不变量2,需要在Student端跟踪选课数量。若在Student实体中维护该数据,需同时更新两个聚合,因此我考虑将逻辑提取到更高层级的聚合——EnrollmentBoard中:

改进方案代码实现

class EnrollmentBoard {
    private courseDetails: List<CourseDetails>
    private studentEnrollments: List<StudentEnrollments>

    public enrollStudentInCourse(studentId: number, courseId: number) {
        const course = courseDetails.find(c => c.courseId == courseId)
        const studentEnrollment = studentEnrollments.find(s => s.studentId === studentId)
        if (course?.canAddStudent() && studentEnrollment?.canEnrollInAnotherCourse()) {
          course.addStudent()
          studentEnrollment.enrollInCourse(courseId)
        }
    }
}

class CourseDetails {
  public courseId: number
  public studentLimit: number
  public studentCount: number
  
  public canAddStudent(): boolean {
    return this.studentLimit - this.studentCount > 0 
  }

  public addStudent(): void {
    this.studentCount += 1
  }
}

class StudentEnrollments {
  public studentId: number
  public courseIds: List<number>

  public canEnrollInAnotherCourse(): boolean {
    return this.courseIds.length < 5
  }

  public enrollInCourse(courseId: number): void {
    this.courseIds.push(courseId)
  }
}

此方案可解决不变量问题,但EnrollmentBoard聚合会变得非常庞大,对于整个大学的数万学生选课场景,内存加载压力大。我有两个疑问:

  1. 是否有无需加载大量数据即可强制执行这些不变量的方法?我曾考虑按学生专业/分类拆分EnrollmentBoard,是否可行?
  2. 同时存在两个Student模型是否合理:一个作为聚合根的Student,另一个是EnrollmentBoard中的StudentEnrollments?前者用于更新学生姓名等信息,后者用于通过EnrollmentBoard处理选课以确保不变量。

解答

1. 无需加载大量数据的不变量强制执行方案,以及拆分EnrollmentBoard的可行性

按专业拆分EnrollmentBoard是可行的,但更高效的方案是将Enrollment设为独立聚合根,搭配查询模型来快速校验约束,而非用大聚合统管所有数据。

具体实现思路:

  • 每个Enrollment聚合根对应一条选课记录,包含学生ID、课程ID、选课时间等核心字段,自身可校验"学生未重复选该课程"的不变量(通过聚合内的唯一性约束)。
  • 维护两个轻量的查询模型:
    • CourseEnrollmentStats:仅存储每个课程的courseId、studentLimit、currentEnrollCount,用于快速校验课程容量是否超标。
    • StudentEnrollmentStats:仅存储每个学生的studentId、enrolledCourseIds、enrollCount,用于快速校验学生选课数量是否达上限。
  • 选课流程:
    1. 先查询两个统计模型,校验三个不变量是否满足。
    2. 校验通过后,创建Enrollment聚合根并保存,同时发布StudentEnrolled领域事件。
    3. 异步监听事件,更新两个查询模型的统计数据。

这种方式无需加载大量聚合数据,同时通过事件异步保持查询模型的一致性。而拆分EnrollmentBoard虽然能减少单聚合数据量,但本质还是大聚合思路,灵活性和扩展性不如独立Enrollment聚合+查询模型的方案。

另外,并发场景下的竞态问题(比如同一学生同时选两门课导致超5门),可以通过数据库层面的约束兜底:给Enrollment表加(student_id, course_id)唯一约束,同时用数据库的统计查询(比如SELECT COUNT(*) FROM enrollment WHERE student_id = ?)来实时校验选课数量,确保最终一致性。

2. 同时存在两个Student模型的合理性

这种设计是合理的,本质是CQRS模式的实践:

  • 作为聚合根的Student是写模型,负责维护学生核心身份数据(姓名、年龄、邮箱)的变更,保证这类数据的事务一致性。
  • StudentEnrollments是读模型(或专用约束模型),仅关注与选课相关的数据,为选课场景提供高效的校验能力,不涉及学生基本信息的变更。

两者职责完全分离:写模型保证核心数据的完整性,读模型为特定业务场景提供高性能查询。只要通过领域事件同步两者的关键数据(比如学生ID变更时更新StudentEnrollments),这种设计完全符合DDD的单一职责和边界划分原则。

你可以进一步明确边界:Student聚合根只暴露身份相关的必要方法,StudentEnrollments作为独立的查询模型,由事件处理器自动维护,无需依赖Student聚合根的直接调用。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 03:55:58