在域驱动设计实体和聚合根中处理和返回错误(非异常和异常)的方法是什么?

pro*_*mer 10 error-handling domain-driven-design exception-handling

我正在尝试找一篇关于DDD实体如何处理错误的好文章/示例(以及将被视为异常错误和什么不会错误)以及它们如何将它们传递到调用应用程序层(通常包含事务中的操作)那将需要回滚).

目前我正在考虑将所有会破坏聚合事务(例如验证)的错误视为异常.这样我就可以在"catch"块中回滚事务.例如:

SomeApplicationService:

// start transaction here
// ...

try 
{
    $user = $userRepository->userOfId($id);
    $user->doSomething();
    $user->doSomethingElse();  // <-- imagine an error thrown here
    $userRepository->save($user);
} 
catch (CustomFriendlyForUIException $e)
{
    // Custom Friendly for UI error
    // Rollback transaction and add error/message to UI payload DTO
    // ...
}
catch (AnotherCustomException $e)
{
    // Not friendly to UI, so use general error message for UI
    // Rollback transaction and add error/message to UI payload DTO
    // ...
}
catch (Exception $e)
{
    // Catch all other exceptions, use general error message for UI
    // Rollback transaction and add error/message to UI payload DTO
    // ...
}

// end transaction
Run Code Online (Sandbox Code Playgroud)

这是正确的方法,还是我错过了什么?

小智 7

通常,有两种类型的错误:

  • 具有商业意义的商业错误。例如,StockFullErrorProductNotAvailableError等。这些错误是预料之中的,因为它们是在代码中显式创建的。

    此类错误可以使用异常或使用函数方式来实现:在返回类型中显式显示错误。例如:Either<Error, T>

    异常使代码更简单。处理错误的函数式编程方式使代码更容易推理并且更可预测。我建议后者。

  • 基础设施错误是与数据库、网络、外部服务等相关的错误。这些错误通常是意外的。

发生错误。当错误发生时,您不希望应用程序处于不一致的状态。例如,您希望避免将一半的聚合存储在数据存储中。

聚合完整性

这里的关键问题是保持总体完整性。关键点是何时必须将聚合写入数据存储中。我可以看到两种情况:

持久化聚合的操作是原子的

一旦您的数据存储可以提供原子操作,就不必担心数据完整性,因为原子操作的结果要么失败,要么成功。不存在部分写入。

这意味着你可以让你的 http 层处理异常并返回 500 例如。

持久化聚合的操作不是原子的

有时,确保原子性是不可能的。

多个表中的一个聚合

如果您需要将聚合存储在多个表中,有一些解决方案:

  • 交易。将代码包围在事务中可能是一个解决方案。然而,交易也有一些缺点。如果计算时间太长或者这部分代码调用得太频繁,就会减慢应用程序的速度。
  • 工作单元是一种旧模式。这个想法是注册您想要执行的操作:添加、更新、删除等。最终,UOW 使用事务将更改应用到数据库。请参阅福勒的文章了解更多信息。

SAGA:有时不可能进行大笔交易

当您发送电子邮件并将某些内容保存在数据库中时,您无法将电子邮件包含在事务中。

在这种情况下,您可以使用 SAGA。这个想法是,有时,您无法通过一个大事务来强制执行原子性。然而,通常很容易进行几笔小额交易。

SAGA 的想法是将每一个事务与一个补偿事务相关联。例如,给定“交易”:发送​​电子邮件以确认购买了产品,补偿性“交易”可以发送一封电子邮件以使用优惠券道歉。

每笔交易都运行小交易。如果其中之一失败,则会运行补偿事务。最终,这能够获得原子性。