CRUD类库设计,返回有用的业务逻辑失败信息,而不是异常。

6

我正在构建一个基本的CRUD库,预计将在本地(添加引用)和WCF(添加服务引用)环境中使用。

对于CRUD设置中具有更复杂业务规则的创建、更新和删除部分,哪种是最好的返回类型?

我希望能够尽量减少网络通信次数,但同时也要向客户端提供有意义的信息,以便在操作未通过业务逻辑但在技术上有效时(因此不是异常情况)进行处理。

以Person类的CRUD为例,该类具有以下字段:FirstName、MiddleName、LastName和Date of Birth。其中,First、Last和DOB是必填项,而Middle不是。

我应该如何将业务逻辑失败的信息传达回客户端?例如,“您必须为FirstName指定一个值。”

  1. 这是否应该抛出异常?(感觉不像是异常情况,所以我认为不是,但我可能错了。)
  2. 应该使用void和“out”参数吗?如果是,它应该是什么类型?
  3. 应该使用对象返回类型并在其中放置有关发生情况的数据吗?
  4. 我完全忽略了其他选项吗?
6个回答

3

为了提高性能,应该在客户端层执行初始输入验证。(即:不要将错误数据发送到网络。) 在客户端层,输入验证失败实际上是一个非常预期的问题,不应该抛出异常。但是,如果你在客户端层放置了这个“早期”验证,那么在任何更深的层次遇到数据验证失败都可以合理地视为意外问题,因此在这些层次中为数据验证错误抛出异常是不合适的。


3
为了保障安全,数据验证必须在服务器端进行,因为客户端可能会受到攻击。我在始终将客户端视为敌对方面做得正确吗? - Nate
3
你完全正确地将客户端视为敌对方。客户端验证并不能替代服务端验证,而是作为其补充。我所建议的是,如果你在客户端和服务接口都进行验证,大多数无效数据应该永远不会到达服务端,因此服务端可以将数据验证问题视为异常情况。 - Nicole Calinoiu
我经过一些反思又回到了这个问题,但由于这是一个老问题,可能无法接受答案。我得出了相同的结论。 - Nate

1

编辑:我应该指出,通常我会将验证内容抽象成业务层,在验证成功后处理验证并调用CRUD方法。

解决此问题的一种方法是返回一个“响应”类,其中包含您的库的使用者需要评估发生了什么以及下一步该怎么做的所有信息。您可以使用的一个非常基本的类示例如下:

public class Response<T> where T:BusinessObject
{
    public Response(T oldOriginalValue, T newValue)
    {
    }

    /// <summary>
    /// List of Validation Messages
    /// </summary>
    public List<ValidationMessage> ValidationMessages { get; set; }

    /// <summary>
    /// Object passed into the CRUD method
    /// </summary>
    public T OldValue { get; private set; }

    /// <summary>
    /// Object passed back from the CRUD method, with values of identity fields, etc populated
    /// </summary>
    public T NewValue { get; private set; }

    /// <summary>
    /// Was the operation successful
    /// </summary>
    public bool Success { get; set; }
}

public class ValidationMessage
{
    /// <summary>
    /// Property causing validation message (i.e. FirstName)
    /// </summary>
    string Property { get; set; }

    /// <summary>
    /// Validation Message text
    /// </summary>
    string Message { get; set; }
}

我想我应该提一下,我的真正的“CRUD”是使用LinqToSQL...我正在以与CRUD相同的形式公开业务逻辑。那样做有问题吗? - Nate
例如,我所谓的CRUD实际上是对输入进行验证,然后调用LinqToSQL来更新数据库或失败,而失败部分是我不确定如何处理的,但我喜欢你的想法。 - Nate
不,那很好。你的CRUD方法实际上是你的业务层,而LinqToSql则是你的数据层,你所称呼的CRUD方法将返回响应对象以指示成功或失败,并提供一系列消息来描述失败的原因。从LinqToSql中捕获的任何错误也可以放入你的ValidationMessage集合中(可能更好地命名为Messages,这样它就可以保存来自LinqToSql的验证和错误消息,而不会让任何人感到困惑)。 - Greg Andora

1

将返回包含故障详细信息的结果结构与抛出异常之间的辩论可以概括为“我能成功执行请求的操作吗?”您正在设计一个库,库如何通信失败模式是其中的一部分。

如果要求库创建用户对象但由于用户名未通过验证而无法创建,您可以传回一个空用户以及验证失败消息,并希望客户端代码测试返回值。 我(DCOM pre ATL 回忆录)的经验是,必须测试错误码的返回值很容易变得自满(或懒惰)并跳过它。

根据您描述的情况,使用异常并不是不可能的。 为库可以抛出的所有异常类型定义父异常。 这样,客户端就不必包含大量子异常列表,除非他们真的想要。 SqlException 就是这种情况的一个例子。


1
1. 这里是我应该抛出异常的地方吗?(感觉不像是一个异常情况,所以我认为不应该,但我可能错了)。
个人认为,您应该返回一个带有结果和任何验证错误的对象,而不是为数据验证抛出异常,无论是由于缺少信息(格式验证)还是业务逻辑验证。然而,我建议对与数据本身无关的错误抛出异常-例如:如果数据库提交失败,则应抛出异常等。
我的想法是,验证失败不是“异常情况”。我个人认为,用户只需不输入足够/正确/等数据即可搞砸的任何事情都不是异常情况-这是标准做法,并且应由API直接处理。
与用户正在执行的操作无关的事情(例如:网络问题、服务器问题等)是异常情况,并且需要抛出异常。
2. 我应该使用void和“out”参数吗?如果是这样,它应该是什么类型?
3. 我应该使用对象返回类型并在其中放置有关发生情况的数据吗?

我个人更喜欢第三种选择。"out"参数并不是很有意义。此外,您将希望从此调用返回多个状态信息 - 您将希望返回足够的信息来标记适当的属性为无效,以及任何完整的操作范围信息。

这可能需要一个类,其中至少包含提交状态(成功/失败格式/失败业务逻辑等),属性->错误映射列表(即:IDataErrorInfo样式信息)以及可能与特定属性无关但涉及操作的业务逻辑或建议属性值组合的错误列表。

4.我完全忽略了其他一些选项吗?

另一个选项,我相当喜欢的是将验证放在与业务处理层分开的单独程序集中。这使您可以在客户端上重用验证逻辑。

好处在于您可以大大简化和减少网络流量。客户端可以预先验证信息,并仅在数据有效时通过网络发送数据。

服务器可以接收良好的数据,并重新验证它,然后返回单个提交结果。我认为这应该至少有三个响应 - 成功、由于业务逻辑失败或由于格式错误而失败。这提供了安全性(您不必信任客户端),并向客户端提供有关未正确处理的信息,但避免了从客户端->服务器传递坏信息和从服务器->客户端传递验证信息,因此可以大大减少流量。
然后,验证层可以(安全地)将信息发送到CRUD层进行提交。

感谢您抽出时间撰写如此详细的回复。在您看来,大多数情况下最好的选择是什么?从其他回复中可以看出,#3非常受欢迎,您是否同意它是最好的“万能”解决方案?除了特殊或奇怪的边缘情况外? - Nate
1
如果你担心流量问题,我的第四个选项是第三个的变体,提供了一个在我看来更好的选择。它还使客户端验证变得轻松。否则,第三个选项是我最喜欢的。 - Reed Copsey

1
你可能会对Rob Bagby的这篇博客文章感兴趣;他描述了如何实现一个仓储来处理CRUD操作,并且,正如你所说,特别是如何实现验证,在出现问题时向客户端返回"RuleViolation"集合。

http://www.robbagby.com/silverlight/patterns-based-silverlight-development-part-ii-repository-and-validation/

[编辑] 对我来说,这是一个抛出异常的情况:如果创建用户需要名字,但没有提供名字,那么调用者没有使用正确的参数,并且没有按照预期的方式使用该方法。InvalidArgumentException听起来很合适。


1
那么你的方法会是这样吗?CreateUser(String firts, String middle, String Last, DateTime dob) 因为我更倾向于使用 CreateUser(User user),这是一个不好的设计想法吗?在非 WCF 版本中,我可能还会添加重载,但在 WCF 中我会使用接受对象的签名。 - Nate
当你抛出异常时,你如何知道在用户界面上指向用户的哪个区域?我喜欢当你忘记输入一个字段并点击提交时,应用程序不仅会给出一条消息,还会将光标聚焦在该字段上。 - Nate
方法名CreateUser意味着您将创建一个用户对象;如果您已经有一个用户对象要传递到该方法中,为什么还要创建一个新的呢? - Jeremy S
也许它应该被命名为CreateUserInDatabase,因为我认为作为客户端,我拥有用户的DataContract,我已经将它的属性绑定到我的UI字段上,当用户点击按钮时,我会将该对象提交给服务。 - Nate

1

你真的应该看看Rocky Lhotka在他的CSLA框架中实现的验证规则。

注意:我并没有建议大量使用他的框架,因为它存在一些耦合问题,这会破坏最新的.NET开发趋势中的一些SRP努力。

但是,他的框架确实利用了“自动”通知到UI层和与验证错误消息集成的功能,并支持Web/Winforms控件。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接