领域驱动设计——如何为公司和员工用例建模?

Domain Driven Design - How do I model a Company and Employee use case?

过去几周我第一次深入研究 DDD。我正在开发一个 HR 应用程序,并且真的在为聚合根的想法而苦苦挣扎。

所以当前的实体是:

现在考虑到 Employee 的生命周期,首先将其作为子实体包含在 Company 中是有意义的。可以将员工添加到公司(新员工或现有员工),然后该员工可以辞职或被解雇 - 此时员工记录可能会更新为某种状态,因此可以将其与活跃员工区分开来。

关于域逻辑,公司的每个员工都必须有一个唯一的电子邮件。因此,如果公司始终包含其所有员工的列表,我想可以将其建模为:

company.AddEmployee(employee) - 此方法将包含确保电子邮件唯一的逻辑。此外,它会根据公司的员工人数更新公司的规模 属性。 (<100 为小,<500 为中等)

现在我看到人们讨论的最大问题是关于大型聚合。我认为这个用例会受到这种关注,因为在这个 HR 应用程序中,一家公司可能拥有 10k+ 名员工。如果我一次添加一个员工,收集所有 10k+ 似乎真的很浪费,即使只是他们的电子邮件。

我将公司设为聚合根是否正确,还是有更好的方法?

您可以将 Company 保留为聚合根,但我会使用域事件来防止重复的电子邮件。

我的方法如下:

class Company
{
    List<Employee> Employees = new List<Employee>();
    public int EmployeeCount { get; private set; }

    public void AddEmployee(Employee employee)
    {
        Employees.Add(employee);

        EmployeeCount++;

        AddDomainEvent(new EmployeeCreated(employee));
    }
}
  1. 检索没有现有员工的公司。只需检索 EmployeeCount 属性.

  2. 调用 AddEmployee 方法:

    • 将员工添加到集合
    • 增加 EmployeeCount
    • 为新员工添加领域事件。
  3. 在提交工作单元之前,处理存储在工作单元中的实体上的所有领域事件。在这种情况下,这将是一个 EmployeeCreated 事件。

  • 该事件将包括新员工的建议电子邮件。
  • 如果建议的电子邮件地址已被使用,您的域事件处理程序可以进行 returns 标量 True/False 值的 dB 查询。
  • 如果已经使用,则抛出异常
  1. 如果没有抛出错误,则提交工作单元。

  2. 为了防止并发问题,在 Company 上添加一个并发令牌,这样我们就不会同时调用 AddEmployee 导致 DB 中有两个新员工,而只有一个 Employee 增量计数。

https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/domain-events-design-implementation#the-deferred-approach-to-raise-and-dispatch-events

我假设您正在开发的应用程序处理许多组织,否则就不需要单独的“公司”聚合。

聚合应保留其事务边界。当您建立这个边界时,总会有一个权衡。例如。您可以使整个系统成为一个聚合体。但是这样你就只能通过大量的锁定和等待严格顺序地处理所有请求。

或者您可以将 Company 作为一个聚合来处理 Employees。然后将按顺序处理给定公司的所有员工。这在大公司的规模上可能效果不佳。

或者您可以将 Company 聚合起来以处理 organisation-wide 事情。并使 Employee 成为一个集合来处理个人事务。在这种情况下,您将需要一些工作来检查 e-mail 是否唯一,或者其他一些属性是否正常(例如,向某些外部安全系统发出请求),并将此 Employee 更新为“已验证”或“有效”状态。

最后一种方法适用于高度并发的设置。

因此,作为系统设计者,您应该选择其中一种方法并接受 trade-offs。

聚合建模与 parent-child 层次结构关系不大。如前所述,它是关于事务边界的。此外,请考虑聚合提供 public API 以对您的域模型执行事务。

更容易抛开 parent-child 层次结构思维,首先也是性能方面的考虑。而是思考:

Are there use cases to perform transactions (changes) on this entity without the need to apply domain rules that can only be adhered by some encapsulating root entity? That means, can this entity contain all domain logic (business rules) on its own to perform transactions on it?

在您的案例中应用这种思维方式可能有以下推理:

There are use cases where I want to modify an Employee entity where it does not make sense for the Company to check business invariants. Like, the main phone number contact information of an employee must never be empty or have invalid format.

这当然是一个人为的例子,但它表明通过这种推理,这样的交易不需要业务不变量,而业务不变量宁愿位于 Company 实体中。按照这种逻辑,使 Employee 成为一个单独的聚合是有意义的。

此外,对我来说,从战术 DDD(或者基本上也是从 object-oriented 编程)中学习的一个关键关键是 将逻辑放在数据所在的位置。如果员工拥有执行保持事务一致性所需的逻辑的所有数据,而无需询问 公司,它本身就是一个很好的聚合候选者。

注意:当然,根据性能要求,将作为聚合一部分的实体单独聚合也是有意义的(如果child实体集合会变得太大)。但我不会根据性能要求开始建模聚合,而是问自己上面列出的问题。

现在进入检查 e-mail 唯一性 的主题。

它的数据在哪里?你可以说,如果 Company 知道所有员工,它也会知道到目前为止该公司员工的所有电子邮件。但是仅仅让 Employee 成为 Company 的 child 似乎并不是正确的理由。

如果您将 Employee 建模为自己的聚合,通常会有一个特殊的 域服务 维护员工聚合的集合,并提供检索和修改在您的业务领域中有意义的此类聚合的访问功能 - 聚合 存储库 .

这也是放置提问逻辑的好地方,是否已经有员工使用该电子邮件地址?因为存储库具有提供该逻辑的数据。

剩下的问题当然是 什么 code/component 应该而不是在存储库上调用此逻辑?。你可以,例如有一个特殊的 员工服务 来协调员工的创建。但视情况而定,也可能不合适。为了帮助做出此类决定,最好了解 DDD trilemma.

的含义

要获得有关此类决策的进一步提示,您可以在 Whosebug 上查看此 ,其中还讨论了类似问题的域服务。

如果“公司”的唯一职责是处理与“人力资源”相关的问题,那么“公司”聚合根就有意义。

关于“电子邮件唯一性”。这是一个应用层问题。如果你运行用笔和纸做生意,业务方不会关心两个员工是否有相同的电子邮件。