使用DDD访问数据进行验证和确定默认值

4

假设有一个音乐学校的学生入学系统,需要在该系统中存在一种方法来输入新学生的数据,以便他们能够获得该学校的教学指导。我们可以假设学生可以通过网站访问此功能。

  • 入学流程的一部分是询问学生希望接受哪种乐器的指导。
  • 由于输入了关于她所选乐器的信息,系统会自动为其分配一个默认的指导老师,该老师负责该组乐器的教学。
  • 在完成入学申请之前,学生还可以更改分配给她所选乐器的其他老师。
鉴于这个描述,我遇到了一些困难,即如何管理可能教授特定乐器的教练列表,以选择新学生的默认教练,并在提交注册前验证所选教练时如何使用相同的数据。其他技术限制是,到目前为止,我一直在使用一种自我验证技术,与 Jimmy Nilsson 的书 应用领域驱动设计和模式 中介绍的技术非常相似,因此我不清楚在需要访问外部数据时如何继续遵循自我验证技术,因为我通常认为这超出了测试实体有效性的范围。
我知道的选项有:
  1. 将验证移出实体本身。也许可以将验证移入一组服务或每个实体一个单独的服务,该服务分析整个实体的当前状态并确定其是否有效,并提供领域事件或其他值对象以发出更多关于哪些验证规则已被破坏的见解。在这种情况下,我仍然有点担心服务如何获取有关教练的必要信息。
  2. 允许从试图执行此验证的必要实体访问教练存储库。
  3. 创建一个服务,允许按乐器类别访问教练列表。或者创建两个单独的服务,一个返回给定类别的教练列表中是否存在给定的教练,另一个返回给定类别的默认教练。
  4. 在我的聚合根(可能是学生或学生入学申请)中加载一组教练值对象,可由聚合根或包含在根中的实体用于验证。
在上述的前两种情况中,使用讲师存储库似乎过于复杂,因为我不需要访问代表讲师的聚合根,而是将讲师视为一个值对象,用于描述学生注册请求,并且让存储库返回值对象似乎会模糊存储库的职责。对于后两个选项,在选项4的情况下,允许从服务或工厂访问数据似乎是错误的,因为存储库难道不应该负责访问这样的数据吗?如果存储库是处理此逻辑的正确位置,那么存储或访问存储库的引用的适当位置在哪里?我已经确信,在组成模型的任何实体或值对象中直接访问存储库存在原因不可行,因此我想知道是否有可能在这种情况下放弃该假设。我还应该提到,我对DDD还很陌生,现在遇到了一些令人费解的时刻,并尝试不要束缚自己,因此,对于这个主题的任何有见地的意见都非常宝贵。
2个回答

4

将验证移出实体本身。

一个教练不应该知道所有其他教练的情况。针对教练组的验证不是特定教练的责任。

允许从试图执行此验证的必要实体访问教练存储库。

领域模型应该是持久性无关的。需要打破这种情况,表明您的模型存在缺陷。

创建一个服务,允许按仪器类别访问教师列表。

这些信息揭示了聚合根的缺乏 - 我会称其为InstrumentClass

将其引入您的模型将解决您的一些问题。InstrumentClass将保存教授特定乐器的可用教练。

您需要解决的下一件事是如何正确描述分配给班级的学生。不幸的是,我现在无法命名它(也许是Participation?)。但是该实体将用于InstrumentClass以确定哪些教练过于忙碌。


以下是我关于建模您的领域的“自由风格”(只是为了展示我所看到的):

using System;

public class Main{
  public Main(){
    var instructor = new Instructor();
    var instrument = new Instrument("saxaphone");
    var saxaphoneClass = new InstrumentClass(saxaphone,teacher);
    var me=new Person("Arnis");
    //here, from UI, I can see available classes, choose one
    //and choose according instructor who's assigned to it
    var request=me.RequestEnrollment(saxaphoneClass, instructor);
    saxaphoneClass.EnrollStudent(request);
  }
}
public class Person{
  public IList<EnrollmentRequest> EnrollmentRequests { get; private set; }
  public EnrollmentRequest RequestEnrollment
   (InstrumentClass instrumentClass,Instructor instructor){
    if (!instrumentClass.IsTeachedByInstructor(instructor))
      throw new Exception("Instructor does not teach this music instrument");
    var request=new EnrollmentRequest(this,instrumentClass,instructor);
    EnrollmentRequests.Add(request);
    return request;
  }
}
public class EnrollmentRequest{
  public Person Person{ get; private set; }
  public InstrumentClass InstrumentClass { get; private set; }
  public Instructor Instructor{ get; private set; }
}
public class InstrumentClass{
  public void EnrollStudent(EnrollmentRequest request){
    var instructor=request.Instructor;
    var student=new Student(request.Person);
    var studies=new Studies(this,student,instructor);
    //TODO: this directiveness isn't good
    //student/instructor should listen for class events themselves
    //and class should listen if by any reason instructor or student cannot
    //participate in studies
    student.EnrollInClass(studies);
    instructor.AssignStudent(studies);
    Studies.Add(studies);
  }
  public bool IsTeachedByInstructor(Instructor instructor){
    return Instructors.Contains(instructor);
  }
  public InstrumentClass
   (Instrument instrument, params Instructor[] instructors){
    Instrument=instrument; Instructors=instructors.ToList();
  }
  public IList<Instructor> Instructors{get;private set;}
  public IList<Studies> Studies { get; private set; }
  public Instrument Instrument { get; private set; }
}
public class Studies{
  public Student Student { get; private set; }
  public Instructor Instructor { get; private set; }
  public InstrumentClass InstrumentClass { get; private set; }
}
public class Student{
}
public class Instructor{
}
public class Instrument{
}

1
谢谢您验证了我的一些担忧。我会思考您所写的内容,并看看我们的建议是否能带领我找到一个合理的解决方案。 - jpierson
验证一组教师的有效性并不是某个特定教师的责任。我不认为我提到过是某个特定教师的责任,我在这部分模型中考虑的聚合根要么是学生,要么是学生注册请求。只是对于教师列表如何适应该模型,我还不太清楚。 - jpierson
@jpierson 学生和 EnrollmentRequest 听起来不错。但我仍然认为 InstrumentClass 是你要找的。 - Arnis Lapsa
这种设计的问题在于它假设在学生注册活动期间有关于教师的任何知识。如我上面所述,情况是学生注册时提供了乐器的详细信息,然后根据该乐器是否属于乐器类别自动分配一个教师。只有在此之后,学生才被允许更改预选教师为其选择的教师,并且在那时,新选择的教师可以再次基于所选乐器进行验证。 - jpierson
出于假设情况并没有明确说明,但我打算在这个例子中不涉及类等复杂的事情,假设这些可能是私有指令。我真正想要关注的是基于实际乐器选择来设置默认教练以及在报名请求中更改教练时验证教练的核心功能。感谢您宝贵的意见。 - jpierson

2
我的回答不涵盖详细的实现或代码,因为你似乎是在使用它作为了解DDD更多的练习。请记住,在大多数情况下,你不会第一次就把模型做对,需要演化模型。当你 "玩弄" 它时,某些严格的部分将变得更加灵活易变(如Eric的比喻所说的园艺手套)。随着你对领域有了新的认识,你会发现需要向模型中引入新的概念。使用 "简单的例子" 也有危险,例如可能缺乏深度。但是有时需要简单的例子来掌握DDD,而且幸运的是,我们也可以演化样例;)
我听过Eric Evans提到的一件事是,如果领域感觉不正确或者你无法表达模型中的某些东西,那么你可能会遗漏一个概念。自然地,如果你在领域中有了概念,你就可以 "感受到" 或者找到验证自然发生的地方。
在设置上下文之后,我的建议如下:
Enterprise Patterns and MDA有一些复杂的模式,但你可以从Inventory archetype的CapacityManager中获得指导。不幸的是,第278页上的模型在网上不可用,但请查看ServiceInventory archetype。
教练向学生销售他们的服务。(上次我检查过,教练有薪水:)。如果我将你的示例映射到Inventory archetype以获得想法,我将使用:
- Inventory - 仪器/课程列表 - ServiceType - 仪器/课程详情、开始结束等 - ServiceInventoryEntry - 仪器+可用位置(使用容量管理器) - ServiceInstance - 报名 - 上课地点(已预定,取消,完成) - CapacityManager(由ServiceInventoryEntry使用) - ReservationRequest - 报名请求
我认为添加的关键概念是CapacityManager:你可以调用ServiceInventoryEntry::getCourses()方法,它将使用CapacaityManager(服务或类)来显示/计算可用的教师或返回默认教师。因此,根据上下文多次调用它:默认或提出一系列可用位置/座位/教师。
使用这个模型,您应该能够找到自然的位置(何时何地)进行验证。来自《流畅的对象建模》的指导是将验证放在数据所在的位置。虽然不是硬性规定,但对象有正确的关注点和数据组合在一起的自然倾向。例如,容量管理器知道有关注册和仪器的信息。(来自MDA-CapacityManger:通过释放ServiceInstances管理容量利用率)
为了获得聚合根,请查看您要执行的交易/更改,以便确保它们强制执行不变式(规则)。在您的示例中,我会将ServiceType(Course)作为值对象,ServiceInventoryEntry和ReservationRequests作为聚合根。(取决于您希望采取多么复杂的规则)。您还可以根据MDA书籍将学生和教师添加为参与方。我倾向于使用存储库来获取我的聚合根,然后依赖于控制反转,如您引用的Jimmy的书中所述。
我喜欢MDA模式的原因是它让我想到了我或业务可能没有想象到的用例和模型概念。但是,要小心只对需要的内容进行建模,因为MDA模式可能很大甚至诱人。好处是它们被设计为模块化或“可降级”的。
因此,简而言之: - 您的聚合根应确保您的领域处于有效状态(规则/不变式) - 将验证放在数据所在的位置。您的模型将指导您。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接