DDD:本地实体身份应该包括 parent 吗?
DDD: should local Entity identity include the parent's?
在 DDD 中,Entities 具有 identity 的概念,它可以唯一标识每个实例,而不管所有其他属性。通常这个身份在实体所在的BC中必须是唯一的,但有一个例外。
有时我们需要创建聚合,这些聚合不仅由根实体和一些值 Objects 组成,而且具有一个或多个 child / 嵌套实体(据我所知称为 本地实体)。对于这种实体,身份只需要在本地是唯一的,即在聚合边界中是唯一的。
鉴于此,我们还要考虑以下事实:在 DDD 中对 has-a 关系进行建模有两种方式,具体取决于实际业务需求:单独聚合或聚合根 + child 个实体。
在第一种情况下,关系的“child”聚合引用了 parent 的标识,后者通常有一个工厂方法来创建和 return child:
的实例
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
是否仍然包含 parent Forum
的 ForumId
还是多余的,因为它只会存在于特定的 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;
}
所以拥有一个聚合的主要目的是对这个聚合进行原子性的任何更改。
聚合根包含内部的完整子实体,例如论坛将有一个线程集合。
由于线程已经在论坛中,因此将 ForumId 放入其中没有任何意义,因为负责保存它的存储库已经知道该 ID,因为我们将保存整个论坛而不是单个线程。
还想补充一点,论坛聚合似乎是一个巨大的聚合,这意味着您应该考虑一些权衡。
在 DDD 中,Entities 具有 identity 的概念,它可以唯一标识每个实例,而不管所有其他属性。通常这个身份在实体所在的BC中必须是唯一的,但有一个例外。
有时我们需要创建聚合,这些聚合不仅由根实体和一些值 Objects 组成,而且具有一个或多个 child / 嵌套实体(据我所知称为 本地实体)。对于这种实体,身份只需要在本地是唯一的,即在聚合边界中是唯一的。
鉴于此,我们还要考虑以下事实:在 DDD 中对 has-a 关系进行建模有两种方式,具体取决于实际业务需求:单独聚合或聚合根 + child 个实体。
在第一种情况下,关系的“child”聚合引用了 parent 的标识,后者通常有一个工厂方法来创建和 return child:
的实例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
是否仍然包含 parent Forum
的 ForumId
还是多余的,因为它只会存在于特定的 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;
}
所以拥有一个聚合的主要目的是对这个聚合进行原子性的任何更改。 聚合根包含内部的完整子实体,例如论坛将有一个线程集合。 由于线程已经在论坛中,因此将 ForumId 放入其中没有任何意义,因为负责保存它的存储库已经知道该 ID,因为我们将保存整个论坛而不是单个线程。
还想补充一点,论坛聚合似乎是一个巨大的聚合,这意味着您应该考虑一些权衡。