【问题标题】:Handling aggregate root处理聚合根
【发布时间】:2018-05-21 19:01:43
【问题描述】:

我是 DDD 的新手,所以我正在做一些练习以了解更多。我的课程 BC 具有以下规则:

  1. 必须先创建课程,然后他们才能创建一门课程的模块
  2. 每个模块都由用户上传作业完成
  3. 当用户完成所有模块后,课程将由用户完成

定义: 课程涵盖特定主题,由模块组成。例如,sap课程有10个模块,例如:模块1:它是什么?,模块2:如何使用它?...

在这之后,我意识到课程是模块的聚合根,因为模块已经完成我必须关闭用户的状态与课程。

模型将是:

public class Course : AggregateRoot
{
    private string title;
    private List<Module> modules;
}

但模块也是作业的聚合根,因为当用户上传他的作业时,模块必须关闭。这让我认为这种方法是错误的,因为在 DDD 中不可能嵌套聚合根。有人知道这是怎么回事吗?

[更新]

好的,现在我了解了工作的原理以及为什么将其拆分为公元前 2 年。然而我做了一些改变,我想到了一些问题。

-我将注册方法创建为静态,并将构造函数设置为私有。

-Course 必须是一个数组,因为一个学生可以有多个。

-我放了更多与课程和老师相关的参数。当然是老师和实体吧?

-我创建状态当然是在模块完成时更新它,这样我不必阅读所有模块就可以知道它。好吗?

-如何为每个模块传递更多信息,例如标题和描述?课程实体是如何创建所有模块的,对吗?

public class StudentEnrolment: AggregateRoot
{
    private StudentId studentId;
    private Course courses;

    private constructor(
        StudentId studentId, 
        Course course, 
       ){
        this.studentId= studentId;
        this.courses[] = course;
    }

    public statuc function enroll(
        StudentId studentId,
        CourseId courseId, 
        string courseTitle,
        string courseLink,
        string teacherId,
        string teacherName,
        List<Tuple<ModuleId, string>> modules) {
        teacher = new Teacher(...);
        courseStatus = new courseStatus();
        new course(courseTitle, courseLink, courseStatus, teacher);
        return new self(studentId, course);
    }

    public function void uploadModuleHomework(ModuleId moduleId, Homework homework){ 
        /* forward to course.uploadModuleHomework */ 
    }

    public boolean isCourseFinished(){ 
         /* forward to course.isFinished */ 
    }

    public List<Tuple<ModuleId, string>> getModules(){ 
         /* forward to course.getModules */ 
    }
}

【问题讨论】:

  • 你不能有嵌套的 AR,但你可以在 AR 中嵌套实体。
  • 好的,你的意思是这个模块是一个实体,它有另一个实体叫做作业。但是您如何看待这种方法?你会以不同的方式做吗?
  • 我会问业务专家什么是一致性要求。
  • 好吧,我已经解释过了。上面的文字有3条规则。
  • 向课程添加新模块时会发生什么?

标签: domain-driven-design aggregateroot


【解决方案1】:

有两个不同的子域(所以我们有两个有界上下文):

1.教师可以管理的课程和模块管理;这里CourseModule 可以是聚合根,course 可以保存对Modules ID 的引用(而不是实例!)。

public class Course: AggregateRoot
{
    private string title;
    private List<ModuleId> modules;
}

2.学生参与课程。这里有一个 StudentEnrolment 聚合根,它包含对来自其他 BC 的 CourseModule 的引用,但作为 Value 对象;它将学生参与建模为一门课程;在这个有界上下文中,有一个新的实体 Homework,用于跟踪学生的作业上传和课程参与状态。

public class StudentEnrolment: AggregateRoot
{
    private StudentId studentId;
    private Course course;
    private List<Homework> homeworks;

    // initialize a student enrolment as public constructor or make constructor private and use a static method
    // here is important to notice that only this AR creates its entities, it does not receive them as parameter
    public constructor(
        StudentId studentId, 
        Course course, 
        List<Module> modules){
        this.studentId = studentId;
        this.course = course;
        //build the the homeworks entity list based on the modules parameter
        //for each module create a Homework entity, that initially is not uploaded, like:
        this.homeworks  = modules.map(module => new Homework(module))
     }

    public function void uploadFileForHomework(ModuleId moduleId, string file){ 
        /* find the homework by module Id and upload file*/ 
    }

    public boolean isCourseFinished(){ 
         /*returns true if all homeworks are uploaded*/
         /*optimization: you could have a status that is updated when a homework's file is uploaded*/
    }

    public List<Tuple<ModuleId, string, boolean>> getHomeworks(){ 
         /* returns a list of readonly Homeworks, i.e. Tuple<ModuleId, string /*module title*/, boolean /*is uploaded*/> */
    }
}


public class Homework: Entity
{
    private Module module;
    private string file;
    public constructor(Module module){
        this.module = module;
    }

    public void upload(string file){ this.file = file;}

    public boolean isUploaded(){return (boolean)this.file;}

    public string getUploadedFile(){return this.file;}

    public ModuleId getModuleId(){return this.module.getId();}
}

public class Course: ValueObject
{
    private string title;
    private CourseId id;
    public constructor(id, title){...}
    public string getTitle(){return this.title;}
    public string getId(){return this.title;}
}

public class Module: ValueObject
{
    private string title;
    private string description;
    private ModuleId id;
    public constructor(id, title, description){...}
    public string getTitle(){return this.title;}
    public string getDescription(){return this.description;}
    public string getId(){return this.title;}
}

如果您需要查询 Enrollment 以获取作业,则不应返回作业列表,因为客户端代码会认为它可以直接调用 Homework.upload(file),这是不允许的(只有聚合根可以修改其内部实体)。相反,您可以返回 Tuple 或更好,您可以创建 Homework 类的不可变版本。

【讨论】:

  • @AgustinCastro 在我的回答中,StudentEnrolment 仅适用于课程(可以重命名为 StudentEnrolmentToACourse)。无需对所有标准学生的课程注册进行 AR 建模(无需保护业务不变量)
  • @AgustinCastro 我已经修改了我的答案以更好地模拟您的案例。
  • 我不明白两件事:第一,为什么你认为作业比模块最重要?对我来说,家庭作业必须在模块内。因为我要向学生展示所有课程,每个课程都有模块,每个模块都有作业。这是自然的流动,对吧?第二个:如果课程和模块都有 id,为什么课程和模块是 VO 而不是实体。我在想的一件事是,只保存每个实体(课程、模块...)的 ID 而不是保存标题、描述并不是更好。然后我们需要额外的信息,我可以打到其他 BC(课程和模块管理员)
  • @AgustinCastro 1. 在这个有界上下文 (BC2) 中,模块和课程是价值对象。在 BC1 中,它们是聚合体。这不是重要性的问题,而是分离的问题。一个 BC 中的聚合是其他 BC 中的值对象。在 BC2 中,只需要它们的 ID 和标题,它们也是不可变的。
  • @AgustinCastro 2. 他们有 ID 无关紧要;需要该 ID 才能与 BC1 中的聚合建立链接;如果你不需要它,你可以放弃它,但我认为你会这样做(即在 UI 中显示某种链接)。需要title 才能不前往 到BC1 并再次获取它,以更好地分离BC。正如你所说,你可以放弃它,但这样你有更好的resilience(如果BC1出现故障,即DB,它也不会降低BC2)。
猜你喜欢
  • 1970-01-01
  • 2021-10-31
  • 2021-07-02
  • 2021-09-09
  • 1970-01-01
  • 2014-10-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多