domain-driven-design – 当列表中存在不变量时定义聚合根

前端之家收集整理的这篇文章主要介绍了domain-driven-design – 当列表中存在不变量时定义聚合根前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。
我正在做一个家庭日托应用程序,并认为我会尝试使用DDD / CQRS / ES,但我遇到了很好地设计聚合的问题.域名可以很简单地描述:

>一个孩子报名参加
>孩子可以到达
>孩子可以离开

目标是跟踪访问的次数,生成发票,记录(例如午餐,伤害等),以防止访问.到目前为止,这些其他操作将是与系统最常见的交互,因为访问每天开始一次,但有趣的事情一直在发生.

我正在努力的不变量是:

>如果孩子已经在这里,他们就无法到达

据我所知,我有以下选择

1.单个聚合根子

创建一个Child聚合根,其中包含事件ChildEnrolled,ChildArrived和ChildLeft

这看起来很简单,但由于我希望每个其他事件与访问相关联,这意味着访问将是Child聚合的实体,每次我想添加注释或任何内容时,我都必须获取所有访问对于那个孩子,永远.似乎效率低下且相当无关紧要 – 孩子本身和其他每次访问都与孩子午餐时的情况无关.

2.儿童和访问的总体根源

Child会直接使用ChildEnrolled,而Visit会获取ChildArrived和ChildLeft.在这种情况下,我不知道如何维持不变量,除了让访问为此目的服务,我见过这是灰心丧气的.

是否有另一种方法来强制使用此设计的不变量?

这是一种虚假的不变量

我认为这是可能的,我应该防止多个人同时登录同一个孩子,或延迟意味着使用多次点击“登录”按钮.我认为这不是答案.

我错过了一些明显的东西

这似乎最有可能 – 这肯定不是一些特殊的雪花,这通常是如何处理的?我几乎找不到具有多个AR的示例,更不用说带有列表的示例了.

解决方法

骨料

你在谈论访问以及访问期间发生的事情,所以它似乎是一个重要的领域概念.
我想你也会有一个DayCareCenter,所有关心的孩子都在这里注册.

所以我会选择这个聚合根:

> DayCareCenter
>孩子
>参观

顺便说一句:我看到另一个不变量:
“孩子不能同时在多个日托中心”

“多次点击’登录’按钮”

如果每个命令都有一个唯一的id,它是为每次有意的尝试生成的 – 不是每次点击都产生的(无意的),你可以缓冲最后n个收到的命令id并忽略重复.

或者您的消息传递基础设施(服务总线)可以为您处理.

创建访问

由于您使用多个聚合,因此必须查询一些(可靠,一致)存储以查明是否满足不变量.
(或者如果很少发生冲突并且手动“取消”无效访问是合理的,那么最终一致的读取模型也会起作用……)

由于儿童只能进行一次当前访问,因此儿童只会存储关于上次开始访问的一些信息(事件).

每当开始新的访问时,都会查询任何先前访问的“真实来源”(写模型)并检查访问是否结束.

(另一个选择是访问只能通过Child聚合结束,再次存储一个“结束” – 在Child中的事件,但这对我来说感觉不太好……但这只是个人观点)

查询(验证)部分可以通过特殊服务完成,或者只是将存储库传递给方法并直接在那里查询 – 这次我使用第二个选项.

这是一些C#-ish脑编译的伪代码来表达我认为你可以处理它的方式:

  1. public class DayCareCenterId
  2. {
  3. public string Value { get; set; }
  4. }
  5. public class DayCareCenter
  6. {
  7. public DayCareCenter(DayCareCenterId id,string name)
  8. {
  9. RaiseEvent(new DayCareCenterCreated(id,name));
  10. }
  11. private void Apply(DayCareCenterCreated @event)
  12. {
  13. //...
  14. }
  15. }
  16.  
  17. public class VisitId
  18. {
  19. public string Value { get; set; }
  20. }
  21. public class Visit
  22. {
  23. public Visit(VisitId id,ChildId childId,DateTime start)
  24. {
  25. RaiseEvent(new VisitCreated(id,childId,start));
  26. }
  27. private void Apply(VisitCreated @event)
  28. {
  29. //...
  30. }
  31.  
  32. public void EndVisit()
  33. {
  34. RaiseEvent(new VisitEnded(id));
  35. }
  36. private void Apply(VisitEnded @event)
  37. {
  38. //...
  39. }
  40. }
  41.  
  42. public class ChildId
  43. {
  44. public string Value { get; set; }
  45. }
  46. public class Child
  47. {
  48. VisitId lastVisitId = null;
  49.  
  50. public Child(ChildId id,string name)
  51. {
  52. RaiseEvent(new ChildCreated(id,name));
  53. }
  54. private void Apply(ChildCreated @event)
  55. {
  56. //...
  57. }
  58.  
  59. public Visit VisitsDayCareCenter(DayCareCenterId centerId,IEventStore eventStore)
  60. {
  61. // check if child is stille visiting somewhere
  62. if (lastVisitId != null)
  63. {
  64. // query write-side (is more reliable than eventual consistent read-model)
  65. // ...but if you like pass in the read-model-repository for querying
  66. if (eventStore.OpenEventStream(lastVisitId.Value)
  67. .Events()
  68. .Any(x => x is VisitEnded) == false)
  69. throw new BusinessException("There is already an ongoning visit!");
  70. }
  71.  
  72. // no pending visit
  73. var visitId = VisitId.Generate();
  74. var visit = new Visit(visitId,this.id,DateTime.UtcNow);
  75.  
  76. RaiseEvent(ChildVisitedDayCenter(id,centerId,visitId));
  77.  
  78. return visit;
  79. }
  80. private void Apply(ChildVisitedDayCenter @event)
  81. {
  82. lastVisitId = @event.VisitId;
  83. }
  84. }
  85.  
  86. public class CommandHandler : Handles<ChildVisitsDayCareCenter>
  87. {
  88. // http://csharptest.net/1279/introducing-the-lurchtable-as-a-c-version-of-linkedhashmap/
  89. private static readonly LurchTable<string,int> lastKnownCommandIds = new LurchTable<string,bool>(LurchTableOrder.Access,1024);
  90.  
  91. public CommandHandler(IWriteSideRepository writeSideRepository,IEventStore eventStore)
  92. {
  93. this.writeSideRepository = writeSideRepository;
  94. this.eventStore = eventStore;
  95. }
  96.  
  97. public void Handle(ChildVisitsDayCareCenter command)
  98. {
  99. #region example command douplicates detection
  100.  
  101. if (lastKnownCommandIds.ContainsKey(command.CommandId))
  102. return; // already handled
  103. lastKnownCommandIds[command.CommandId] = true;
  104.  
  105. #endregion
  106.  
  107. // OK,now actual logic
  108.  
  109. Child child = writeSideRepository.GetByAggregateId<Child>(command.AggregateId);
  110.  
  111. // ... validate day-care-center-id ...
  112. // query write-side or read-side for that
  113.  
  114. // create a visit via the factory-method
  115. var visit = child.VisitsDayCareCenter(command.DayCareCenterId,eventStore);
  116. writeSideRepository.Save(visit);
  117. writeSideRepository.Save(child);
  118. }
  119. }

备注:

> RaiseEvent(…)立即在场景后面调用Apply(…)
> writeSideRepository.Save(…)实际上保存了事件
> LurchTable用作固定大小的MRU命令列表
>如果您感兴趣,可以为它提供服务,而不是传递整个事件存储

免责声明:我不是知名专家.这就是我接近它的方式.在这个答案中可能会损害一些模式.

猜你在找的HTML相关文章