38

我一直在阅读一些关于异常及其使用的问题和答案。似乎是一种强烈的观点,即只应针对异常、未处理的情况提出异常。所以这让我想知道验证如何与业务对象一起工作。

假设我有一个带有 getter/setter 对象属性的业务对象。假设我需要验证该值是否在 10 到 20 之间。这是一个业务规则,因此它属于我的业务对象。所以这对我来说似乎意味着验证代码在我的设置器中。现在我将 UI 数据绑定到数据对象的属性。用户输入5,所以规则需要失败,不允许用户移出文本框。. UI 是数据绑定到属性的,因此将调用 setter、检查规则并失败。如果我从我的业务对象中提出一个异常说规则失败,那么 UI 会接受它。但这似乎与异常的首选用法背道而驰。鉴于它是一个二传手,你真的不会有二传手的“结果”。

那么验证应该如何工作呢?

编辑:我可能在这里使用了一个过于简单的例子。像上面的范围检查这样的事情可以由 UI 轻松处理,但如果验证更复杂,例如业务对象根据输入计算一个数字,如果计算出的数字超出范围,它应该被拒绝。这是更复杂的逻辑,不应该出现在 UI 中。

还考虑基于已输入的字段输入的进一步数据。例如,我必须在订单上输入一个项目以获得某些信息,如现有库存、当前成本等。用户可能需要此信息来决定进一步输入(例如要订购多少个单位),或者可能需要它来订购以进行进一步验证。如果项目无效,用户是否应该能够输入其他字段?重点是什么?

4

18 回答 18

18

您想深入研究一下Paul Stovell在数据验证方面的出色工作。他在这篇文章中一次总结了自己的想法。我碰巧分享了他对此事的看法,我在自己的库中实现了这一点。

用 Paul 的话来说,这是在 setter 中抛出异常的缺点(基于Name属性不应为空的示例):

  • 有时您实际上需要一个空名称。例如,作为“创建帐户”表单的默认值。
  • 如果您依靠它在保存之前验证任何数据,您将错过数据已经无效的情况。我的意思是,如果您从数据库中加载一个空名称的帐户并且不更改它,您可能永远不会知道它是无效的。
  • 如果您不使用数据绑定,则必须编写大量带有try/catch块的代码来向用户显示这些错误。在用户填写表单时尝试在表单上显示错误变得非常困难。
  • 我不喜欢为非异常的事情抛出异常。用户将帐户名称设置为“Supercalafragilisticexpialadocious”不是一个例外,而是一个错误。这当然是个人的事情。
  • 很难获得所有已被破坏的规则的列表。例如,在某些网站上,您会看到“必须输入姓名。必须输入地址。必须输入电子邮件”之类的验证消息。要显示它,您将需要很多try/catch块。

以下是替代解决方案的基本规则:

  1. 拥有无效的业务对象并没有错,只要您不尝试持久化它。
  2. 任何和所有被破坏的规则都应该可以从业务对象中恢复,以便数据绑定以及您自己的代码可以查看是否存在错误并适当地处理它们。
于 2008-09-25T12:35:51.680 回答
9

假设您有单独的验证和持久化(即保存到数据库)代码,我会执行以下操作:

  1. UI 应该执行验证。不要在这里抛出异常。您可以提醒用户注意错误并阻止保存记录。

  2. 您的数据库保存代码应该为错误数据抛出无效参数异常。在这里这样做是有意义的,因为此时您无法继续进行数据库写入。理想情况下,这不应该发生,因为 UI 应该阻止用户保存,但您仍然需要它来确保数据库一致性。此外,您可能会从没有 UI 数据验证的 UI(例如批量更新)以外的地方调用此代码。

于 2008-09-18T02:14:50.897 回答
8

我一直很喜欢 Rocky Lhotka 在CSLA 框架中的方法(正如 Charles 所提到的)。通常,无论是由 setter 驱动还是通过调用显式 Validate 方法,BrokenRule 对象的集合都由业务对象在内部维护。UI 只需要检查对象上的 IsValid 方法,该方法反过来检查 BrokenRules 的数量,并适当地处理它。或者,您可以轻松地让 Validate 方法引发 UI 可以处理的事件(可能是更简洁的方法)。您还可以使用 BrokenRules 列表以摘要形式或在相应字段旁边显示错误消息以供使用。尽管 CSLA 框架是用 .NET 编写的,但整体方法可以用于任何语言。

在这种情况下,我不认为抛出异常是最好的主意。我绝对遵循这样的思想流派,即异常应该用于特殊情况,而简单的验证错误则不是。在我看来,引发 OnValidationFailed 事件将是更干净的选择。

顺便说一句,我从来不喜欢在字段处于无效状态时不让用户离开的想法。在许多情况下,您可能需要暂时离开该字段(可能先设置其他字段),然后再返回并修复无效字段。我认为这只是不必要的不​​便。

于 2008-09-18T04:33:28.850 回答
5

您可能希望将验证移到 getter 和 setter 之外。您可以有一个名为 IsValid 的函数或属性来运行所有验证规则。t 将使用所有“Broken Rules”填充字典或哈希表。这个字典会暴露给外界,你可以用它来填充你的错误信息。

这是 CSLA.Net 中采用的方法。

于 2008-09-18T03:38:00.137 回答
4

Exceptions should not be thrown as a normal part of validation. Validation invoked from within business objects is a last line of defense, and should only happen if the UI fails to check something. As such they can be treated like any other runtime exception.

Note that here's a difference between defining validation rules and applying them. You might want to define (ie code or annotate) your business rules in your business logic layer but invoke them from the UI so that they can handled in a manner appropriate to that particular UI. The manner of handling will vary for different UI's, eg form based web-apps vs ajax web-apps. Exception-on-set validation offers very limited options for handling.

Many applications duplicate their validation rules, such as in javascript, domain object constraints and database constraints. Ideally this information will only be defined once, but implementing this can be challenge and requires lateral thinking.

于 2008-09-18T00:29:53.567 回答
3

我倾向于相信业务对象在传递违反其业务规则的值时应该抛出异常。然而,winforms 2.0 数据绑定架构似乎相反,因此大多数人都支持这种架构。

我同意 shabbyrobe 的最后一个回答,即业务对象应该被构建为可用并在多个环境中正常工作,而不仅仅是 winforms 环境,例如,业务对象可以用于 SOA 类型的 Web 服务、命令行界面、asp .net 等。在所有这些情况下,该对象应正确运行并保护自己免受无效数据的影响。

一个经常被忽视的方面是在管理 1-1、1-n 或 nn 关系中的对象之间的协作时会发生什么,这些是否也接受添加无效的协作者并只维护一个应该检查或应该检查的无效状态标志它积极拒绝添加无效的协作。我不得不承认,我深受 Jill Nicola 等人的流线型对象建模 (SOM) 方法的影响。但还有什么是合乎逻辑的。

接下来是如何使用 Windows 窗体。我正在考虑为这些场景的业务对象创建一个 UI 包装器。

于 2008-09-25T12:08:12.167 回答
3

也许您应该考虑同时进行客户端和服务器端验证。如果有任何东西通过客户端验证,那么如果您的业务对象无效,您可以随意抛出异常。

我使用的一种方法是将自定义属性应用于描述验证规则的业务对象属性。例如:

[MinValue(10), MaxValue(20)]
public int Value { get; set; }

然后可以处理属性并用于自动创建客户端和服务器端验证方法,以避免重复业务逻辑的问题。

于 2008-09-17T23:41:21.453 回答
3

我肯定会提倡客户端和服务器端验证(或在各个层进行验证)。这在跨物理层或进程进行通信时尤其重要,因为抛出异常的成本变得越来越昂贵。此外,您等待验证的链条越往下,浪费的时间就越多。

至于是否使用异常进行数据验证。我认为可以在流程中使用异常(尽管仍然不是优选的),但在流程之外,调用一个方法来验证业务对象(例如在保存之前)并让该方法返回操作的成功以及任何验证错误。错误不是异常的。

Microsoft 在验证失败时从业务对象中抛出异常。至少,企业库的验证应用程序块是这样工作的。

using Microsoft.Practices.EnterpriseLibrary.Validation;
using Microsoft.Practices.EnterpriseLibrary.Validation.Validators;
public class Customer
{
  [StringLengthValidator(0, 20)]
  public string CustomerName;

  public Customer(string customerName)
  {
    this.CustomerName = customerName;
  }
}
于 2008-09-17T23:50:49.090 回答
3

您的业​​务对象应该为错误的输入抛出异常,但在正常程序运行过程中绝不应该抛出这些异常。我知道这听起来很矛盾,所以我会解释一下。

每个公共方法都应验证其输入,并在输入不正确时抛出“ArgumentException”。(私有方法应该使用“Debug.Assert()”验证它们的输入以简化开发,但这是另一回事。)这条关于验证公共方法(当然还有属性)输入的规则对于应用程序的每一层都是正确的.

软件接口的要求当然应该在接口文档中说明,而调用代码的工作就是确保参数正确并且永远不会抛出异常,这意味着 UI 需要验证在将输入交给业务对象之前。

虽然上面给出的规则几乎不应该被打破,但有时业务对象验证可能非常复杂,并且不应该将这种复杂性强加到 UI 上。在这种情况下,BO 的接口最好允许它接受的内容有一些余地,然后提供一个显式的 Validate(out string[]) 谓词来检查属性并就需要更改的内容提供反馈。但是请注意,在这种情况下,仍然存在明确定义的接口要求,并且不需要抛出异常(假设调用代码遵循规则)。

遵循后一种系统,我几乎从不对属性设置器进行早期验证,因为这种软化使属性的使用变得复杂,(但在问题中给出的情况下,我可能会)。(顺便说一句,请不要仅仅因为其中包含错误数据而阻止我从字段中跳出。当我无法在表单周围跳动时,我会感到幽闭恐惧症!我会在一分钟内返回并修复它,我保证!好吧,我现在感觉好多了,对不起。)

于 2008-09-18T03:40:27.807 回答
3

这取决于您将执行哪种验证以及在何处执行。我认为可以轻松保护应用程序的每一层免受不良数据的影响,而且这样做太容易了,不值得。

考虑一个多层应用程序和每一层的验证要求/设施。中间层,对象,是这里似乎有待讨论的层。

  • 数据库
    通过列约束和引用完整性保护自己免受无效状态的影响,这将导致应用程序的数据库代码抛出异常

  • 对象

  • ASP.NET/Windows Forms
    使用验证器例程和/或控件来保护表单的状态(不是对象)而不使用异常(winforms 不附带验证器,但是 msdn 上有一个很好的系列描述了如何实现它们

假设您有一张包含酒店房间列表的表格,每一行都有一个称为“床位”的床位数列。该列最合理的数据类型是无符号小整数*。您还有一个普通的 ole 对象,它具有一个名为“Beds”的 Int16* 属性。问题是您可以将 -4555 粘贴到 Int16 中,但是当您将数据持久保存到数据库时,您将得到一个异常。这很好 - 我的数据库不应该被允许说酒店房间的床位少于零,因为酒店房间的床位不能少于零。


* 如果您的数据库可以表示,但我们假设它可以

关于对象是否应该代表您的业务实体,或者它们是否应该代表您的表单状态存在一些混淆。当然,在 ASP.NET 和 Windows 窗体中,窗体完全能够处理和验证自己的状态。如果您在 ASP.NET 表单上有一个文本框,将用于填充相同的 Int16 字段,您可能已经在页面上放置了一个 RangeValidator 控件,该控件在输入分配给您的对象之前对其进行测试。它会阻止您输入小于零的值,并且可能会阻止您输入大于例如 30 的值,希望这足以满足您可以想象的最严重跳蚤出没的旅馆的需求。在回发时,您可能会在之前检查页面的 IsValid 属性构建你的对象,从而防止你的对象代表少于零的床,并防止你的 setter 被调用一个它不应该持有的值。

但是您的对象仍然能够表示少于零的床位,并且再次,如果您在不涉及集成了验证的层(您的表单和数据库)的场景中使用该对象,那么您就不走运了。

为什么你会在这种情况下?这一定是一组非常特殊的情况!因此,您的 setter 在收到无效数据时需要抛出异常。它永远不应该被抛出,但它可以被抛出。您可能正在编写一个 Windows 窗体来管理对象以替换 ASP.NET 窗体并忘记在填充对象之前验证范围。您可以在根本没有用户交互的计划任务中使用该对象,并且该对象保存到数据库的不同但相关的区域,而不是对象映射到的表。在后一种情况下,您的对象可以进入无效状态,但直到其他操作的结果开始受到无效值的影响时您才会知道。如果您正在检查它们并抛出异常,那就是。

于 2008-09-18T04:59:27.243 回答
3

正如 Paul Stovell 的文章所提到的,您可以通过实现 IDataErrorInfo 接口在业务对象中实现无错误验证。这样做将允许WinForm 的 ErrorProviderWPF 与验证规则的绑定通知用户错误。验证对象属性的逻辑存储在一个方法中,而不是存储在每个属性获取器中,并且您不必求助于 CSLA 或验证应用程序块等框架。

就阻止用户将焦点移出文本框而言:首先,这通常不是最佳实践。用户可能希望无序填写表单,或者,如果验证规则依赖于多个控件的结果,则用户可能必须填写一个虚拟值才能离开一个控件来设置另一个控件。也就是说,这可以通过将 Form 的AllowValidate属性设置为其默认值EnableAllowFocusChange 并订阅 Control.Validating 事件来实现:

    private void textBox1_Validating(object sender, CancelEventArgs e)
    {
        if (textBox1.Text != String.Empty)
        {
            errorProvider1.SetError(sender as Control, "Can not be empty");
            e.Cancel = true;
        }
        else
        {
            errorProvider1.SetError(sender as Control, "");
        }
    }

使用存储在业务对象中的规则进行此验证有点棘手,因为在焦点更改和数据绑定业务对象更新之前调用了 Validating 事件。

于 2008-12-29T17:09:51.733 回答
1

在您的情况下抛出异常很好。您可以将这种情况视为真正的例外,因为某些东西正试图将整数设置为字符串(例如)。业务规则对您的视图缺乏了解意味着他们应该将这种情况视为例外并将其返回给视图。

在将输入值发送到业务层之前是否验证输入值取决于您,我认为只要您在整个应用程序中遵循相同的标准,那么您最终将获得干净且可读的代码。

您可以使用上面指定的 spring 框架,只是要小心,因为链接文档中的大部分都指示编写非强类型的代码,IE 您可能会在运行时遇到无法在编译时获取的错误。这是我尽量避免的事情。

我们目前这样做的方式是,我们从屏幕上获取所有输入值,将它们绑定到数据模型对象,如果值错误则抛出异常。

于 2008-09-17T23:38:07.560 回答
1

如果数据无效,您是否考虑过在 setter 中引发事件?这将避免引发异常的问题,并消除显式检查对象是否存在“无效”标志的需要。您甚至可以传递一个参数来指示哪个字段验证失败,以使其更易于重用。

如果需要,事件的处理程序应该能够将焦点重新放在适当的控件上,并且它可以包含通知用户错误所需的任何代码。此外,您可以简单地拒绝连接事件处理程序,并在需要时随意忽略验证失败。

于 2008-09-18T03:21:48.460 回答
1

In my experience, validation rules are seldom universal across all screens/forms/processes in an application. Scenarios like this are common: on the add page, it may be ok for a Person object not to have a last name, but on the edit page it must have a last name. That being the case I've come to believe that validation should happen outside of an object, or the rules should be injected into the object so the rules can change given a context. Valid/Invalid should be an explicit state of the object after validation or one that can be derived by checking a collection for failed rules. A failed business rule is not an exception IMHO.

于 2008-09-18T00:24:36.920 回答
1

您可能想考虑一下 Spring 框架采用的方法。如果您使用的是 Java(或 .NET),则可以按原样使用 Spring,但即使您不是,您仍然可以使用该模式;您只需要编写自己的实现即可。

于 2008-09-17T23:17:04.477 回答
0

我认为这是一个可以抛出异常的例子。您的属性可能没有任何上下文来纠正问题,因为这样的异常是正常的,调用代码应该处理这种情况,如果可能的话。

于 2008-09-17T23:12:08.823 回答
0

如果输入超出业务对象实现的业务规则,我会说这是业务对象未处理的情况。因此我会抛出一个异常。即使在您的示例中设置器会“处理” 5,但业务对象不会。

但是,对于更复杂的输入组合,需要验证方法,否则您最终会得到分散在各处的相当复杂的验证。

在我看来,您必须根据允许/不允许输入的复杂性来决定采用哪种方式。

于 2008-09-17T23:15:48.613 回答
0

我认为这取决于你的商业模式有多重要。如果你想走 DDD 的路,你的模型是最重要的。因此,您希望它始终处于有效状态。

在我看来,大多数人都试图用域对象做太多事情(与视图通信、持久化到数据库等),但有时你需要更多的层和更好的关注点分离,即一个或多个视图模型。然后,您可以在视图模型上应用无异常验证(对于不同的上下文,验证可能会有所不同,例如,Web 服务/网站/等)并将异常验证保留在您的业务模型中(以防止模型被破坏)。您将需要一个(或多个)应用程序服务层来将您的视图模型与您的业务模型映射。业务对象不应被通常与特定框架相关的验证属性所污染,例如 NHibernate Validator。

于 2009-08-27T00:51:16.643 回答