首页 / 知识

关于c#:Business Objects,Validation和Exceptions

2023-04-16 21:46:00

关于c#:Business Objects,Validation和Exceptions

Business Objects, Validation And Exceptions

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

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

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

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

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


您想深入了解Paul Stovell关于数据验证的出色工作。他在本文中一次总结了他的想法。我碰巧在这个问题上分享了他的观点,我在自己的库中实现了这个观点。

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

  • There may be times where you actually need to have an empty name. For example, as the default value for a"Create an account" form.
  • If you're relying on this to validate any data before saving, you'll miss the cases where the data is already invalid. By that, I mean, if you load an account from the database with an empty name and don't change it, you might not ever know it was invalid.
  • If you aren't using data binding, you have to write a lot of code with try/catch blocks to show these errors to the user. Trying to show errors on the form as the user is filling it out becomes very difficult.
  • I don't like throwing exceptions for non-exceptional things. A user setting the name of an account to"Supercalafragilisticexpialadocious" isn't an exception, it's an error. This is, of course, a personal thing.
  • It makes it very difficult to get a list of all the rules that have been broken. For example, on some websites, you'll see validation messages such as"Name must be entered. Address must be entered. Email must be entered". To display that, you're going to need a lot of try/catch blocks.

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

  • There is nothing wrong with having an invalid business object, so long as you don't try to persist it.
  • Any and all broken rules should be retrievable from the business object, so that data binding, as well as your own code, can see if there are errors and handle them appropriately.

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

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

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


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

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

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


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

    这是CSLA.Net采用的方法。


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

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

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


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

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

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

    • 宾语

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

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

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

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

    但是你的对象仍然能够代表不到零的床,而且,如果你在不涉及已经集成验证的层(你的形式和你的数据库)的场景中使用对象,你就会失去运气。

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


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

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

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

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

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


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

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

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

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


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

    至于阻止用户将焦点从文本框中更改为关注:
    首先,这通常不是最好的做法。用户可能想要不按顺序填写表单,或者,如果验证规则取决于多个控件的结果,则用户可能必须填写虚拟值以便从一个控件中取出以设置另一个控件。也就是说,这可以通过将Form的AllowValidate属性设置为其默认值EnableAllowFocusChange并订阅Control.Validating事件来实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
        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事件。


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

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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;
      }
    }

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

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

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

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


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


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

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

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

    我们目前在这里采用的方式是,我们从屏幕获取所有输入值,将它们绑定到数据模型对象,并在值出错时抛出异常。


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


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

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


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

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


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


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

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

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


    案例对象业务验证

    最新内容

    相关内容

    猜你喜欢