我正在努力寻找合适的DDD方式来建立父母/孩子oneToMany关系,即:
- 确保实体不能以无效状态存在
- 不暴露不需要的方法(即干净的API)
我使用的是PHP和Doctrine2,但我想这也适用于许多其他语言/平台。这是我的基本实体代码。我有Parent
和Child
对象。没有父级,Child
就不可能存在。
/**
* @ORM'Entity
*/
class ParentClass
{
/**
* @ORM'OneToMany(targetEntity="Child", mappedBy="parent", orphanRemoval=true, cascade={"persist", "remove"})
*/
private $children;
}
/**
* @ORM'Entity
*/
class Child
{
/**
* @ORM'ManyToOne(targetEntity="Base", inversedBy="children")
* @ORM'JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE", nullable=false)
*/
private $parent;
}
但是,我应该如何创建和删除子实体?
为了增强一致性,我可以将父项作为Child:的构造函数参数
class ParentClass
{
public function addChild(Child $child)
{
$this->children[] = $child;
}
public function removeChild(Child $child)
{
$this->children->removeElement($child);
}
}
class Child
{
public function __construct(ParentClass $parent)
{
$this->parent = $parent;
$this->parent->addChild($this);
}
}
$parent = new ParentClass();
$child = new Child($parent);
问题是它暴露了addChild,现在开发人员真的不应该使用它。它需要大量的额外检查,以确保你不能在父母之间转移孩子。
作为替代方案,我可以使用setter:
class ParentClass
{
public function addChild(Child $child)
{
$child->setParent($this);
$this->children[] = $child;
}
public function removeChild(Child $child)
{
$this->children->removeElement($child);
}
}
class Child
{
public function setParent(ParentClass $parent)
{
$this->parent = $parent;
}
}
$parent = new ParentClass();
$parent->addChild(new Child());
这里的问题是,在您调用addChild之前,Child将处于无效状态。
第三种选择是让addChild创建一个新的子级:
class ParentClass
{
public function addChild()
{
$child = new Child($parent);
$this->children[] = $child;
return $child;
}
public function removeChild(Child $child)
{
$this->children->removeElement($child);
}
}
class Child
{
public function __construct(ParentClass $parent)
{
$this->parent = $parent;
}
}
$parent = new ParentClass();
$child = $parent->addChild();
这样做的问题是子构造函数暴露给开发人员。此外,我的(Symfony)表单库可能会讨厌我,导致我有一堆DTO和映射程序,只是为了一个简单的用例。
可能还有更多可能的方法来处理这个问题。确保干净域模型的首选方法是什么?
确保一个干净的域模型意味着忽略所有与数据库相关的内容,比如一对多关系。您的父/子问题是一种气味,暗示您正在使用数据库驱动的设计。
在域级别,聚合根(AR)充当"父",尽管该术语是错误的。聚合表示域概念,而AR负责确保其一致性。"儿童"是概念不可能存在的元素。您将始终使用AR与"孩子"一起工作,因为这是确保一致性的唯一方法。基本上,AR负责创建实际的"儿童"对象。
将AR视为容器是一种反模式在DDD中有,由定义,而不是包含的。几年前我写过一些关于它的帖子,但它们仍然有效。
Symfony表单库不应该讨厌你,因为这是一个UI问题,而不是域问题。您应该使用一个特定的视图模型/输入,该模型/输入将被发送到应用程序服务,应用程序服务将使用它来创建/更新域模型。如果您可以直接将域模型用于UI目的,那么您所拥有的可能只是一个不需要DDD的CRUD应用程序。