如何避免贫血的领域模型
如何避免贫血的领域模型
原文:https://medium.com/hackernoon/how-to-avoid-anemic-domain-model-5e1c3e6fe4d0
以用户注册为例
我有意选择了一个广为流传且简单的领域,我猜每个人都很熟悉。我想展示如何用不同的观点看待它,用对象思维来思考。这篇文章可以看作是我在对某个领域建模时采取的一系列步骤。
首先,我们来谈谈表格
如今,表单被用作数据验证的工具,目的是让这些数据变得有用。我经常看到验证脱离了业务逻辑。在这种情况下,数据被视为被动的:首先,验证它,然后做这个,然后做那个。嗯,很棒,叫过程化编程。下一步是将验证逻辑,域概念固有的,转移到那些值对象和实体。这就是 DDD 教我们做的。在这种情况下,表单只是让用户修改一些数据的工具。但是在某些情况下,表单是一个成熟的领域概念,只是在某些方面看起来像 web 表单。
我的领域
在这里。
我的域名是一个注册用户。他们打开一个页面,看到一个注册表,填写它,然后提交。然后,通过填写表格时显示的电子邮件地址发送确认电子邮件。它包含一个带有秘密令牌的确认链接,因此如果用户遵循这个链接,就意味着他填写了一封真正属于他的电子邮件。所以点击这个链接就完成了注册过程:一个新用户出现在系统中。
不过,我并没有试图对这个领域的整体进行建模。只是个素描。
*语义网 我用语义网开始分解。它帮助我发现基本物体。此外,它直观地表现了正在发生的事情以及事物之间的联系。在这个例子中,这是相当琐碎的。*

Simple semantic net of user registration domain
*只有名词,没有“服务” 在画素描时,我牢记物体隐喻。实际上,这主要导致缺少服务类别。为什么?因为一个大卫·韦斯特的人性比喻。或者是艾伦凯的细胞隐喻。因为对象在 OOP 中的意义。对象就像聪明而独立的成年人,他们不需要被告知如何做他们应该做的事情。*
*理解领域规则 就像在识别服务边界中一样,我使用相同的技术。我一头扎进过去,看看某个特定领域当时是什么样子。*
所以我们来考虑一些纸质形式。可能是申请表或登记表,随便什么都行。在某个地方注册是什么感觉?你拿走了表格。它是用打字机打印的。你拿了一支笔。装满了。然后你把它还给一个秘书。他或她看了一下,可能马上就注意到了一些打字错误或其他错误,检查了你的护照,以确保你是你所说的那个人,然后把它留下来做进一步的验证,这可能需要几天的时间。然后你可以几天后回来看看你的状态如何。或者你可以收到一封有结果的信。
如何对一个领域建模
*几句如何建模领域规则 现在有什么不同,没有打字机,没有秘书的时候?表单仍然需要显示和验证。谁应该做那件事?长话短说,对象思维意味着一个形式本身应该。现在,我们正在对注册过程中固有的行为进行建模。它与打印机、任何验证器、其他“er”或“or”或“s 无关,现在它只是一个注册表、一个令牌和一封电子邮件。好的,还有一个带用户的访问者,只是碰巧以“or”和“er”结尾。但是,没有什么可以阻止这些类利用它们所需要的一切来实现它们的职责。此外,鼓励对象拥有所有必要的资源:数据库连接、外部资源、缓存资源。如果对一个班级来说太多了——没问题,装饰者是一个不错的选择。这个概念的一个例子是一个 ORM :你的对象不再被操纵,它们自己决定做什么。同样的情况到处都是。服务是反向的。他们不再对物体进行操作了。 因此,我们实体的职责可能如下所示。注册表单负责显示自身、验证和保存自身。邮件负责自己发送。为了全面了解情况,我们应该想出并阐述一组用户故事。*
*用户故事 哪些用户故事构成了注册的流程?以下情况如何:*
一个访问者请求一个表格并填写它。
我可以从这个故事中得出结论,一个形式应该有责任展示自己。
表单确保所有字段都有效。
验证过程需要更多的细节。还记得五十年前我写的关于注册是如何工作的吗?验证的过程包括一个秘书需要检查你是不是你所说的那个人,所以你向他或她出示了你的护照。如今,电子邮件就像护照一样。我们在电子邮件上发送一封信,访问者填写了该表格,他或她应该确认它,这个过程包括在一个术语“验证”中。因此,任何注册都不可避免地包含异步部分。那么尝试同步验证一切是没有意义的。我在同步验证中留下的是一些轻量级的检查,当违反这些检查时,永远不会让注册成功。这些支票是什么?例如,出生日期比今天或一年前要早。出生日期应该是真实的。或者电子邮件符合 RFC822 。因此,如果所有这些检查都成功通过,我说表单可以被接受。 有些异步检查可能是并行的,有些应该是顺序的。展望未来,我可以说这样的检查可以作为传奇来实现。每一个都需要自己的用户故事。这就是整个过程的图示:

Form validation ending with registration of a user
如果一个表单有效,它注册一个访问者(记住对象是活动的!),所以就成了用户。
所以这个故事卡揭示了另一个表单的责任:用户注册。
下一个详细的验证用户故事可以开始了。我不想在这里深究,只把自己局限在那些关于电子邮件确认的问题上。他们在这里:
如果同步验证可以,编写一封确认电子邮件并发送给访问者。

A interaction diagram for composing and sending a confirmation email
显然,一封电子邮件应该在表格的帮助下撰写,毕竟它是一个信息专家。但是一封电子邮件应该拥有能够自己发送的所有资源。
访问者通过电子邮件中的确认链接成为用户。

A interaction diagram for confirming an email address
在这里,表单将确认委托给令牌,因为它更清楚如何确认输入的令牌字符串。如果没问题,那么表单注册一个新用户。这是一个常见的流程,反映了从面向对象的角度来看的一个领域:一个实体创建另一个实体,等等。实体(即集合体)不会自己弹出。
*CRC 卡 随着用户故事的发现,我可以制作 CRC 卡。就算说 CRC 正方体(一个源于 CRC 卡的概念,我第一次遇到是在大卫·韦斯特的《客体思维》里),对我最管用的那一面,也是有责任的那一面。所以我遍历了所有涉及特定对象的用户故事,并收集了它的职责。它们表示我的对象可以提供的服务,并且它们形成了对象的契约。我不会深入研究 CRC 立方体的形式表示,让我们直接看代码。*
密码
好的,我看了一下我的职责,发现我的表单应该能够:
- 展示自己;
- 验证自身;
- 撰写确认电子邮件,作为验证的一部分;
- 确认电子邮件,也是验证的一部分;
- 注册用户。
所以这些是形成一个对象契约的候选者。
请记住,下面的代码只是上面提出的想法和原则的代表。它比伪代码多,但比生产代码少。
*显示表单 我希望我的注册表单只负责显示数据,把装饰留给其他人。我不认为让表单知道每一个表示细节是一个好主意,所以我希望来自特定类的协作。它可能如下所示:*
**// an entry point. It might also be a controller action.* **public function** display()
{
(**new** RegistrationForm(
**new** UUID(),
**new** RegistrationFormDataStorage()
))
->display()
;
}*
和表单方法本身。这种精神可以在中实现。
***class** RegistrationForm
{
*/**
** ***@var*** *$id UUID
*/* **private $id**;
*/**
** ***@var*** *$storage DataStorage
*/* **private $storage**;
**public function** __construct(UUID $id, DataStorage $storage)
{
$this->**id** = $id;
$this->**storage** = $storage;
}
**public function** display()
{
(**new** RegistrationWebForm(
**new** HiddenElement(
$this->**id**->value()
),
**new** NameElement(),
**new** PassportElement(),
**new** EmailElement()
))
->display();
}
}*
受理表单
***public function** accept(**array** $data)
{
**try** {
(**new** RegistrationForm(
**new** FixedUUID($data[**'id'**]),
**new** RegistrationFormDataStorage()
))
->accept(
**new** Name($data[**'name'**]),
**new** Passport(
**new** PassportNumber($data[**'passport_number'**]),
**new** PassportIssuedAt($data[**'passport_issued_at'**]),
**new** PassportIssuedWhere($data[**'passport_issued_where'**])
),
**new** Email($data[**'email'**])
)
;
} **catch** (Exception $e) {
(**new** ErrorPage())->display();
}
(**new** RegistrationAcceptedPage())->display();
}*
同样,这也是入口点的样子。下面是registration form->accept()方法的实现:
***public function** accept(
IValidatableElement $name,
IValidatableElement $passport,
IValidatableElement $email
)
{
$this->**storage** ->transactionally(
**function** () **use** ($name, $passport, $email) {
$id =
$this->**storage** ->save(
[
**'name'** => $name->value(),
**'passport'** => $passport->value(),
**'email'** => $email->value(),
]
)
;
$this->**storage**->appendFormAcceptedEvent($id);
}
);
}*
注意数据库中事务性存储的表单接受的事件。我不想在一次运行中执行所有的逻辑,因为我不想滥用 DDD 的“一个集合—一个事务”的经验法则。我有一个单独的处理程序来处理那个事件;它构成了一封确认电子邮件。
撰写确认邮件
***public function** composeConfirmationEmail(UUID $formId)
{
(**new** RegistrationForm(
$formId,
**new** RegistrationFormDataStorage()
))
->composeConfirmationEmail(
**new** SmtpClient(),
**new** EmailConfirmationEmailHeader(),
**new** EmailConfirmationEmailBody(),
(**new** ConfirmationTokens(
**new** TokenStorage()
))
->add(
**new** ConfirmationToken(
**new** UUID(),
**new** TokenStorage(),
**new** UUID() // it's a token string
)
)
)
->send();
}*
下面是一个对象持久化的变体。我使用了一个类似集合的类 ConfirmationTokens ,它可以用于添加和搜索令牌。这里有一个非常简单的 registration form->composeConfirmationEmail()实现:
***public function** composeConfirmationEmail(
ICanSendEmails $transport,
EmailHeader $subject,
EmailBody $body,
ConfirmationToken $token
)
{
**return** (**new** ConfirmationEmail(
$transport,
$this->**storage**->getById($this->**id**)[**'email_address'**],
$subject,
$body,
$token
))
->save();
}*
最后,这是用户确认电子邮件的入口点:
***public function** confirmEmail(UUID $formId, UUID $tokenValue)
{
(**new** RegistrationForm(
$formId,
**new** RegistrationFormDataStorage()
))
->confirmEmail(
(**new** ConfirmationTokens(
**new** TokenStorage()
))
->byValue($tokenValue)
);
}*
成功的确认导致新的正式用户的注册:
***public function** confirmEmail(ConfirmationToken $token)
{
**if** ($this->generatedToken()->confirm($token)) {
$data = $this->**storage**->getById($this->**id**);
(**new** User(
**new** UUID(),
**new** UserDataStorage()
))
->register(
**new** Name($data[**'name'**]),
**new** Passport(
**new** PassportNumber($data[**'passport_number'**]),
**new** PassportIssuedAt($data[**'passport_issued_at'**]),
**new** PassportIssuedWhere($data[**'passport_issued_where'**])
),
**new** Email($data[**'email'**])
);
}
}*
包装它
因此,通过完全抛弃数据模型和对象应该持久化的方式,并专注于行为,我可以提出一个下降模型。