gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-12-04 08:21:07 28 4
gpt4 key购买 nike

我正在做一个家庭日托应用程序,并认为我会尝试 DDD/CQRS/ES,但我在设计聚合方面遇到了问题。域可以非常简单地描述:

  • child 入学
  • child 可以到
  • child 可以离开

  • 目标是跟踪访问时间,生成发票,对访问进行记录(例如午餐吃了什么,受伤等)。到目前为止,这些其他操作将是与系统最常见的交互,因为访问每天开始一次,但有趣的事情一直在发生。

    我正在努力解决的不变量是:
  • 如果 child 已经在这里,则无法到达

  • 据我所知,我有以下选择

    1.单聚合根Child

    创建单个 Child聚合根,事件 ChildEnrolled , ChildArrivedChildLeft
    这看起来很简单,但由于我希望彼此的事件与访问相关联,这意味着访问将是 Child 的实体。汇总,每次我想添加注释或任何东西时,我都必须为那个 child 提供所有访问的来源,永远。似乎效率低下且相当不相关—— child 本身以及其他每次访问都与 child 午餐吃什么无关。

    2. 聚合根 ChildVisit
    Child将仅提供 ChildEnrolled , 和 Visit将来源 ChildArrivedChildLeft .在这种情况下,除了拥有 Visit 之外,我不知道如何维护不变量。为了这个目的而接受服务,我已经看到这是不鼓励的。

    有没有另一种方法来强制这种设计的不变量?

    3. 这是一个假不变式

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

    4. 我遗漏了一些明显的东西

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

    最佳答案

    聚合体

    你在谈论 Visits以及在此期间发生了什么 Visit ,所以它似乎是一个重要的领域概念。
    我想你也会有一个 DayCareCenter其中所有关心Children被录取。

    所以我会使用这个聚合根:

  • 日托中心
  • child
  • 访问

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

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

    如果每个命令都有一个为每个 生成的唯一 ID有意 尝试 - 不是每次点击都会产生(无意) ,您可以缓冲最后 n 个收到的命令 ID 并忽略重复项。

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

    创建访问

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

    Child只能有一个电流 Visit , Child仅存储有关上次启动的一些信息(事件) Visit .

    每当新 Visit应该开始,“真实来源”(写模型)被查询任何前面的 Visit并检查是否 Visit结束与否。

    (另一种选择是 Visit 可以 只有 通过 Child 聚合结束,再次在 Child 中存储“结束”事件,但这对我来说感觉不太好。 .但这只是个人意见)

    查询(验证)部分可以通过特殊服务完成,也可以通过将存储库传递给方法并直接在那里查询来完成 - 这次我选择了第二个选项。

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

    public class DayCareCenterId
    {
    public string Value { get; set; }
    }
    public class DayCareCenter
    {
    public DayCareCenter(DayCareCenterId id, string name)
    {
    RaiseEvent(new DayCareCenterCreated(id, name));
    }
    private void Apply(DayCareCenterCreated @event)
    {
    //...
    }
    }

    public class VisitId
    {
    public string Value { get; set; }
    }
    public class Visit
    {
    public Visit(VisitId id, ChildId childId, DateTime start)
    {
    RaiseEvent(new VisitCreated(id, childId, start));
    }
    private void Apply(VisitCreated @event)
    {
    //...
    }

    public void EndVisit()
    {
    RaiseEvent(new VisitEnded(id));
    }
    private void Apply(VisitEnded @event)
    {
    //...
    }
    }

    public class ChildId
    {
    public string Value { get; set; }
    }
    public class Child
    {
    VisitId lastVisitId = null;

    public Child(ChildId id, string name)
    {
    RaiseEvent(new ChildCreated(id, name));
    }
    private void Apply(ChildCreated @event)
    {
    //...
    }

    public Visit VisitsDayCareCenter(DayCareCenterId centerId, IEventStore eventStore)
    {
    // check if child is stille visiting somewhere
    if (lastVisitId != null)
    {
    // query write-side (is more reliable than eventual consistent read-model)
    // ...but if you like pass in the read-model-repository for querying
    if (eventStore.OpenEventStream(lastVisitId.Value)
    .Events()
    .Any(x => x is VisitEnded) == false)
    throw new BusinessException("There is already an ongoning visit!");
    }

    // no pending visit
    var visitId = VisitId.Generate();
    var visit = new Visit(visitId, this.id, DateTime.UtcNow);

    RaiseEvent(ChildVisitedDayCenter(id, centerId, visitId));

    return visit;
    }
    private void Apply(ChildVisitedDayCenter @event)
    {
    lastVisitId = @event.VisitId;
    }
    }

    public class CommandHandler : Handles<ChildVisitsDayCareCenter>
    {
    // http://csharptest.net/1279/introducing-the-lurchtable-as-a-c-version-of-linkedhashmap/
    private static readonly LurchTable<string, int> lastKnownCommandIds = new LurchTable<string, bool>(LurchTableOrder.Access, 1024);

    public CommandHandler(IWriteSideRepository writeSideRepository, IEventStore eventStore)
    {
    this.writeSideRepository = writeSideRepository;
    this.eventStore = eventStore;
    }

    public void Handle(ChildVisitsDayCareCenter command)
    {
    #region example command douplicates detection

    if (lastKnownCommandIds.ContainsKey(command.CommandId))
    return; // already handled
    lastKnownCommandIds[command.CommandId] = true;

    #endregion

    // OK, now actual logic

    Child child = writeSideRepository.GetByAggregateId<Child>(command.AggregateId);

    // ... validate day-care-center-id ...
    // query write-side or read-side for that

    // create a visit via the factory-method
    var visit = child.VisitsDayCareCenter(command.DayCareCenterId, eventStore);
    writeSideRepository.Save(visit);
    writeSideRepository.Save(child);
    }
    }

    评论:
  • RaiseEvent(...)电话Apply(...)瞬间幕后
  • writeSideRepository.Save(...)实际保存事件
  • LurchTable用作命令 ID 的固定大小的 MRU 列表
  • 如果您对您有益,您可以为它提供服务,而不是通过整个事件商店

  • 免责声明:
    我不是著名的专家。这就是我将如何处理它。
    在此回答过程中,某些模式可能会受到损害。 ;)

    关于domain-driven-design - 当列表中存在不变量时定义聚合根,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/30814832/

    28 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com