业务对象,验证和例外

时间:2008-09-17 23:04:21

标签: c# validation exception business-objects

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

假设我有一个业务对象,其中包含对象属性的getter / setter。假设我需要验证该值是否介于10和20之间。这是一个业务规则,因此它属于我的业务对象。所以这似乎意味着我的验证码在我的二传手中。现在我将UI数据绑定到数据对象的属性。用户输入5,因此规则需要失败,并且不允许用户移出文本框。 。 UI被数据绑定到属性,因此将调用setter,规则检查并失败。如果我从业务对象中引发异常以说规则失败,则UI会选择该规则。但这似乎违背了例外的首选用法。鉴于它是一个制定者,你实际上并不会对setter产生“结果”。如果我在对象上设置另一个标志,则意味着UI必须在每次UI交互后检查该标志。

那么验证应该如何运作?

编辑:我可能在这里使用了一个过于简化的例子。上面的范围检查之类的东西可以通过UI轻松处理,但是如果样品更复杂,例如业务对象根据输入计算一个数字,如果计算出的数字超出范围,则应该重新注入。这是更复杂的逻辑,不应该在UI中。

还考虑根据已输入的字段输入的其他数据。例如,我必须在订单上输入一个项目以获得某些信息,如库存,当前成本等。用户可能要求此信息做出进一步进入的决定(请说明订购了多少个单位)或者可能需要按顺序进一步验证。如果该项目无效,用户是否应该能够输入其他字段?有什么意义呢?

18 个答案:

答案 0 :(得分:18)

您想在Paul Stovell关于数据验证的出色工作中深入研究一下。他在this article中一次总结了他的想法。我碰巧在这个问题上分享了他的观点,我在my own libraries中实现了这个观点。

用保罗的话来说,这是在设置者中抛出异常的缺点(基于Name属性不应为空的样本):

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

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

  
      
  1. 拥有无效的业务对象没有任何问题,只要您不试图坚持它。
  2.   
  3. 应该从业务对象中检索任何和所有损坏的规则,以便数据绑定以及您自己的代码可以查看是否存在错误并适当地处理它们。
  4.   

答案 1 :(得分:8)

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

在这种情况下,我不认为抛出异常是最好的主意。我绝对遵循思想学派的观点,即异常应该是针对特殊情况,而简单的验证错误则不然。在我看来,提升OnValidationFailed事件将是更清晰的选择。

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

答案 2 :(得分:8)

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

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

  2. 您的数据库保存代码应该为坏数据抛出无效的参数异常。这样做是有道理的,因为此时您无法继续进行数据库写入。理想情况下,这应该永远不会发生,因为UI应该阻止用户保存,但您仍然需要它来确保数据库的一致性。此外,您可能会在UI之外的其他内容(例如批量更新)中调用此代码,而无需进行UI数据验证。

答案 3 :(得分:5)

您可能希望在getter和setter之外移动验证。您可以拥有一个名为IsValid的函数或属性来运行所有验证规则。 t将使用所有“Broken Rules”填充字典或散列表。该词典将暴露给外界,您可以使用它来填充错误消息。

这是CSLA.Net采用的方法。

答案 4 :(得分:4)

不应将异常作为验证的正常部分抛出。从业务对象内部调用的验证是最后一道防线,并且只有在UI无法检查某些内容时才会发生。因此,它们可以像任何其他运行时异常一样对待。

请注意,这是定义验证规则和应用它们之间的区别。您可能希望在业务逻辑层中定义(即代码或注释)业务规则,但是从UI调用它们,以便可以以适合该特定UI的方式处理它们。对于不同的UI,处理方式会有所不同,例如基于表单的web-apps与ajax web-apps。异常验证提供了非常有限的处理选项。

许多应用程序复制其验证规则,例如javascript,域对象约束和数据库约束。理想情况下,此信息仅定义一次,但实施此信息可能是挑战,需要横向思考。

答案 5 :(得分:3)

正如Paul Stovell提到的article,您可以通过实现IDataErrorInfo接口在业务对象中实现无错误验证。这样做将允许WinForm's ErrorProviderWPF's binding with validation rules发出用户错误通知。验证对象属性的逻辑存储在一个方法中,而不是存储在每个属性获取器中,并且您不一定要求助于CSLA或Validation Application Block等框架。

至于阻止用户将焦点从文本框中更改为关注: 首先,这通常不是最好的做法。用户可能想要不按顺序填写表单,或者,如果验证规则取决于多个控件的结果,则用户可能必须填写虚拟值以便从一个控件中取出以设置另一个控件。也就是说,这可以通过将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事件。

答案 6 :(得分:3)

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

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

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

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

答案 7 :(得分:3)

这取决于您将要执行的验证类型以及位置。我认为应用程序的每一层都可以很容易地保护免受不良数据的影响,并且它太容易做到,因为它不值得。

考虑多层应用程序以及每个层的验证要求/设施。中间层,对象,似乎在这里有争议。

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

  • <强>对象

  • ASP.NET / Windows表单
    使用验证器例程和/或控件保护表单的状态(而不是对象)没有使用异常(winforms不附带验证器,但在msdn describing how to implement them有一个很好的系列)

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

*如果您的数据库可以代表它,但我们假设它可以 *我知道您可以在C#中使用ushort,但出于本示例的目的,我们假设您不能

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

但是你的对象仍然能够表示少于零的床,而且,如果你在一个场景中使用该对象而不涉及已经集成验证的层(你的表单和你的DB)你运气不好。

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

答案 8 :(得分:3)

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

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

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

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

在后一个系统之后,我几乎从不对属性设置器进行早期验证,因为这种软件使得属性的使用复杂化(但在问题中给出的情况下,我可能)。 (顺便说一句,请不要因为它有错误的数据而阻止我跳出一个字段。当我无法在表单上找到时,我会感到clausterphobic!我会在一分钟后回去修复它,我保证!好的,我现在感觉好多了,抱歉。)

答案 9 :(得分: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;
  }
}

答案 10 :(得分:3)

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

我使用的一种方法是将自定义属性应用于业务对象属性,该属性描述了验证规则。 e.g:

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

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

答案 11 :(得分:1)

如果数据无效,您是否考虑在设置者中提出事件?这样可以避免抛出异常的问题,并且无需显式检查对象是否存在“无效”标志。您甚至可以传递一个参数来指示哪个字段验证失败,以使其更具可重用性。

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

答案 12 :(得分:1)

根据我的经验,验证规则在应用程序中的所有屏幕/表单/进程中很少通用。像这样的场景很常见:在添加页面上,Person对象可能没有姓氏,但在编辑页面上它必须有一个姓氏。在这种情况下,我开始相信验证应该在对象之外发生,或者应该将规则注入到对象中,以便在给定上下文的情况下规则可以更改。有效/无效应该是验证后对象的显式状态,或者可以通过检查集合中的失败规则来导出。失败的业务规则不是恕我直言。

答案 13 :(得分:1)

在您的情况下抛出异常是好的。您可以认为该情况是一个真正的异常,因为有些东西试图将整数设置为字符串(例如)。业务规则缺乏对您的观点的了解意味着他们应该考虑这种情况,并将其返回到视图中。

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

您可以使用上面指定的spring框架,只要注意链接文档的大部分内容都表示编写的代码不是强类型的,I.E。您可能会在运行时遇到错误,在编译时无法获取。这是我尽可能避免的事情。

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

答案 14 :(得分:1)

您可能会考虑the approach taken by the Spring framework。如果您使用的是Java(或.NET),则可以按原样使用Spring,但即使您不是,也可以使用该模式;你只需编写自己的实现。

答案 15 :(得分:0)

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

答案 16 :(得分:0)

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

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

答案 17 :(得分:0)

如果输入超出了业务对象实现的业务规则,我会说这是一个不由业务对象处理的情况。因此,我会抛出异常。即使setter在你的例子中“处理”了5,业务对象也不会。

对于更复杂的输入组合,虽然需要一个vaildation方法,否则你最终会得到分散在各处的非常复杂的验证。

在我看来,你必须根据允许/不允许输入的复杂程度来决定走哪条路。