DDD中的例外


11

我正在学习DDD,并且正在考虑在某些情况下引发异常。我知道对象不能进入错误状态,因此这里的异常很好,但是在许多示例中,例如,如果我们尝试使用数据库中存在的电子邮件添加新用户,也会抛出异常。

public function doIt(UserData $userData)
{
    $user = $this->userRepository->byEmail($userData->email());
    if ($user) {
        throw new UserAlreadyExistsException();
    }

    $this->userRepository->add(
        new User(
            $userData->email(),
            $userData->password()
        )
    );
}

因此,如果存在使用此电子邮件的用户,那么我们可以在应用程序服务中捕获异常,但我们不应该使用try-catch块来控制应用程序的操作。

最好的方法是什么?


1
让域服务(处理程序)抛出异常很好。
安迪

Answers:


20

让我们从对问题空间的简短回顾开始:DDD的基本原则之一是将业务规则尽可能地靠近需要执行的地方。这是一个非常重要的概念,因为它使您的系统更具“凝聚力”。通常,将规则“向上”移动是贫血模型的标志。其中对象只是数据包,规则将与要执行的数据一起注入。

贫瘠的模型对于从DDD开始的开发人员来说可能很有意义。您将创建一个User模型,模型EmailMustBeUnqiueRule将注入必要的信息以验证电子邮件。简单。优雅。问题在于这种“种类”的思考本质上是程序性的。不是DDD。最终发生的事情是,您留下了一个模块,其中包含数十个经过Rules巧妙包装和封装的模块,但是它们完全缺少上下文,无法更改它们,因为不清楚何时何地执行它们。那有意义吗?这可能是不言而喻的,一个 EmailMustBeUnqiueRule将在创建的应用User,但怎么样UserIsInGoodStandingRule?慢慢但可以肯定的是,提取Rules脱离他们的上下文会使您拥有一个难以理解的系统(因此无法更改)。仅当实际处理/执行太冗长以致于您的模型开始失去焦点时,才应封装规则。

现在转到您的特定问题:使用Service/ CommandHandler引发问题Exception是您的业务逻辑开始泄漏(“向上”)到您的域之外。为什么您Service/ CommandHandler需要知道电子邮件必须是唯一的?应用程序服务层通常用于协调而非实现。如果我们ChangeEmail向您的系统添加方法/命令,就可以简单地说明其原因。现在,这两种方法/命令处理程序都需要包含唯一检查。这是开发人员可能会试图“提取”的地方EmailMustBeUniqueRule。如上所述,我们不想走那条路。

一些其他的知识紧缩可以使我们得到更多的DDD答案。电子邮件的唯一性是一个不变性,必须在一组User对象之间强制实施。您的域中是否存在一个表示“ User对象集合”的概念?我想您可能可以看到我要去的地方。

对于这种特殊情况(以及更多涉及在集合中强制执行不变式的情况),实现此逻辑的最佳位置将在您的中Repository。这特别方便,因为您Repository还“知道”执行这种验证所需的额外基础结构(数据存储)。在您的情况下,我会将此检查放在add方法中。这有道理吧?从概念上讲,正是这种方法真正User为您的系统添加了一个。数据存储是实现细节。


请问你是什么意思Moving rules "upwards" is generally a sign of an anemic model?向上意味着到应用层?还是领域模型?
Anyname Donotcare

1
“向上”表示朝向您的应用程序层(考虑分层体系结构)。我在上面提到了这一点,但是将规则向上移动的原因是贫乏模型的标志,因为它代表了数据和行为的分离,从而破坏了拥有核心域模型的整个目的。如果验证电子邮件的方式与发送电子邮件的方式不在单独的模块中,则您的Email模型必须是一袋数据,然后在发送之前检查其不变性。请参阅:softwareengineering.stackexchange.com/questions/372338/...
王方滑

2
将域逻辑移动到存储库是错误的。您应该通过在域层中聚合根来强制执行域规则。
Arash

1
@Arash Set验证非常棘手,并且通常无法与DDD配合使用。您可以选择尝试某个过程之前先验证某个过程是否可以运行(添加User),或者接受这种特定类型的不变式不会被整齐地封装在您的域中。前者代表数据和行为的分离,从而产生了程序范式。这不理想。验证应该数据一起存在,而不是围绕数据。此外,创建仅用于检查该规则的域服务有什么好处?表面上看,这项服务...
国王侧滑

1
@Arash将您的RepositoryUser和耦合在一起,因此该变量不变,因此无论如何都无法实现您追求的纯粹的愿景。存储库实现是域的一部分,因此可以赋予某些职责。
国王侧滑

0

您可以在应用程序的每一层中进行验证。在何处强加哪些规则取决于您的应用程序。

例如,实体实现代表业务规则的方法,而用例实现特定于您的应用程序的规则。

如果您要构建类似Gmail的电子邮件服务,则可以说“用户应具有唯一的电子邮件地址”是一项业务规则。

如果您为新的CRM系统构建注册流程,则此规则可能最好在用例中实施,因为该规则也可能是用例的一部分。此外,由于您可以在用例测试中轻松对存储库调用进行存根,因此它也可能更易于测试。

为了实现额外的安全性,您还可以在存储库中强制执行此规则。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.