【问题标题】:How to model associations in DDD approach?如何在 DDD 方法中建模关联?
【发布时间】:2016-05-26 19:31:02
【问题描述】:

通过阅读 Eric Evans 和 Vaughn Vernon 的书籍,我正在逐步学习虚构业务领域的 DDD 方法,并尝试在我的项目中使用 PHP 来实现它(但在这里真的没关系)。

最近我阅读了很多关于应该由域定义的模型的 AggregateAggregateRootEntity 模式。而且,坦率地说,我不确定我是否理解所有定义,所以我决定在这里问我的问题。

首先,我想介绍我负责员工假期管理的(子)域,这样可以更轻松地回答我的问题。

最简单的情况是Employee 可以在许多Teams 中找到。当员工决定请几天假时,他必须发送一个HolidaysRequest,其中包含假期类型(如休息假期、请假照顾孩子等)、接受状态以及时间等元数据当他不打算出现在他的办公室时范围。当然HolidaysRequest 应该知道是哪个Employee 发送了HolidaysRequest。我还想查找Employee 发送的所有HolidaysRequest

我很确定DateRangeHolidayType 之类的东西是纯值对象。这对我来说很清楚。当我必须定义实体的边界时,问题就开始了。我可能有通过在实体中嵌套对象来定义关联的不良做法,所以请告诉我在这里找出责任的定义。

  1. 这里的实体是什么? Aggregate 应该是什么?AggregateRoot 的位置在哪里?
  2. 如何定义实体之间的关联?例如。一个Employee 可以属于多个Teams 或HolidaysRequestEmployee 创作并分配给另一个可以接受它的Employee。它们是否应该作为聚合来实现?

我为什么要问这些问题?因为几周前我在这里发布了一个问题,其中一个答案是考虑EmployeeTeams 之间的关系,它们应该在一个名为EmployeeInTeam 的聚合中,但我不确定我是否理解以适当的方式。

感谢您的建议。

【问题讨论】:

  • 员工的团队如何与假期请求相关?这里要保护的不变量是什么?例如,如果同一团队的其他员工请求的假期重叠,员工是否可以请求假期?等等。如果不知道要保护哪些不变量,就无法定义适当的 AR 边界。
  • @plalx 假设没有边界。假设可能有一天没有人在给定的一天工作。另一种情况:假设团队中至少有一个人必须在工作,而其他人则在度过甜蜜的假期。我唯一需要知道的是如何为这些关系建模。员工不能休息的情况是某种事件的责任不是吗?另一方面,提出此类请求的员工必须知道他不能发送假期请求,因为当天没有员工可用。你怎么看?
  • @plalx 要完成我的回答,该员工是已发送假期请求的作者,因此将有一些权限接受它的另一个 Employee 知道是谁发送了它。所以基本上HolidaysRequest由一些HolidaysRequestMetadata和Employee组成。
  • @KubaT 我看不出你怎么能真正从头到尾读完这两本书(尤其是 Vaughn Vernon 的那本书),但仍然会提出这些问题。 Plalx 是对的,您需要对您的领域进行一些领域分析和事务分析来推导出设计。它不是凭空出现的。
  • @guillaume31 真诚的,但是您在阅读我的问题时实际上失败了,因为我没有写我已经“从头到尾”阅读它们。实际上,我仍在阅读它们并使用 Vernon 的技术编写代码。可能这是我的错 - 我开始使用我不完全理解的东西:)无论如何,谢谢,我会阅读更多关于它的内容,也许答案会自行出现:)

标签: domain-driven-design aggregate aggregateroot


【解决方案1】:

DDD 的主要内容是把重点放在领域,这就是为什么它被称为领域驱动设计。

当您开始询问关系、聚合和实体时,甚至没有深入探索您的域的组成部分,您实际上是在寻找数据库建模而不是域。

拜托,我不是说你问错了问题,也不是批评他们,我认为你在学习中尝试实践并没有错。

我不是 DDD 专家,我和你一样在学习,但我会尽力提供帮助。

首先考虑假日管理可能出现的情况。当您对某事有不同的规则时,您可以从使用 strategies 开始(我说的是最终解决方案)。

构建一个漂亮且有意义的域非常困难(至少对我而言)。你写代码。测试一下。有洞察力,抛出你的代码并重写它。重构它。在软件的生命周期中,您应该关注领域,因此您应该始终改进它。

从编码开始(如域的草稿),看看它的样子。让我们锻炼一下。首先,为什么我们需要管理这些东西?我们试图解决什么问题?啊,有时员工会请假,我们想控制它。我们可能会批准或不批准,这取决于他们想要“假期”的原因,以及我们的团队状态如何。如果我们拒绝了,他们仍然回家,我们会迟到决定是解雇还是减薪。执行无处不在的语言,让我们用代码表达这个问题:

public interface IHolydayStrategy
{
    bool CanTakeDaysOff(HolydayRequest request);
}

public class TakeCareOfChildren : IHolydayStrategy
{
    public bool CanTakeDaysOff(HolydayRequest request)
    {
        return IsTotalDaysRequestedUnderLimit(request.Range.TotalDays());
    }

    public bool IsTotalDaysRequestedUnderLimit(int totalDays)
    {
        return totalDays < 3;
    }
}

public class InjuredEmployee : IHolydayStrategy
{
    public bool CanTakeDaysOff(HolydayRequest request)
    {
        return true;
    }
}

public class NeedsToRelax : IHolydayStrategy
{
    public bool CanTakeDaysOff(HolydayRequest request)
    {
        return IsCurrentPercentageOfWorkingEmployeesAcceptable(request.TeamRealSize, request.WorkingEmployees)
            || AreProjectsWithinDeadline(request.Projects);
    }

    private bool AreProjectsWithinDeadline(IEnumerable<Project> projects)
    {
        return !projects.Any(p => p.IsDeadlineExceeded());
    }

    private bool IsCurrentPercentageOfWorkingEmployeesAcceptable(int teamRealSize, int workingEmployees)
    {
        return workingEmployees / teamRealSize > 0.7d;
    }
}

public class Project
{
    public bool IsDeadlineExceeded()
    {
        throw new NotImplementedException();
    }
}

public class DateRange
{
    public DateTime Start { get; set; }
    public DateTime End { get; set; }

    public int TotalDays()
    {
        return End.Subtract(Start).Days;
    }

    public bool IsBetween(DateTime date)
    {
        return date > Start && date < End;
    }
}

public enum HolydayTypes
{
    TakeCareOfChildren,
    NeedToRelax,
    BankOfHours,
    Injured,
    NeedToVisitDoctor,
    WannaVisitDisney
}

public class HolydayRequest
{
    public IEnumerable<Project> Projects { get; internal set; }
    public DateRange Range { get; set; }
    public HolydayTypes Reason { get; set; }
    public int TeamRealSize { get; internal set; }
    public int WorkingEmployees { get; internal set; }
}

我是这样快速写下这篇文章的:

  • 可能会授予或不授予假期,具体取决于情况和 原因,让我们创建一个IHolydayStrategy
  • 创建了一个空的(无属性HolydayRequest 类。
  • 针对每种可能的原因,让我们制定不同的策略。
  • 如果是为了照顾孩子,他们可以请假 请求的总天数低于限制。
  • 如果原因是员工受伤,我们没有 允许请求以外的选择。
  • 如果原因是因为他们需要放松,我们检查是否有 可接受的工作员工百分比,或者项目是否在 截止日期。
  • 一旦我需要策略中的一些数据,我就使用CTRL + .HolydayRequest 中自动创建属性。

看看我什至不知道这些东西将如何存储/映射?我只是编写代码来解决问题,并获取解决问题所需的信息。

显然这不是最终域名,只是一个草稿。如果需要,我可能会拿走这段代码并重写,但还没有感觉。

人们可能认为创建 InjuredEmployee 类只是为了始终返回 true 是没有用的,但这里的重点是利用 无处不在的语言,使事情尽可能明确,任何人都会阅读并理解同一件事:“好吧,如果我们有受伤的员工,他们总是可以请假,不管团队的情况和他们需要多少天。”。 DDD 中的这一概念解决的问题之一是开发人员、产品所有者、领域专家和其他参与者之间对术语和规则的误解。

在此之后,我将开始使用模拟数据编写一些测试。我可能会重构代码。

这个“3”:

    public bool IsTotalDaysRequestedUnderLimit(int totalDays)
    {
        return totalDays < 3;
    }

还有这个“0.7d”:

    private bool IsCurrentPercentageOfWorkingEmployeesAcceptable(int teamRealSize, int workingEmployees)
    {
        return workingEmployees / teamRealSize > 0.7d;
    }

是规范,在我看来,它不应该存在于策略中。我们可能会应用规范模式来解耦。

在我们通过测试得到一个合理的初始解决方案之后,现在让我们考虑应该如何存储它。我们可能会在这里使用最终定义的类(例如 Team、Project、Employee)来由 ORM 映射。

一旦你开始编写你的域,你的实体之间就会出现关系,这就是为什么我通常根本不关心 ORM 将如何持久化我的域,以及什么是聚合点。

看看我是如何还没有创建 Employee 类的,尽管这听起来很重要。这就是为什么我们不应该从创建实体及其属性开始,因为这与创建表和字段完全相同。

您的 DDD 变成了数据库驱动设计,我们不希望这样。当然,最终我们会创建 Employee,但让我们一步一步来,只在需要时创建。不要试图立即开始建模所有内容,预测您将需要的所有实体。专注于您的问题,以及如何解决它。

关于您的问题,什么是实体,什么是聚合,我认为您不是在问它们的定义,而是考虑到您的领域,是否将 Employee 视为一个或另一个。一旦您的代码开始显示您的域,您最终会回答自己。当您开始开发 Application Layer 时,您就会知道这一点,它应该负责加载数据并委托给您的域。我的域逻辑需要什么数据,我从哪里开始查询。

我希望我帮助了某人。

【讨论】:

  • 感谢您的精彩回答!与此同时,在互联网上阅读关于聚合的这个和那个,尤其是在事件溯源的背景下。据我了解,你写的关于关联(或缺乏关联)的内容,我开始思考如何考虑事件之间的一致性。假设有一名员工在 12 月 31 日(今天是 6 月 21 日)请求休假。这是一个长期存在的事件。当员工的团队名称更改或员工更改职位时会发生什么?我应该保留对团队身份的引用以按需检索最新值吗?
  • @KubaT 完美的例子。这是我喜欢向领域专家提出的问题。我们将如何创建关联、聚合取决于他们对这种情况的理解。据我所知,在谈到事件溯源时,您永远不会删除 事物。他们会在那里,因此您的全新员工(属于新团队或已更改职位)可能不再有那个圣日。我也在学习这个概念,这对我来说有点新。如果您得出结论,请告诉我,如果我能以某种方式提供帮助,我很高兴。
  • ES 中的“不删除”的东西是显而易见的,但我宁愿考虑关联实体更改问题。看我的例子,当HolidaysRequest被放置并且它包含Employee数据时,当Employee发生变化时该怎么办?我无法在 EventStore 中搜索此具体 Employee 发生的所有事件。即使它像HolidaysWithEmployee 这样的聚合,我也不能仅仅因为员工数据发生了变化而对其进行更新。对吗?
  • @KubaT 你是对的。跟踪所有事件并更新它们不是域的责任。当员工改变职位时,域应该触发一个EmployeeChangedPosition 事件(可能是一个EventBus),然后EventHandlers 订阅了这个事件并且知道该做什么。您将拥有一个HolydayRequestHandler,它将实现用于处理EmployeeChangedPositionEmployeeFired 和其他事件的接口,并执行它必须做的任何事情。也许被解雇的员工应该驳回神圣日的请求,也许改变职位应该改变对他们老板的分析。
  • @KubaT 现在,关于更新数据库中的引用,这很奇怪。我真诚地会尝试使 ES 数据库与域不同。像往常一样处理域的数据库、更新实际数据以及为 EventSourcing 使用单独的数据库怎么样?如果您开始使用 ES 数据库读取数据,事情可能会变慢,我认为您实际上不需要域的 历史数据。 ES 允许您在任何状态下重新创建事物,您在域中根本不需要它。用户可能会在旧状态下使用“旧”数据执行命令(然后再次变为新数据)。
猜你喜欢
  • 2013-05-30
  • 1970-01-01
  • 1970-01-01
  • 2023-04-03
  • 2014-07-21
  • 1970-01-01
  • 2021-12-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多