MVC、DDD以及业务逻辑中的错误抛出

3
我已经阅读了Scott Millett所著“专业ASP.NET设计模式”的书籍,他在示例中使用Validate()方法验证业务逻辑,如果出现任何破坏规则的情况,将其添加到集合中,服务层会调用模型上名为GetBrokenRules()的方法。
现在我也阅读了几本关于DDD的书籍、博客和论坛,其中提到在DDD中,对象不应进入无效状态。
我看到的所有DDD示例都会在违反业务规则时抛出错误,而不是返回一组破损的规则。我甚至下载了Scott Millett的最新源代码,他现在将代码更改为抛出错误而不是传回破损规则列表。我也看到其他DDD代码示例采用同样的方法。
我正在与一个团队成员进行辩论,他认为抛出错误会消耗资源,我们不应该抛出错误,而应像当前所做的那样返回一组破损的规则。但通过这样做,我们正在传递一个无效的对象,因为它具有不良数据,我们只在最后检查其破损的规则。
我正在思考其他人对此问题的看法。当业务规则失败时,我们应该立即抛出错误吗?如果是这样,你能否列举一些做法的利弊呢?
我不知道在.NET中抛出错误会有多大的资源消耗,所以我无法反驳这一点,但我想知道这是否也是一种个人观点而非编码标准。
Mike

我想知道在这个问题的背景下,“expensive”是什么意思? - Dmitry Shvetsov
4个回答

5

只有在实际抛出异常时,异常才会变得昂贵。

由于应该预先验证逻辑,因此异常情况应该少之又少。如果不是这种情况,则您做错了。

异常是通知模型处于无效状态的最佳机制。如果应用程序不终止,它们需要明确处理。

使用错误集合/返回值不是一个好习惯,因为这些东西可能会被使用代码忽略。


1

@Oded 所说的完全有效。

然而,还有另一种处理验证的方法,不需要抛出异常,也不需要传递无效对象。简而言之,您总是通过工厂方法创建对象。如果一切顺利,将返回一个实例,否则将返回null。当然,这意味着您在创建对象时必须检查null,但仅此而已。 工厂方法(可以是服务的一部分)可以将IValidationDictionary(不是BCL接口)作为参数,工厂可以在其中设置错误。但我们在这里只谈论来自用户的输入数据(格式验证)。没有业务规则在此。如果该对象将在其状态无效的情况下使用,则必须引发异常。

您可以通过在对象或其聚合根上具有CanDoThat()方法来避免99%的问题。如果object.CanDoThat(context)可以这样做并避免异常。但是,如果由于并发性质而发生变化,则可能触发异常。

我通常有以下工作流程:

  • 在控制器层进行用户输入验证(这是应用程序的防腐层)。只有数据格式处理在此处处理。
  • 一旦数据有效,就会通过命令处理程序进行处理。
  • 在处理时,我会询问聚合根是否可以执行特定操作。如果可以,一切都会前进。
  • 如果不能,则有两种情况:

    1. 很可能出现了错误,例如错误或异常,因此在此处适当使用异常(99%的情况)。
    2. 在该上下文中,它是有效的响应,因此它被简单地忽略。
  • 当然,您可能会遇到聚合根级别一切正常,但由于数据库约束而在存储库级别失败的情况。这意味着存储库会抛出异常,我会处理它(因为我期望发生这种情况)。如果用户需要响应,则会引发特定异常以信号错误,这将设置视图的ModelState。

现在这是一个棘手的情况,因为看起来我正在使用异常来控制代码流程,但事实并非如此。虽然我希望数据库抛出违反约束的异常,但这仍然是一种异常情况,因为这并不是每次都会发生的,并且这不是用户可以避免的情况。我接下来抛出特定的异常,因为第一个是与持久性相关的异常,但我的控制器不知道它,所以我必须以它理解的方式传达这个异常状态。

好的,回答有点长,但思路是:有些错误虽然是预期的,但仍然是异常情况,需要作为异常处理。但是,如果输入数据存在格式验证错误或者以特定的方式无效,则应该在第一个防腐层(即控制器本身)中处理,无需使用异常。控制器不应让无效的输入继续进行。如果它继续进行,则是一个错误,是一种异常情况。

希望没有让您感到困惑 :)


我们已经达成妥协并决定使用CanDo()方法,因为我们之前在状态模式中使用过它。 - user1180223
数据格式在许多情况下都是业务规则,例如可能存在用户在域中设置特定规则的领域。例如,用户可能会指定书籍可以拥有的天数和样本视图数量的情况。这不是UI逻辑,因为您需要获取聚合以检查由所有者设置的书籍上的规则。在这里,数据必须具有有效的INT格式。 - Tudor
@TudorTudor,通常这是一个值对象,UI验证器可以重用它进行验证。 - MikeSW
这里缺少的一个细节是,我们也会在值对象中抛出异常。我(像Mike一样)倾向于在到达实体行为之前在上层重用这些值对象(通常是接受值对象的显式方法)。因此,我会从值对象中尽早获得异常。所以我做的是,让命令(而不是控制器)构建所有的值对象并捕获任何异常。命令将有一个 isValid 标志。在处理命令之前,我们可以检查它是否有效并返回错误消息。在处理程序本身中,我们也可以这样做。 - prograhammer

1
非常好的讨论。它使旧的讨论主题重获生机。我已经测试了几种方法,并发现混合使用可以是一个很好的选择。当验证输入参数到服务方法或构造函数时,我使用异常。如果验证知识不是实体责任的一部分,则使用验证器与访问者模式一起检查域实体。如果您是持久性无关的信仰者(我是),则可以使用位于基础设施层中的验证器,以验证是否可以持久化(或通过适配器导出到另一个系统...)。但是,我在我的实体上很少使用公共自动属性,只是为了使实体处于无效状态更加困难。我还让实体根据自己的领域范围/责任来验证自己(尝试遵循SRP...)。
将抛出异常作为通知其他部分/层的一般解决方案可能并不是每种情况下都最优,但对于某些情况可能是最优的。
/干杯

0
为什么不使用带有错误规则列表的异常呢?
与构建规则、确定如何处理规则、告知用户错误以及其中所有网络流量的成本相比,异常几乎没有任何成本。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接