【问题标题】:DDD Effective modelling of aggregates and root aggregation creationDDD 聚合和根聚合创建的有效建模
【发布时间】:2018-01-11 11:24:37
【问题描述】:

我们正在启动一个新项目,并且热衷于应用 DDD 原则。该项目使用 dotnet core,EF core 为 SQL Server 提供持久性。

域的初始视图 我将使用任务跟踪器的示例来说明我们的问题和挑战,因为这将遵循类似的结构。

一开始我们了解以下内容:-

  • 我们有一个项目
  • 用户可以关联到项目
  • 一个项目工作流
  • 工作流任务
  • 用户可以针对任务发布评论
  • 用户能够更改任务的状态(进行中、完成等)
  • 一个项目,以及相关的工作组任务最初是从一个模板创建的

最初的设计是一个大型集群聚合,其中 Project 是根聚合,包含 ProjectUsersWorkstreams 的集合, Workstreams 有一系列 Tasks

这种方法显然会导致许多争用和性能问题,因为必须为该聚合中的任何更改加载整个 Project 聚合。

无论对错,我们的下一个修订是从聚合中拆分出Comments,并使用Comment 作为根形成一个新的聚合。这样做的动机是,企业设想每个任务都会收到大量评论

由于每个 Comment 都与 Task 相关,因此 Comment 需要将外键保留回 Task。但是,根据您只能通过其根引用另一个聚合的原则,这是不可能的。为了克服这个问题,我们将 Task 分解为另一个聚合。这似乎也满足了任务可以由不同的人完成并再次减少争用的需求。

然后我们遇到了同样的问题,从 TaskTask 所属的 Workstream 的引用导致我们创建一个新的 Task strong>Workstream 与 Task 中的外键聚合回 Workstream

结果是:-

  • Project 聚合仅包含分配给该项目的用户列表
  • Workstream 聚合,其中包含 Project 的外键
  • Task 聚合,其中包含 Project 的外键
  • Comments 聚合,其中包含返回 Task 的外键

Project 有一个创建Workstream 新实例的方法,允许我们设置外键。 IE。稍微简化的版本

public class Project()
{
    string _name { get; private set;}
    public Project(Name)
    {
         _name = Name;
    }
    public Workstream CreateWorkstream(string name)
    {
        return new Workstream(name, Id);
    }

    ....+ Methods for managing user assignment to the project
}

以类似的方式,Workstream 有一个方法来创建一个任务

public class Workstream()
{
    string _name { get; private set;}
    public int ProjectId { get; private set; }

    public Workstream(Name, Id)
    {
         _name = Name;
         _projectId = Id;
    }
    public Task CreateTask(string name)
    {
         return new Task(name, Id);
    }

    private readonly List<Task> _activities = new List<Task>();
    public IEnumerable<Task> Activities => _activities.AsReadOnly();
}
  • 添加了 Activities 属性,纯粹是为了在使用实体构建读取模型时支持导航。

团队对这种方法感到不舒服,感觉有些不对劲。主要问题是:-

  • 感觉创建项目逻辑上应该是创建项目,向项目添加一个或多个工作流,向工作流添加任务,然后让EF处理持久化该对象结构。
  • 令人不安的是,必须首先创建项目,并且开发人员需要确保它被持久化,以便获得一个 Id,以便在调用创建模板的方法时做好准备,该方法依赖于外来的 Id钥匙。是否可以将此责任推给域服务中的方法CreateProjectFromTemplate() 以协调各个存储库的单独对象的创建和持久性?
  • 即使在正确的位置创建新工作流的方法是否正确?
  • 实体用于形成用于创建读取模型的查询(由导航属性支持)。也许担心的是对象结构受到我们需要如何以只读方式呈现数据的影响

我们现在正处于兜圈子的地步,确实可以使用一些建议来给我们一些方向。

【问题讨论】:

标签: domain-driven-design aggregate


【解决方案1】:

团队对这种方法感到不舒服,感觉有些不对劲。

这是一个非常好的迹象。

但是,根据您只能通过其根引用另一个聚合的原则,这是不可能的。

你会想放弃这个想法,它会妨碍你。

简短的回答是标识符不是引用。保留另一个实体的标识符副本是可以的。

更长的答案:DDD 是基于 Eric Evans 的工作,他描述了一种在千禧年初为他在 java 项目中工作的风格。

他正在苦苦挣扎的是:如果允许应用程序对任意数据实体进行对象引用,那么域的行为最终会分散在整个代码库中。这增加了您了解该领域所需的工作量,并增加了进行(和测试!)更改的成本。

反应是引入一门学科;通过将应用程序的访问权限限制在一些仔细约束的守门人(“聚合根”对象)中,将数据与应用程序隔离开来。应用程序可以保存对根对象的对象引用,并且可以向这些根对象发送消息,但应用程序不能保存对隐藏在 api 后面的对象的引用或直接发送消息聚合。

相反,应用程序向根对象发送消息,然后根对象可以将消息转发给其自身聚合中的其他实体。

因此,如果我们想向某个 Project 内部的 Task 发送消息,我们需要一些机制来知道 要加载哪个项目,以便我们可以将消息发送到要发送的项目给任务的消息。

实际上,这意味着您需要一个可以获取 TaskId 并返回相应 ProjectId 的函数。

最简单的方法是将两个字段存储在一起

{
    taskId: 67890,
    projectId: 12345
}

感觉创建项目逻辑上应该是创建项目,向项目中添加一个或多个工作流,向工作流中添加任务,然后让EF处理持久化该对象结构。

也许担心的是对象结构受到我们需要如何以只读方式呈现数据的影响

这里有一种味道,那就是你在描述数据结构的关系。聚合不是由关系定义的,而是由变化定义的。

是否可以将此责任推给域服务 CreateProjectFromTemplate 中的方法

将草稿聚合(理解编辑)与已发布聚合(理解使用)分开是很正常的。领域驱动设计的部分要点是改进业务,通过注意用例之间的隐含边界并将它们明确化。

可以使用域服务从模板创建项目,但在常见情况下,我猜您应该“手动”完成——从草稿中复制状态,然后发送使用该状态来创建项目;当发布和编辑同时发生时,它可以避免混淆。

【讨论】:

    【解决方案2】:

    这里有一个不同的视角,可能会让你摆脱僵局。

    我觉得你在做数据建模而不是真正的领域建模。您关心的是一个将使用 ORM (EF) 直接持久化的关系模型,而不太关心实际的问题域。这就是为什么你担心项目会加载太多东西,或者哪些对象会持有外键。

    另一种方法是暂时忘记坚持,专注于什么事情可能需要什么责任。对于职责,我指的不是保存/加载/搜索之类的技术性内容,而是领域定义的内容。比如创建任务、完成任务、添加评论等。这应该会给你一个大纲,比如:

    interface Task {
        ...
        void CompleteBy(User user);
        ...
    }
    
    interface Project {
        ...
        Workstream CreateWorkstreamFrom(Template template);
        ...
    }
    

    另外,不要过多关注实体、值对象、聚合根。首先,以您和您的同事都满意的方式正确地代表您的业务。这是重要的部分。尝试与非技术人员讨论您的模型,看看您使用的语言是否合适,是否可以与之对话。您可以稍后决定哪些对象是实体或值对象,这部分纯粹是技术性的,不太重要。

    还有一点:不要将模型直接绑定到 ORM。 ORM 是钝器,可能会迫使您做出错误的决定。您可以在域对象中使用 ORM,但不要让它们成为ORM 的部分。通过这种方式,您可以以正确的方式处理您的域,而不必害怕为特定功能加载太多。您可以为所有业务功能做正确的事情。

    【讨论】:

    • 这很有意义。在我们可以针对一个任务有大量 cmets 的场景中。将评论拆分为单独的聚合是否有意义?如果是这样,这就引出了一个问题,即我们如何引用评论所属的任务,而不会将任务从项目/工作流/任务聚合中分离出来
    • 这些都是技术性的东西。 Comment 有什么隐藏的属性是无关紧要的,一个实现细节。它可以在内部具有指向不同其他对象的外键。它可以知道它的任务、项目或任何其他对象。重要的部分是它拥有的 API,它反映了域。
    • 你有最后一点的例子吗?我看到的每个 DDD 示例都只是用作聚合的 EF 模型。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-03-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-11-05
    • 2011-01-16
    相关资源
    最近更新 更多