业务对象,validation和例外
我一直在阅读关于exception及其使用的一些问题和答案。 似乎是一个强烈的意见,认为只有例外,未经处理的案件才能提出例外。 所以这导致我想知道如何validation与业务对象的工作。
比方说,我有一个业务对象的getters / setter对象的属性。 假设我需要validation值在10到20之间。这是一个业务规则,所以它属于我的业务对象。 所以这似乎暗示我的validation码在我的设置。 现在我有我的UI数据绑定到数据对象的属性。 用户input5,所以规则需要失败,用户不能移出文本框。 。 UI是数据绑定到属性,所以setter将被调用,规则检查和失败。 如果我从业务对象中提出exception来说规则失败,那么UI将会select这个exception。 但这似乎违背例外的首选用法。 鉴于这是一个二传手,你不会有一个“结果”的二传手。 如果我在对象上设置另一个标志,那么这意味着UI必须在每个UI交互之后检查该标志。
那么validation如何工作呢?
编辑:我可能在这里使用了一个过度简化的例子。 类似上面的范围检查可以很容易地由用户界面处理,但如果这种情况更复杂,例如,业务对象根据input计算一个数字,如果计算出的数字超出范围,则应该被拒绝。 这是更复杂的逻辑,不应该在UI中。
还根据已经input的字段考虑进一步input数据。 例如,我必须在订单上input一个项目,才能获得库存量,当前成本等特定信息。用户可能需要这些信息来决定进一步input(需要订购多less个单位),或者可能需要订购为了进一步validation完成。 如果该项目无效,用户是否可以input其他字段? 重点是什么?
你想在Paul Stovell关于数据validation的杰出工作中钻研一下。 他在这篇文章中一次总结了他的想法。 我恰好分享了他在这个问题上的观点,我在自己的图书馆里实施了这个观点。
用保罗的话来说,这里有一个缺点,就是在setters中抛出exception(根据Name
属性不应该为空的示例):
- 有时候你可能真的需要一个空的名字。 例如,作为“创build帐户”窗体的默认值。
- 如果您在保存之前依靠此来validation任何数据,则会错过数据已无效的情况。 那么,我的意思是,如果你从数据库中加载一个空名的帐号,并且不改变它,你可能永远不知道它是无效的。
- 如果不使用数据绑定,则必须用
try/catch
块编写大量代码才能向用户显示这些错误。 试图在用户填写表单时显示错误变得非常困难。- 我不喜欢为非例外事件抛出exception。 将用户名设置为“Supercalafragilisticexpialadocious”的用户不是一个例外,这是一个错误。 这当然是个人的事情。
- 这使得很难列出所有被破坏的规则。 例如,在某些网站上,您会看到validation消息,如“必须input姓名,必须input地址,必须input电子邮件地址” 。 要显示,你将需要很多的
try/catch
块。
这里有一个替代解决scheme的基本规则:
- 拥有一个无效的业务对象没有任何问题,只要你不试图坚持它。
- 应该可以从业务对象中检索任何和所有破碎的规则,以便数据绑定以及您自己的代码可以查看是否存在错误并正确处理它们。
假设你有单独的validation和坚持(即保存到数据库)的代码,我会做以下几点:
-
UI应该执行validation。 不要在这里抛出exception。 您可以提醒用户错误,并防止保存logging。
-
你的数据库保存代码应该为坏的数据抛出无效的参数exception。 这样做是有道理的,因为在这一点上你不能进行数据库写入。 理想情况下,这应该永远不会发生,因为UI应该防止用户保存,但是仍然需要它来确保数据库的一致性。 你也可以从没有UI数据validation的界面(比如批处理更新)中调用这个代码。
我一直是Rocky Lhotka在CSLA框架中的一个迷(如Charles所提到的)。 通常,无论是由setter驱动还是通过调用明确的Validate方法,BrokenRule对象的集合都由业务对象在内部维护。 UI只需要检查对象上的IsValid方法,然后检查BrokenRules的数量,并对其进行适当的处理。 或者,你可以很容易地让Validate方法引发一个UI可以处理的事件(可能是更干净的方法)。 您还可以使用BrokenRules列表以摘要forms或在相应字段旁显示错误消息。 尽pipeCSLA框架是用.NET编写的,但总体方法可以用于任何语言。
在这种情况下,我不认为抛出exception是最好的主意。 我绝对遵循学派的思想,说exception应该是特殊的情况下,这是一个简单的validation错误不是。 提出一个OnValidationFailed事件将是更清洁的select,在我看来。
顺便说一句,我从来不喜欢在用户处于无效状态时不让用户离开的想法。 有很多情况下,您可能需要暂时离开现场(也许先设置其他字段),然后再返回并修复无效字段。 我认为这只是一个不必要的不便。
你可能想把validation移到getters和setter之外。 您可以有一个名为IsValid的函数或属性来运行所有的validation规则。 t会填充所有“Broken Rules”的字典或散列表。 这个词典将暴露给外部世界,你可以用它来填充你的错误信息。
这是在CSLA.Net采取的方法。
例外情况不应该作为validation的正常部分抛出。 业务对象内部调用的validation是最后一道防线,只有当UI未能检查时才会发生。 因此,它们可以像其他任何运行时exception一样对待。
请注意,在定义validation规则和应用它们之间存在差异。 您可能希望在业务逻辑层中定义(即编写或注释)您的业务规则,但是可以从UI调用它们,以便可以以适合该特定UI的方式处理它们。 处理的方式会因不同的用户界面而有所不同,例如基于表单的networking应用和ajaxnetworking应用。 exception现场validation提供了非常有限的处理选项。
许多应用程序复制它们的validation规则,例如在javascript中,域对象约束和数据库约束。 理想情况下,这个信息只会被定义一次,但是实现这个信息可能是一个挑战,需要横向思考。
也许你应该看看有客户端和服务器端validation。 如果有什么东西滑过客户端validation,那么如果您的业务对象会失效,则可以随意抛出exception。
我使用的一种方法是将自定义属性应用于描述validation规则的业务对象属性。 例如:
[MinValue(10), MaxValue(20)] public int Value { get; set; }
这些属性可以被处理并用于自动创build客户端和服务器端validation方法,以避免重复业务逻辑的问题。
我肯定会提倡客户端和服务器端validation(或者在各个层面validation)。 这在通过物理层或进程进行通信时特别重要,因为抛出exception的代价变得越来越昂贵。 而且,等待validation的链条越晚,浪费的时间就越多。
至于使用例外或不使用数据validation。 我认为可以在进程中使用exception(虽然仍然不是优选的),但在进程之外,调用一个方法来validation业务对象(例如在保存之前),并让该方法返回操作的成功以及任何validation错误 。 错误不是例外。
当validation失败时,Microsoft会从业务对象中引发exception。 至less,这就是企业库的validation应用程序块的工作方式。
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; } }
您的业务对象应该为错误input引发exception,但是这些exception不应该在正常程序运行的过程中抛出。 我知道这听起来是矛盾的,所以我会解释一下。
每个公共方法应该validation它的input,并且当它们不正确时抛出“ArgumentException”。 (私有方法应该使用“Debug.Assert()”来validation它们的input以简化开发,但这是另一回事)。关于validation公共方法(当然是属性)的input的这个规则对于应用程序的每一层。
当然,软件接口的要求应该在接口文档中详细说明,调用代码的工作是确保参数是正确的,并且不会抛出exception,这意味着UI需要validation在将它们交给业务对象之前进行input。
尽pipe上面给出的规则几乎不会被破坏,但有时候业务对象validation可能非常复杂,复杂性不应该强加到UI上。 在这种情况下,BO的接口允许在接受的方面有一些余地,然后提供一个明确的Validate(out string [])谓词来检查属性并给出需要改变的反馈。 但是请注意,在这种情况下,仍然存在定义明确的接口需求,并且不需要抛出任何exception(假设调用代码遵循规则)。
在后一个系统之后,我几乎从不对属性设置者进行早期validation,因为这个属性的使用会变得复杂(但是在这个问题中,我可能会这样做)。 (顺便说一下,请不要因为数据不好而阻止我退出某个领域,当我无法在表单中find某个表单时,我会产生恐吓症状,我会在一分钟后回来修复它,我保证!好吧,我现在感觉好多了,对不起。)
这取决于你将进行什么样的validation以及在哪里。 我认为应用程序的每一层都可以很容易地被保护以免受到不好的数据影响,而且这样做太容易了,不值得。
考虑一个多层应用程序和每个层的validation要求/设施。 对象这个中间层似乎是在这里辩论的。
-
数据库
通过列约束和引用完整性保护自己免于无效状态,这将导致应用程序的数据库代码抛出exception -
目的
? -
ASP.NET / Windows窗体
使用validation器例程和/或控件保护表单的状态(不是对象), 而不使用exception(winforms不附带validation器,但在msdn上有一个很好的系列描述了如何实现它们 )
假设你有一张桌子,上面有一张酒店客房的列表,每一列都有一个叫“床”的床的列。 该列最明智的数据types是无符号小整数*。 你也有一个简单的ole对象与Int16 *属性称为“床”。 问题是,你可以坚持-4555到一个Int16,但是当你坚持数据到数据库你会得到一个exception。 哪个好 – 我的数据库不应该被允许说酒店房间的床位不到零,因为酒店房间的床位不能less于零。
*如果你的数据库可以代表它,但我们假设它可以
*我知道你可以在C#中使用ushort ,但为了这个例子的目的,我们假设你不能
对于对象是否应该代表您的业务实体,或者它们是否代表您的表单状态,存在一些困惑。 当然,在ASP.NET和Windows Forms中,表单完全有能力处理和validation自己的状态。 如果在ASP.NET窗体上有一个文本框用于填充同一个Int16字段,那么您可能会在页面上放置一个RangeValidator控件,该控件在将其分配给对象之前对input进行testing。 它可以防止你input一个小于零的值,并且可能会阻止你input一个大于30的值,这个值足以应付你能想象到的最糟糕的跳蚤宿舍。 回发时,您可能会在构build对象之前检查页面的IsValid属性,从而阻止您的对象performance为小于零的床位,并阻止您的设置者使用不应该保持的值。
但是你的对象仍然可以表示不到零张床,而且如果你在一个不涉及将validation集成到其中的图层的场景(你的表单和你的数据库)中使用这个对象的话,那么你就不幸运了。
为什么你会在这种情况下? 这一定是非常特殊的情况! 因此,setter在收到无效数据时需要抛出exception。 它不应该被抛出,但它可能是。 您可能正在编写Windows窗体来pipe理对象以replaceASP.NET窗体,并且在填充对象之前忘记validation范围。 您可以在计划任务中使用该对象,根本没有用户交互,并将其保存到数据库的不同但相关的区域,而不是该对象映射到的表。 在后一种情况下,你的对象可以进入一个无效的状态,但是直到其他操作的结果开始受到无效值的影响,你才会知道。 如果你正在检查它们并抛出exception,那就是。
我倾向于认为业务对象在传递违反业务规则的值时应该抛出exception。 但是,似乎winforms 2.0数据绑定体系结构是相反的,所以大多数人都被用来支持这种体系结构。
我同意shabbyrobe的最后一个答案:业务对象应该被构build为可用,并在多个环境中正确工作,而不仅仅是winforms环境,例如,业务对象可以用于SOAtypes的Web服务,命令行界面,asp .net等。在所有这些情况下,对象应该正确行为并保护自己免受无效数据的侵害。
经常被忽视的一个方面也是在pipe理1-1,1-n或nn关系中的对象之间的协作时会发生什么事情,如果这些关系也接受增加无效的协作者并且维持一个应该被检查或应该被检查的无效状态标志它积极拒绝添加无效的合作。 我必须承认,我深受Jill Nicola等人的Streamlined Object Modeling(SOM)方法的影响。 但还有什么是合乎逻辑的。
接下来的事情是如何使用Windows窗体。 我正在为这些场景的业务对象创build一个UI包装器。
正如Paul Stovell的文章所述,您可以通过实现IDataErrorInfo接口在业务对象中实现无错误的validation。 这样做将允许用户错误通知WinForm的ErrorProvider和WPF的绑定与validation规则 。 validation对象属性的逻辑存储在一个方法中,而不是存储在每个属性获取器中,而且您不必使用CSLA或validation应用程序块等框架。
至于阻止用户将焦点从文本框转移出去,首先,这通常不是最佳实践。 用户可能希望不按顺序填写表单,或者,如果validation规则依赖于多个控件的结果,则用户可能必须填写虚拟值才能摆脱一个控件来设置另一个控件。 也就是说,这可以通过将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, ""); } }
使用存储在业务对象中的规则进行此validation比较棘手,因为在焦点更改和数据绑定业务对象更新之前调用了validation事件。
您可能想要考虑Spring框架所采用的方法 。 如果你正在使用Java(或者.NET),你可以直接使用Spring,但即使你不使用,你仍然可以使用这种模式。 你只需要编写自己的实现。
在你的情况下抛出一个例外是好的。 你可以考虑这种情况是一个真正的exception,因为某些东西试图设置一个整数string(例如)。 商业规则缺乏对你的意见的了解意味着他们应该考虑这个案例,并将其归还给观点。
无论您在将input值发送到业务层之前是否validationinput值都取决于您,我认为只要您在整个应用程序中遵循相同的标准,那么您最终将得到干净可读的代码。
你可以使用上面指定的spring框架,只需要小心尽可能多的链接文档指示编写的代码不是强types的,IE可能会在运行时得到错误,在编译时无法获取。 这是我尽量避免的事情。
目前我们这样做的方式是,我们从屏幕上获取所有input值,将它们绑定到数据模型对象,并在值出错时抛出exception。
根据我的经验,validation规则在应用程序中的所有屏幕/表单/进程中很less是通用的。 像这样的情况是很常见的:在添加页面上,Person对象可能没有姓,但在编辑页面上必须有姓。 在这种情况下,我认为validation应该发生在对象之外,或者规则应该注入到对象中,这样规则就可以根据上下文而改变。 有效/无效应该是validation后的对象的显式状态,或者可以通过检查集合的失败规则来派生。 失败的商业规则不是一个例外恕我直言。
如果数据无效,您是否考虑过在二传手中引发一个事件? 这将避免抛出exception的问题,并且将消除显式检查对象为“无效”标志的需要。 你甚至可以传递一个参数,指出哪个字段validation失败,使其更加可重用。
事件的处理程序应该能够把焦点放回适当的控制,如果需要的话,它可以包含任何需要通知用户错误的代码。 此外,您可以简单地拒绝挂接事件处理程序,并根据需要自由忽略validation失败。
我认为这是抛出exception的例子。 你的财产可能没有任何上下文来纠正问题,因为这样的例外是有条件的,如果可能的话,调用代码应该处理这种情况。
如果input超出了业务对象所实现的业务规则,我会说这是一个不由业务对象处理的情况。 所以我会抛出一个exception。 尽pipesetter会在你的例子中“处理”5,但是业务对象不会。
对于更复杂的input组合,虽然需要一个vaildation方法,否则最终会在整个地方散落相当复杂的validation。
在我看来,您必须根据允许/不允许的input的复杂性来决定走哪条路。
我认为这取决于你的商业模式有多重要。 如果你想要去DDD的方式,你的模型是最重要的。 因此,您希望它始终处于有效状态。
在我看来,大多数人都试图做太多(与意见沟通,坚持数据库等)与域对象,但有时你需要更多的层次和更好的分离关注,即一个或多个视图模型。 然后你可以在你的视图模型上应用没有例外的validation(对于不同的上下文,比如Web服务/网站等等,validation可能是不同的),并且在你的商业模型中保持exceptionvalidation(防止模型被破坏)。 您将需要一个(或更多)应用程序服务层来将您的View Model与您的业务模型进行映射。 业务对象不应该被与通常与特定框架相关的validation属性(例如NHibernatevalidation器)所污染。