选择聚合根而不同时破坏不变量

Choosing aggregate roots without breaking invariants at the same time

我正在尝试将 DDD 应用于模型。 如何在不破坏我拥有的不变量的情况下将实体聚类成聚合体?

我有 4 个实体(简化):

public class Plan {
    public bool Completed;
    public DateTime StartDate; 
    public DateTime EndDate;
    public IList<Objective> Objectives;
}

public class Objective {
    public bool Completed;
    public IList<Person> Persons;
    public int TargetMeetingCount;
}

public class Person {
    public string Name; 
    public IList<Meeting> Meetings;
}

public class Meeting {
    public DateTime StartDate;
    public DateTime EndDate;
}

不变量:

到目前为止,这是我推理解决方案的方式:

如果计划是包含所有 Objective 的聚合根,则问题在于人员和会议太多。 我们不希望计划聚合中有这么多 object。使用 PlanRepository 计算和检索数据可能会非常慢。 此外,如果您想显示计划列表,则不想检索此数据。

那么选项就是我们也可以使 Objective 成为 AR 并分离 objectives,这将大大简化代码。 应用程序服务将为 UI layer assemble "PlanViewModel",通过使用 PlanRepository 和 ObjectiveRepository 并满足计划中的不变量。

然而,如果 Objectives 被分离,我们将打破如果所有 objectives 都完成则计划完成的不变量,因为域模型本身无法再验证这一点。所以 "PlanViewModel" 是正确的,但不是计划模型。

这里值得一提的是,会议 object 除了实际的日期范围外,还有更多属性,我们可以通过 AR 计划进行过滤,会议 object 还具有没有真正的目的,因为 objective 的实际完成状态将由 SQL 查询计算。

不确定我是否走错了路。但我觉得最终一致性可以在这里使用,但我不太确定这将如何应用。 但也许我在这里只是一个完全错误的轨道,我是 DDD 的新手,我希望这是有道理的。

我经常看到您为聚合选择的设计 - 过于关注 has a 关系以及通过组合获得的漂亮对象图的便利性。

If the Plan is an Aggregate Root with all Objectives, the problem is that as there are too many Persons and Meetings.

你是对的。这也是并发的灾难。您可能会遇到很多失败的交易,因为有人修改了 Plan 而其他人修改了 Person.

保持较小的聚合可以带来更好的并发性、速度和可扩展性等。

我可能会将 PlanObjective 视为聚合根。您可能想知道如何使它们之间的不变量保持一致?那么您必须在 事务一致性 最终一致性 之间做出选择。在这种情况下,您将使用最终一致性。

Objective 被标记为 completed 时,可以引发领域事件 ObjectiveCompleted。在应用程序层,您有一个监听事件的服务。事件侦听器可以:

使用PlanRepository查看是否完整

public function onObjectiveCompleted(ObjectiveCompleted $event)
{
    $planId = $event->getPlanId();
    $plan = $this->planRepository->find($planId);
    $isComplete = $this->planRepository->isComplete($planId)

    // Is the stored completed value consistent
    // with the newly computed value from the database?
    if (!$plan->isCompleted() && $isCompleted) {
        // They are not consistent
        $plan->markCompleted();
    }
}

PlanRepository 可以访问数据存储,因此它可以进行查询以确定 Plan 是否完整。

加载计划的所有 Objective 个聚合根

public function onObjectiveCompleted(ObjectiveCompleted $event)
{
    $planId = $event->getPlanId();

    $plan = $this->planRepository->find($planId);
    $objectives = $this->objectiveRepository->findByPlan($planId);

    $plan->determineIfCompleted($objectives);
}

determineIfCompleted() 方法中,您只需循环遍历所有目标,检查它们是否已完成。如果是,那么您会将 Plancompleted 字段更新为 true。这也是非常容易进行单元测试的代码,这很棒。

您会尝试将方法命名为尽可能接近您的通用语言。当您和您的团队谈论 Plan 成为 completed 时,您可能会称其为 "updating the status"。在这种情况下调用方法 updateStatus()

结论

第一种方法将确定 Plan 是否完整的逻辑推送到数据存储中。这可能是也可能不是您想要的。

第二种方法在我喜欢的域中保持逻辑正确,但是当 Plan 有 1000 秒的 Objective 时它可能效率较低。

此外,在处理域模型时,不要关心 view/presentation 相关概念。领域模型用于解决业务问题。域模型和视图模型是分开的。在某些情况下,您可能会使用域中的存储库来获取视图的数据(为了方便),但通常您只是直接对数据存储进行操作。这样效率更高,并为您提供更大的灵活性,因为您可以执行非常复杂的查询 return 专门针对给定视图的数据(网络 page/HTTP API/desktop 应用程序等)。

我不会为您设计所有聚合,因为我整晚都待在这里并且对您的域了解不多,无法这样做。正确的聚合设计可能是 DDD 中最困难的部分之一。慢慢来,把事情做好。它将在长期 运行.

中得到回报

我认为需要考虑的一个重要问题是什么使某物与众不同。

例如,一个人可以参与超过 1 个 plan/objective? objective 可以加入超过 1 个计划吗?

如果一个人可以参与超过1个objective那么它就成为自己的聚合根。 (并且 objective 将包含 personId 的列表)

此外,实体是聚合的一部分这一事实并不意味着您必须始终主动将它们加载到内存中。您可以应用延迟加载机制,这样您一次只加载 1 个人,并且只在需要时加载。聚合根的目的是确保一致性并成为您的入口点。这并不意味着在数组数据类型中拥有所有 sub-objects 并使用某种语言。这些是实现细节,您不得不想知道要实现哪些问题 (functions/services)。 例如,您可能具有函数

AddMeeting (Objective o, Person p, Meeting m) 

这会为某个人增加一个会议 objective 但是你会不会有一个

IList<Person> GetALlPersons() 

按计划执行?如果没有,那么您就不必加载所有人。你可能只需要一个 GetPeopleWhoDidntCompleteTheirMeetingCount() 就可以逃脱,从而减少你实际需要加载的人数。