DDD聚合和根聚合创建的有效建模

时间:2018-01-11 11:24:37

标签: domain-driven-design aggregate

我们正在开展一个新项目,我们热衷于应用DDD原则。该项目使用dotnet核心,EF核心为SQL Server提供持久性。

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

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

  • 我们有项目
  • 用户可以与项目
  • 相关联
  • 项目工作流程
  • 工作流程任务
  • 用户可针对任务
  • 发布评论
  • 用户可以更改任务的状态(正在进行,完成等)
  • 项目,相关的工作团队任务最初是从模板
  • 创建的

初始设计是一个大型集群聚合,项目是根聚合,包含 ProjectUsers 工作流的集合,工作流程包含任务

的集合

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

正确或错误地,我们的下一个修订是将评论从聚合中删除,并使用评论作为根形成新聚合。这样做的动机是,企业设想对每个任务提出了大量评论

由于每个评论任务相关,评论需要将外键保留回任务。但是,根据您只能通过其根引用另一个聚合的原则,这是不可能的。为了解决这个问题,我们将任务打破了另一个聚合。这也似乎满足了不同的人任务可能已完成的需要,并且再次减少了争用。

然后我们遇到了与任务工作流的引用相同的问题,任务属于我们创建新< strong>工作流将任务中的外键聚合回工作流

结果是: -

  • 项目汇总,仅包含分配给项目的用户列表
  • 工作流聚合,其中包含项目的外键
  • 任务聚合,其中包含项目
  • 的外键
  • 评论聚合,其中包含返回任务的外键

项目有一种方法可以创建工作流的新实例,允许我们设置外键。即略微简化的版本

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()中的方法,以编排单独对象到每个存储库的创建和持久性?
  • 是即使在正确的位置创建新工作流的方法吗?
  • 实体用于形成用于创建读取模型的查询(由导航属性支持)。也许关注的是对象结构受到我们如何在只读中呈现数据的影响

我们现在正处于圈子中,并且可以真正使用一些建议来指导我们。

2 个答案:

答案 0 :(得分:0)

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

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

另一种方法是暂时忘记持久性并专注于可能需要什么责任的事情。有责任我不是指保存/加载/搜索等技术问题,而是域定义的东西。就像创建任务,完成任务,添加评论等一样。这应该给你一个大纲,比如:

interface Task {
    ...
    void CompleteBy(User user);
    ...
}

interface Project {
    ...
    Workstream CreateWorkstreamFrom(Template template);
    ...
}

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

另一点:不要将模型直接绑定到ORM。 ORM是钝器,可能会迫使你做出糟糕的决定。您可以在域对象中使用 ORM,但不要将它们作为ORM的部分。这样,您可以以正确的方式执行您的域,并且不必害怕为特定功能加载太多。您可以为所有业务功能做正确的事情。

答案 1 :(得分:0)

  

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

这是一个非常好的迹象。

  

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

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

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

更长的答案:DDD基于Eric Evans的工作,Eric Evans正在描述一种在千禧年开始为他的Java项目工作的风格。

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

反应是引入一门学科;通过限制应用程序访问一些精心约束的门守护者(“聚合根”对象)来隔离应用程序中的数据。应用程序可以将对象引用保存到根对象,并可以向这些根对象发送消息,但是应用程序无法持有对api之后隐藏的对象的引用或直接发送消息。聚合。

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

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

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

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

{
    taskId: 67890,
    projectId: 12345
}
  

我们认为,逻辑上创建项目应该是创建项目,向项目添加一个或多个工作流,将任务添加到工作流,然后让EF处理持久化的对象结构。

     

可能关注的是对象结构受到我们如何在只读中呈现数据的影响

这里有一种气味,就是你在描述数据结构的关系。关系不像关系那样定义聚合。

  

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

使用与已发布聚合(理解使用)分开的草稿聚合(理解编辑)实际上是相当正常的。域驱动设计的部分要点是通过注意用例之间的隐式边界并使其明确来改进业务

可以使用域名服务从模板创建项目,但在一般情况下,我猜你应该“手工” - 从草稿中复制状态,然后发送使用该状态来创建项目;当发布和编辑同时发生时,它避免了混淆。