领域驱动设计中的异常 - Michał


异常已经被引入来处理函数层面的错误。其目的是为了避免返回错误代码和消除返回类型的模糊性。异常的力量来自于它们通过堆栈向下传播的能力。你没有义务直接处理异常。它允许你将你的正常代码流与错误处理分开。

当函数对参数的假设被打破或者函数不能履行其承诺时,应该抛出一个异常。最简单的例子是除以0。我们必须向调用者发出信号,表示我们不能提供答案。这个异常构成了函数契约的一部分。在语言学中,这有时被称为预设失败。考虑一下法国国王是否是秃头的问题。没有法国国王,所以我们不能用是或不是来回答。任何一个答案都会产生误导。

异常是领域语言的一部分
把异常作为领域语言本身的一部分来考虑是很方便的。这是一种很好的交流方式。像OrderLimitExceeded这样的业务异常是很重要的,而且信息量很大。通过翻阅异常列表,你可以获得关于业务预期的基本信息。有必要建立一个一致的领域异常的层次结构。例如,如果你的订单集合可能会引发许多不同的异常,作为其API的一部分,所有这些异常都应该扩展为一个单一的异常,如OrderException。这使它更容易处理。事实上,控制异常流并不是一件容易的事,它需要大量的纪律性。否则,它迟早会失去控制。

异常的问题
不幸的是,异常并不理想。它们破坏了代码的自然控制流。它是变相的GOTO。使用异常还增加了界面的复杂性,增强了耦合性。抛出异常会破坏引用的透明度,所以我们的方法并不纯粹。我们的函数没有单一的返回点,这大大阻碍了推理。

异常还有一个巨大的问题。异常可能导致模型的不一致状态。如果某些操作无法完成,模型应该保持与之前相同的状态。开发者并不总是注意到这一点。在计算过程中,模型状态只被部分改变并且不一致的情况下,就会引发异常。你的IDE和编译器都不会帮助你解决这个问题。

最近的一项研究表明,分布式系统中90%的故障是指错误处理。

功能性方法
使用类似单体的容器类型,如Option、Either或Result,可读性和优雅性要好得多。这些都是注重功能的语言中众所周知的结构。我们可以在设计我们的API时只考虑单一的返回类型,而不去扭曲控制流。

Order.addItem(Item $item): Either[Order, Error]

添加一个项目可能导致一个新的订单(包含该项目)或一个错误。指定我们可能从该方法得到的错误类型(OrderItemsNumberExceed, CanNotAddItemToFinishedOrder)是至关重要的。该API是优雅而完整的。返回类型明确地告诉我们,我们可能没有任何参数的有效答案。我们可以使用联合类型或通用的OrderError,并在方法文档中描述这些错误,但依赖类型级的安全总是更好。

在语言层面上,必须可以轻松地对许多调用进行排序,而不必为错误处理而烦恼。如果是这样的话,那是非常好的。
选项、任一、结果--保留单体特征的类型很容易被组合。这不会破坏正常的控制流或参考透明性。但是如果没有语言层面上的语法支持,可能会变成很麻烦。否则,例外可能是更有吸引力的选择。

免责声明
这篇文章只提到了商业领域的环境,可能不适用于低级别的或基础设施的情况。