业务对象,验证和异常
我一直在阅读有关异常及其使用的一些问题和解答。似乎强烈认为,仅应针对未处理的例外情况提出例外。因此,这使我想知道验证如何与业务对象一起使用。
可以说我有一个带有getter / setter的业务对象,用于该对象的属性。可以说我需要验证该值在10到20之间。这是一个业务规则,因此它属于我的业务对象。因此,这似乎对我而言意味着验证代码会出现在我的设置器中。现在,我将UI数据绑定到数据对象的属性。用户输入5,因此规则必须失败,并且不允许用户移出文本框。 。 UI已绑定到属性的数据,因此将调用setter,检查规则并失败。如果我从业务对象中提出了一个异常以说规则失败,则UI会接管该异常。但这似乎与例外的首选用法背道而驰。考虑到它的设置者,我们真的不会为设置者产生结果。如果我在对象上设置了另一个标志,则意味着UI必须在每次UI交互之后检查该标志。
那么验证应该如何工作?
编辑:我可能在这里使用了过度简化的示例。 UI可以轻松处理上述范围检查之类的问题,但是如果验证更复杂,例如业务对象根据输入来计算一个数字,如果该计算的数字超出范围,则应将其拒绝。这是UI中不应该包含的更复杂的逻辑。
还需要考虑基于已经输入的字段输入的其他数据。例如,我必须在订单上输入一个项目才能获得某些信息,例如库存,当前成本等。用户可能会要求此信息来决定进一步输入(订购多少个单位),或者可能需要订购以便进行进一步的验证。如果该项无效,用户应该可以输入其他字段吗?重点是什么?
解决方案
我认为这是一个可以抛出异常的示例。媒体资源可能没有任何可用于更正问题的上下文,因为这样的异常是有序的,并且调用代码应尽可能处理这种情况。
如果输入超出了由业务对象实现的业务规则,那么我会说这种情况不是由busines对象处理的。因此,我会抛出一个异常。即使在示例中设置器将"处理" 5,业务对象也不会。
对于更复杂的输入组合,虽然需要一种验证方法,否则最终我们将获得遍布整个地方的相当复杂的验证。
我认为我们必须根据允许/不允许的输入的复杂性来决定要走的路。
我们可能想考虑Spring框架采用的方法。如果我们使用的是Java(或者.NET),则可以按原样使用Spring,但是即使我们没有使用,也可以使用该模式。我们只需要编写自己的实现即可。
在情况下抛出异常是可以的。我们可以将这种情况视为真正的例外,因为某些事情正在尝试为字符串设置整数(例如)。缺乏视图知识的业务规则意味着他们应该考虑这种情况,并将其返回视图。
在我们将输入值发送到业务层之前是否进行验证取决于我自己,我认为,只要我们在整个应用程序中遵循相同的标准,那么最终将获得清晰易读的代码。
我们可以使用上面指定的spring框架,只是要小心,因为许多链接文档都指出编写的代码不是强类型的,即I.E。我们可能会在运行时遇到无法在编译时获取的错误。这是我尽量避免的事情。
目前,我们在此处执行此操作的方式是,我们从屏幕上获取所有输入值,将它们绑定到数据模型对象,如果值有错误,则引发异常。
也许我们应该同时考虑客户端验证和服务器验证。如果有什么事情超出了客户端验证的范围,那么如果业务对象将变得无效,则可以随意引发异常。
我使用的一种方法是将自定义属性应用于业务对象属性,该属性描述了验证规则。例如。:
[MinValue(10), MaxValue(20)] public int Value { get; set; }
然后可以对属性进行处理,并将其用于自动创建客户端和服务器端验证方法,以避免重复业务逻辑的问题。
我绝对会提倡客户端和服务器端验证(或者在各个层进行验证)。当跨物理层或者流程进行通信时,这尤其重要,因为抛出异常的代价变得越来越昂贵。此外,我们等待验证的链越深,浪费的时间就越多。
至于使用异常还是不使用数据进行验证。我认为可以在流程中使用异常(尽管仍然不是可取的),但是在流程之外,可以调用一种方法来验证业务对象(例如在保存之前),并让该方法返回操作成功以及任何验证错误。错误没有例外。
验证失败时,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; } }
以我的经验,验证规则很少在应用程序的所有屏幕/窗体/过程中通用。这样的情况很常见:在添加页面上,Person对象不使用姓氏是可以的,但在编辑页面上必须使用姓氏。在这种情况下,我开始相信验证应该发生在对象之外,或者应该将规则注入到对象中,以便在给定上下文的情况下可以更改规则。有效/无效应该是验证后对象的显式状态,或者可以通过检查失败规则的集合来得出的状态。失败的业务规则也不是恕我直言的例外。
异常不应作为验证的正常部分抛出。从业务对象内部调用的验证是最后一道防线,并且仅当UI无法检查某些内容时才应进行验证。因此,它们可以像其他任何运行时异常一样对待。
请注意,定义验证规则和应用验证规则之间是有区别的。我们可能想在业务逻辑层中定义(即编码或者注释)业务规则,但要从UI调用它们,以便可以以适合该特定UI的方式来处理它们。处理方式因不同的UI而异,例如基于表单的Web应用程序与Ajax Web应用程序。设置异常验证提供了非常有限的处理选项。
许多应用程序重复其验证规则,例如在javascript,域对象约束和数据库约束中。理想情况下,此信息仅会定义一次,但是实现此信息可能会遇到挑战,需要横向思考。
假设我们具有单独的验证并保留(即保存到数据库)代码,我将执行以下操作:
- 用户界面应执行验证。不要在这里抛出异常。我们可以警告用户错误,并防止保存记录。
- 数据库保存代码应针对无效数据抛出无效的参数异常。这样做很有意义,因为此时我们无法继续进行数据库写操作。理想情况下,永远不要发生这种情况,因为UI应该阻止用户进行保存,但是我们仍然需要使用它来确保数据库的一致性。另外,我们可能会从没有UI数据验证的UI之外的其他地方(例如批处理更新)调用此代码。
如果数据无效,我们是否考虑过在设置器中引发事件?这样可以避免引发异常的问题,并且不需要显式检查对象的"无效"标志。我们甚至可以传递一个参数,指示哪个字段未通过验证,以使其更可重用。
事件的处理程序应该能够根据需要将焦点重新放在适当的控件上,并且它可以包含将错误通知用户所需的任何代码。而且,我们可以简单地拒绝挂接事件处理程序,并根据需要随意忽略验证失败。
我们可能希望将验证移到getter和setter之外。我们可能具有一个名为IsValid的函数或者属性,该函数或者属性将运行所有验证规则。 t会使用所有"破碎规则"填充字典或者哈希表。该词典将向外界公开,我们可以使用它来填充错误消息。
这是CSLA.Net中采用的方法。
业务对象应该为错误的输入引发异常,但是在正常的程序运行过程中,绝不要抛出这些异常。我知道这听起来是矛盾的,所以我将进行解释。
每个公共方法都应验证其输入,并在输入不正确时抛出" ArgumentException"。 (并且私有方法应该使用" Debug.Assert()"来验证其输入以简化开发,但这是另一回事了。)关于验证公共方法(当然还有属性)输入的规则对于应用程序的每一层都是适用。
当然,应该在接口文档中阐明软件接口的要求,并且调用代码的工作就是确保参数正确,并且永远不会引发异常,这意味着UI需要验证输入,然后再将其交给业务对象。
尽管上面给出的规则几乎永远不会被打破,但是有时业务对象验证可能非常复杂,并且不应将这种复杂性强加到UI上。在这种情况下,对BO的接口允许其接受的内容留有余地,然后提供一个显式的Validate(out string [])谓词以检查属性并提供有关需要更改的内容的反馈,这是很好的。但是请注意,在这种情况下,仍然存在定义明确的接口要求,并且不需要抛出任何异常(假设调用代码遵循规则)。
在后一个系统中,我几乎从不对属性设置器进行早期验证,因为这种软化使属性的使用变得复杂(但是在问题中给出的情况下,我可能会这样做)。 (顺便说一句,请不要仅仅因为其中包含错误的数据而阻止我跳出该字段。当我无法绕过表单时,我会感到畏惧恐怖! ,我保证!好的,我现在好点了,抱歉。)
我一直是Rocky Lhotka在CSLA框架中所采用的方法的粉丝(正如Charles所提到的)。通常,无论是由setter驱动还是通过调用显式Validate方法驱动,BrokenRule对象的集合都由业务对象在内部维护。 UI仅需要检查对象上的IsValid方法,该方法进而检查BrokenRules的数量并适当地处理它。另外,我们可以轻松地使Validate方法引发UI可以处理的事件(可能是更简洁的方法)。我们还可以使用BrokenRules列表以摘要形式或者在相应字段旁边显示要使用的错误消息。尽管CSLA框架是用.NET编写的,但总体方法可以以任何语言使用。
我不认为在这种情况下抛出Exception是最好的主意。我绝对遵循这样的观点,即异常应在特殊情况下发生,而简单的验证错误则不会。我认为,引发OnValidationFailed事件将是更清洁的选择。
顺便说一句,我从未喜欢过在无效状态下不让用户离开字段的想法。在很多情况下,我们可能需要暂时离开该字段(也许首先要设置其他字段),然后再返回并修复无效的字段。我认为这只是不必要的麻烦。
这取决于我们将执行哪种类型的验证以及在何处进行验证。我认为可以轻松保护应用程序的每一层免受不良数据的侵害,并且这样做太容易了而不值得。
考虑一个多层应用程序以及每层的验证要求/便利性。中间层,对象,似乎是这里值得讨论的那一层。
- 数据库通过列约束和引用完整性保护自己免受无效状态的侵害,这将导致应用程序的数据库代码引发异常
- 目的 ?
- ASP.NET/Windows窗体使用验证程序例程和/或者控件在不使用异常的情况下保护窗体的状态(不是对象)(不带验证程序的Winforms附带,但是msdn上有很多精彩的系列文章描述了如何实现它们)
假设我们有一张桌子,上面列出了酒店房间,每行都有一个列,表示床数,称为"床"。该列最明智的数据类型是无符号小整数*。我们还拥有一个带有Int16 *属性的普通ole对象,该属性称为"床"。问题是我们可以将-4555粘贴到Int16中,但是当我们将数据持久保存到数据库中时,我们将获得异常。很好,我的数据库不允许说酒店房间的床位数少于零,因为酒店房间的床位数不能少于零。
*如果数据库可以表示它,但是让我们假设它可以
*我知道我们可以在C#中使用ushort,但是出于本示例的目的,我们假设我们不能
关于对象是应该代表业务实体还是应该代表表单状态,存在一些困惑。当然,在ASP.NET和Windows窗体中,窗体完全能够处理和验证其自身的状态。如果在ASP.NET表单上有一个文本框将用于填充相同的Int16字段,则可能已在页面上放置了RangeValidator控件,该控件在将输入分配给对象之前对其进行测试。它阻止我们输入小于零的值,并且可能阻止我们输入大于30的值,希望这足以应付我们可以想象的最烂的跳蚤出没的旅馆。在回发时,我们可能会在构建对象之前检查页面的IsValid属性,从而防止对象表示的床位数少于零,并防止使用不应该包含的值调用setter。
但是对象仍然能够表示少于零张床,并且再次说明,如果我们在不涉及已将验证集成到其中的层(表单和DB)的情况下使用该对象,那么我们将失去运气。
为什么会出现这种情况?这一定是非常特殊的情况!因此,setter在收到无效数据时需要引发异常。绝不应该将其抛出,而是可以。我们可能正在编写Windows窗体来管理对象以替换ASP.NET窗体,而忘记在填充对象之前验证范围。我们可能在完全没有用户交互的计划任务中使用该对象,并且该任务保存到数据库的另一个但相关的区域中,而不是对象映射到的表中。在后一种情况下,对象可以进入无效状态,但是直到其他操作的结果开始受到无效值的影响时,我们才知道。如果要检查它们并引发异常,那就是。
我倾向于认为,当传递的值违反其业务规则时,业务对象应引发异常。但是,似乎Winforms 2.0数据绑定体系结构是相反的,因此大多数人都在支持这种体系结构。
我同意shabbyrobe的最后一个回答,即应该将业务对象构建为可用并可以在多个环境中正常工作,而不仅仅是在Winforms环境中,例如,该业务对象可以用于SOA类型的Web服务,命令行界面,asp .net等。在所有这些情况下,对象都应正确运行并保护自己免受无效数据的侵害。
一个经常被忽视的方面是在管理1-1、1-n或者nn关系中的对象之间的协作时会发生什么,如果这些对象也接受添加了无效的协作者并只是维护一个无效的状态标志,则该状态标志应检查或者应它积极拒绝添加无效的协作。我必须承认,我深受Jill Nicola等人的精简对象建模(SOM)方法的影响。但是还有什么是合乎逻辑的。
接下来的事情是如何使用Windows窗体。我正在为这些方案的业务对象创建UI包装。
我们想钻研一下Paul Stovell在数据验证方面的出色工作。他在本文中一次总结了自己的想法。我碰巧分享了他在此问题上的观点,并在我自己的库中实现了这一观点。
用保罗的话讲,这是在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.
如Paul Stovell的文章所述,我们可以通过实现IDataErrorInfo接口在业务对象中实现无错验证。这样做将允许通过WinForm的ErrorProvider以及WPF与验证规则的绑定来通知用户错误。验证对象属性的逻辑存储在一个方法中,而不是存储在每个属性获取器中,并且我们不必求助于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事件。
我认为这取决于业务模式有多重要。如果要采用DDD方式,则模型是最重要的。因此,我们希望它始终处于有效状态。
在我看来,大多数人都试图对域对象做过多的工作(与视图进行通信,持久化到数据库等),但是有时我们需要更多的层次和更好的关注点分离,即一个或者多个视图模型。然后,我们可以在视图模型上无例外地应用验证(验证对于不同的上下文(例如,Web服务/网站/等)可能有所不同),并将异常验证保留在业务模型中(以防止模型损坏)。我们将需要一个(或者多个)应用程序服务层来将视图模型与业务模型进行映射。不应使用通常与特定框架(例如NHibernate Validator)相关的验证属性来污染业务对象。