【问题标题】:DDD: should local Entity identity include the parent's?DDD:本地实体身份应该包括父母的身份吗?
【发布时间】:2021-04-07 18:43:38
【问题描述】:

在 DDD 中,Entities 具有 identity 的概念,它可以唯一地标识每个实例,而不管所有其他属性如何。通常这个身份在 Entity 所在的 BC 中必须是唯一的,但也有例外。

有时我们需要创建不仅由根实体和一些值对象创建的聚合,而且还有一个或多个子实体/嵌套实体(我理解为本地实体)。对于这种实体,身份只需在本地是唯一的,即在聚合边界中是唯一的。

鉴于此,我们还要考虑在 DDD 中建模 has-a 关系的两种方式,具体取决于实际业务需求:单独的聚合或聚合根 + 子实体。 p>

在第一种情况下,关系的“子”聚合具有对父级身份的引用,而父级通常有一个工厂方法来创建和返回子级的实例:

class ForumId extends ValueObject
{
  // let's say we have a random UUID here
  //  forum name is not a suitable identifier because it can be changed
}

// "parent" aggregate
class Forum extends AggregateRoot
{
  private ForumId _forumId;
  private string _name;

  method startNewThread(ThreadId threadId, string title): Thread
  {
    // make some checks, maybe the title is not appropriate for this forum
    //  and needs to be rejected

    ...

    // passing this forum's ID,
    return new Thread(this->_forumId, threadId, title)
  }
}

class ThreadId extends ValueObject
{
  // let's say we have a random UUID here
  //  thread title is not a suitable identifier because it can be changed
}

// "child" aggregate
class Thread extends AggregateRoot
{
  private ForumId _forumId;
  private ThreadID _threadId;
  private string _title;
}

如果我们考虑第二种情况,假设由于某些业务原因我们需要将Thread 作为Forum 的本地实体,那么识别它的正确方法是什么? Thread 是否应该仍然包含父 ForumForumId,或者它是多余的,因为它只会存在于特定的 Forum 中并且永远不会在外部访问?

哪种方式更好,更重要的是为什么?数据模型(即数据库级别)是否可以将决策导向一种或另一种方式,或者我们仍然应该按照良好的 DDD 设计忽略它?

class Forum extends AggregateRoot
{
  private ForumId _forumId;
  private string _name;
  private List<Thread> _threads;

  method startNewThread(string title): ThreadId
  {
    // or use and injected `ThreadIdentityService`'s `nextThreadId(ForumId)` method
    var threadId = this.generateNextLocalThreadId()
    var newThread = new Thread(/*this->_forumId, */ threadId, title)
    this._threads.append(newThread)
    return threadId
  }
}

// "child" aggregate - case 1
class Thread extends LocalEntity
{
  private ForumId _forumId;
  private ThreadID _threadId;
  private string _title;
}

// "child" aggregate - case 2
class Thread extends LocalEntity
{
  private ThreadID _threadId;
  private string _title;
}

【问题讨论】:

    标签: domain-driven-design entity local identity


    【解决方案1】:

    因此,拥有聚合的主要目的是对该聚合原子进行任何更改。 聚合根包含内部的完整子实体,例如论坛将有一个线程集合。 由于 Thread 已经在 Forum 中,因此将 ForumId 放入其中没有任何意义,因为负责保存它的存储库已经知道该 id,因为我们将保存整个论坛而不是单个线程。

    还想补充一点,论坛聚合似乎是一个巨大的聚合,这意味着您应该考虑一些权衡。

    【讨论】:

    • 是的,这正是我的想法,对我来说似乎是重复!顺便说一句,不要担心大聚合,因为论坛/线程只是说明我遇到的问题的一个例子。感谢您的快速回复!
    • 域事件呢?如果我们想让子类引发一个事件,比如说将它添加到一个私有集合中,我该如何构造这样一个事件?假设事件是ThreadRenamed,它包含线程的ID 和它的名称......但是没有论坛ID,我们无法完全识别这个事件,不是吗?所以这似乎暗示了一种从孩子那里访问父母ID的方法......或者孩子不应该引发自己的事件?但这似乎不对……
    • 由于入口点是聚合根,我将把这个逻辑放在聚合根本身,这样它也可以注册这个事件,因为为了保持原子性,没有人应该能够直接访问孩子实体。所以在这种情况下,我们会调用 Forum->renameThread(thread_id, new_thread_name)
    • 所以子/本地实体应该只有身份,但没有“事件管理”......但是如果对父级的操作导致在每个子级上执行方法怎么办?为孩子添加事件的逻辑现在将在这两种方法中重复......人为的例子,但我们可以关闭一个特定的线程,这会引发一个 ThreadClosed 事件,我们可以关闭论坛并拥有 ForumClosed 以及很多 TheadClosed事件,现在我们在 Forum::closeThreadForum::close 方法中都有 ::addDomainEvent(ThreadClosed)
    • 其实很好,如果在你的情况下它更适合从子实体引发事件,我不会把它作为强制性规则,我这样做是因为很容易我的情况。在您的中,您可以拥有聚合根拉事件方法,该方法可以迭代所有子实体以获取它们的域事件,或者您可以在对子实体执行操作后拉域事件并将其保存到事件集合中聚合根。不确定我是否解释得足够好:)
    猜你喜欢
    • 2021-04-10
    • 1970-01-01
    • 1970-01-01
    • 2014-02-10
    • 1970-01-01
    • 2012-05-09
    • 2021-10-04
    • 1970-01-01
    • 2011-06-28
    相关资源
    最近更新 更多