在列表中存在不变量时定义聚合根

时间:2015-06-13 03:43:22

标签: domain-driven-design cqrs event-sourcing aggregateroot

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

  • 孩子已注册
  • 孩子可以到达
  • 孩子可以离开

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

我挣扎的不变因素是:

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

据我所知,我有以下选择

1。单个聚合根Child

创建单个Child聚合根,其中包含事件ChildEnrolledChildArrivedChildLeft

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

2。 ChildVisit

的汇总根

Child仅来源ChildEnrolledVisit来源ChildArrivedChildLeft。在这种情况下,我不知道如何保持不变量,除了Visit为此目的而接受服务之外,我已经看到这是不鼓励的。

是否有其他方法可以强制使用此设计的不变量?

第3。这是一个虚假的不变

我认为这是可能的,我应该防止同时登录同一个孩子的多个人,或延迟,这意味着使用会打到'登录'按钮很多次。我不认为这就是答案。

4。我错过了一些明显的东西

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

4 个答案:

答案 0 :(得分:2)

聚集体

你在谈论VisitsVisit期间发生的事情,所以它似乎是一个重要的领域概念。 我想你也会有一个DayCareCenter,其中所有关注的Children都已注册。

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

  • DayCareCenter
  • 访问
顺便说一句:我看到另一个不变量:
"孩子不能同时在多个日托中心"

"点击'登录'按钮很多次"

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

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

创建访问

由于您正在使用多个聚合,因此您必须查询一些(可靠,一致)商店以确定是否满足不变量。
(或者如果很少发生碰撞而且#34;取消"手动无效的Visit是合理的,最终一致的读取模型也会起作用......)

由于Child只能有一个当前VisitChild只会存储有关上次启动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用作固定大小的命令符号MRU列表
  • 如果您有益于
  • ,您可以为其提供服务,而不是传递整个事件存储

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

答案 1 :(得分:0)

这听起来像&#34; here&#34;在你的不变量中#34;如果孩子已经在这里,他们就无法到达&#34;可能是聚合的想法。可能是LocationDayCareCenter。从那里开始,确保Child无法到达两次似乎微不足道,除非他们之前离开过。

当然,这个聚合会很长寿。然后,您可以考虑使用BusinessDay或类似的聚合来限制儿童到达和离开的原始数量。

只是一个想法。不一定是 解决这个问题的方法。

答案 2 :(得分:0)

我会尝试将设计基于现实,并研究如何在没有软件的情况下解决问题。

我的猜测是他们使用笔记本或打印清单,每天开始使用新表,写今天的日期,然后为每个孩子记录有关到达,午餐等的情况。孩子们入住夜晚的情况应该和问题是 - 在第1天检查并在第2天检查。

聚合根应该关注流程(在您的情况下,每天/每晚每个孩子的照顾)而不是参与的数据对象(访问,孩子,父母等)。

答案 3 :(得分:0)

  

我错过了一些明显的东西

这一个;虽然我会狡辩是否明显。

&#34;儿童&#34;可能不应该将其视为域模型中的聚合。它是存在于模型之外的实体。换句话说,你的模型不是&#34;记录和#34;对于这个实体。

  

我挣扎的不变因素是:

     

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

右。这是一场斗争,因为你的模特在孩子到达和离开时无法控制。当这些事情发生在其他领域(现实世界)时,它会跟踪。所以你的模型不应该拒绝这些事件。

Greg Young:
    The big mental leap in this kind of system is to realize that 
    you are not the book of record. In the warehouse example the 
    *warehouse* is the book of record. The job of the computer 
    system is to produce exception reports and estimates of what 
    is in the warehouse

想一想:公共汽车到达了。您卸下孩子,扫描他们的条形码,并将他们粘在游戏室。在一天结束时,您将反转该过程 - 在将代码加载到总线上时扫描它们的代码。当扫描仪试图检查一个从未登记的孩子时,孩子不会消失

你最合适,因为你无法防止这种&#34;不变违规&#34;,就是检测它。

跟踪此问题的一种方法是事件驱动的状态机。要使用的关键搜索词是&#34;流程管理员&#34;,但在较旧的讨论中,您会看到术语&#34; saga&#34;使用(MIS)。

粗略草图:您的事件处理程序正在侦听这些子事件。它使用子节点的id(它仍然是一个实体,而不是聚合)来查找正确的流程实例,并通知它事件。流程实例将事件与其自身状态进行比较,生成新事件以描述对其自身状态的更改,并发出它们(流程管理器实例可以从其自己的历史中重新补充)。

因此,当流程经理知道孩子在位置X签到,并且收到声称孩子在位置Y签到的事件时,它会记录一个QuantumChildDetected事件来跟踪意外事件。

更复杂的流程管理员也会对ChildEnrolled事件采取行动,以便您的员工知道将这些孩子放入隔离区而不是进入游戏室。

回到原来的问题:您需要考虑Visits是否是您的域模型中存在的聚合,或者是现实世界中发生的事情的日志。