【问题标题】:Error Handling Should I throw exception? Or handle at the source?错误处理 我应该抛出异常吗?还是从源头处理?
【发布时间】:2010-11-24 01:17:19
【问题描述】:

我有这种格式

asp.net MVC 视图 -> 服务层 -> 存储库。

因此视图调用了其中具有业务/验证逻辑的服务层,该逻辑又调用了存储库。

现在我的服务层方法通常有一个 bool 返回类型,因此如果数据库查询顺利,我可以返回 true。或者如果它失败了。然后向用户显示一条通用消息。

我当然会用 elmah 记录错误。但是我不确定我应该如何达到这一点。

就像现在我的存储库有用于更新、创建、删除的 void 返回类型。

所以说如果更新失败,我是否应该在我的存储库中有一个抛出错误的 try/catch,然后我的服务层捕获它并发出 elmah 信号并返回 false?

或者我应该让这些存储库方法返回一个“bool”,尝试/捕获存储库中的错误,然后将“true”或“false”返回给服务层,然后返回“true”或“false”给服务层风景?

异常处理仍然让我困惑如何处理错误以及何时抛出以及何时捕获错误。

【问题讨论】:

    标签: c# asp.net-mvc exception-handling


    【解决方案1】:

    我一直使用的经验法则是:

    • 在低级别,当操作由于异常情况无法完成时抛出。
    • 在中间层,捕获多个异常类型并重新包装在一个异常类型中。
    • 在最后负责的时刻处理异常。
    • 文件!

    这是一个多层 ASP.NET MVC 应用程序(UI、控制器、逻辑、安全、存储库)的伪代码示例:

    1. 用户点击提交按钮。
    2. 执行控制器操作并调用逻辑(业务)层。
    3. 使用当前用户凭据调用安全性的逻辑方法
      • 用户无效
        • 安全层抛出SecurityException
        • 逻辑层捕获、包装在 LogicException 中并带有更通用的错误消息
        • 控制器捕获 LogicException,重定向到错误页面。
      • 用户有效且安全返回
    4. 逻辑层调用存储库以完成操作
      • 存储库失败
        • 存储库抛出 RepositoryException
        • 逻辑层捕获、包装在 LogicException 中并带有更通用的错误消息
        • 控制器捕获 LogicException,重定向到错误页面。
      • 存储库成功
    5. 逻辑层返回
    6. 控制器重定向到成功视图。

    注意,逻辑层只抛出一个异常类型——LogicException。任何冒泡的低级异常都会被捕获,并包装在一个新的 LogicException 实例中,然后抛出该实例。这给了我们很多好处。

    首先,堆栈跟踪是可访问的。其次,调用者只需要处理一个异常类型而不是多个异常。第三,可以处理技术异常消息以显示给用户,同时仍保留原始异常消息。最后,只有负责处理用户输入的代码才能真正了解用户的意图,并在操作失败时确定适当的响应。存储库不知道 UI 是否应该显示错误页面或请求用户使用不同的值重试。控制器知道这一点。


    顺便说一句,没有什么说你不能这样做:

    try
    {
      var result = DoSomethingOhMyWhatIsTheReturnType();
    }
    catch(LogicException e)
    {
      if(e.InnerException is SqlException)
      {
        // handle sql exceptions
      }else if(e.InnerException is InvalidCastException)
      {
        // handle cast exceptions
      }
      // blah blah blah
    }
    

    【讨论】:

    • 此技术存在以下问题: 1) 任何时候低级程序集引入更多检查或更多异常,它上面的所有层都应该更改以正确包装异常 - 否则包装只会变成美化包罗万象,并没有太大意义 2)对不同的错误使用不同的异常类型可以提高错误处理的保真度,如果我们只有一个异常,无论您在哪里处理它,您都必须编写您的大小写逻辑(如果LogicException 是因为一些东西而不是句柄,否则就让它过去)。可能还有其他人......
    • 1) 哇。无论如何,您都必须捕获这些新异常,那么为什么在它处于较低级别而不是在 UI 时哭泣呢? 2)如果你做了很多事情,你可能会在 UI 级别捕获数十种异常类型。这非常烦人,并且在你的代码中乱扔了真正不应该存在的 UI 逻辑。同样,您正在包装原始异常,因此您仍然可以捕获单个类型并检查 InnerException 以满足您的所有保真度需求,而无需大量捕获。 3)可能还有其他反驳,但我懒得一一列举。
    • @Charles Prakash Dasari:根据故障类型区分异常类型并不是很有用。更有用的是通过系统状态来区分它们。例如,如果一个例程应该更新数据库中的一条记录但它不起作用,那么用户可能需要知道它是由于锁定超时还是 TCP 连接重置而发生的,但从程序的角度来看,更重要的是知道是否......
    • @Charles Prakash Dasari: ...(1) 数据库未受影响; (2) 数据库可能要么完全没有改动,要么完全更新,但程序无法判断是哪个; (3) 数据库可能处于某些部分更新、可能已损坏的状态。如果中间层代码捕获并包装了一个数据库异常,它可以很好地猜测这些条件中的哪一个适用。如果中间层不区分这些条件,高层代码就不可能做到这一点。
    【解决方案2】:

    我喜欢这样思考异常处理:你定义你的方法签名,你期望​​做什么。现在,如果您无法做到这一点,那么您必须抛出异常。因此,如果您希望根据您拥有的输入数据失败(忽略环境状态),那么您的方法签名必须指示操作是成功还是失败。但是,如果您的方法不希望根据您的输入而失败(同样,忽略所有其他环境状态),那么当方法失败时会出现异常。

    考虑这两个 API:

    int int.Parse(string integerValue); // In this case, the method will return int
                                        // or it will die! That means your data must be
                                        // valid for this method to function.
    
    bool int.TryParse(string integerValue, out number); // In this case, we expect the data
                                                        // we passed in might not be fully
                                                        // valid, hence a boolean.
    

    【讨论】:

      【解决方案3】:

      虽然返回错误(或成功)代码通常是更好的方法,但与返回代码或静默抑制错误相比,异常有一个巨大的优势:至少您不能忽略它们!

      不要为简单的流控制滥用异常——那是最愚蠢的做法。

      但是如果你的一个函数真的遇到了“异常”问题,那么肯定会抛出一个异常。调用者必须要么明确地处理它,从而知道发生了什么,否则它会轰炸他。

      仅仅返回一个错误代码是危险的,因为调用者可能只是懒得检查代码并且可能仍然继续 - 即使在您的应用程序的逻辑中,确实有问题需要处理。

      所以:不要滥用异常,但如果发生真正的异常需要调用者对其进行处理,我肯定会建议使用该机制来发出异常情况的信号。

      至于处理异常:处理那些你真正可以处理的。例如。如果您尝试保存文件并遇到安全异常,请向用户显示一个对话框,要求保存到其他位置(因为他可能无权保存到他想要的位置)。

      但是,你不能真正处理的异常(你想对“OutOfMemory 异常”做什么,真的吗?)应该保持不变——也许调用堆栈更靠前的调用者可以 处理这些 - 或不处理。 马克

      【讨论】:

      • 我在没有异常的系统中使用的一种方法是将锁存错误代码与应用程序代码在下一次 I/O 操作或下一次 I/O 操作之前明确确认/重置错误的要求相结合。 /O 操作将触发致命错误(在一种情况下,我有非致命错误捕获堆栈和参数值,然后可以检查是否在确认/重置错误之前尝试了另一个 I/O 操作)。这种方法不太适合“现代”范式,但如果异常不可用,则效果很好。
      【解决方案4】:

      首先,没有唯一的方法,当然也没有完美的方法,所以不要想太多。

      通常,您希望对异常情况使用异常(异常会导致性能开销,因此过度使用它们,尤其是在“循环”情况下可能会产生性能影响)。因此,假设存储库由于某种原因无法连接到数据库服务器。然后你会使用一个例外。但是,如果存储库通过 id 执行对某个对象的搜索并且未找到该对象,那么您将希望返回 null 而不是抛出一个异常,说明 ID x 的对象不存在。

      验证逻辑也是如此。由于它正在验证,因此假设有时输入不会验证,因此在这种情况下,最好从验证服务返回 false (或者可能是更复杂的类型,包括一些关于它为什么没有验证的附加信息)。但是如果验证逻辑包括检查用户名是否被使用并且由于某种原因它不能这样做,那么你会抛出一个异常。

      所以说如果更新失败我应该 在我的存储库中有一个 try/catch 抛出错误,然后我的服务 layer 抓住它并做 elmah 发出信号并返回 false?

      为什么更新会失败?发生这种情况是否有一个很好的理由,这是正常过程的一部分?然后,如果由于奇怪的原因而发生异常(假设某些东西在更新之前删除了正在更新的记录),则不要抛出异常,然后异常接缝是合乎逻辑的。实在是没有办法从这种情况中恢复过来。

      【讨论】:

      • "通过 id 搜索某个对象,但找不到该对象,那么您想返回 null" ...然后希望您的调用者检查 null?如果您希望 ID 存在(不是用户输入),通常抛出。如果您不确定,请考虑 Try-Parse 模式。见blogs.msdn.com/kcwalina/archive/2005/03/16/396787.aspx
      • 由调用者了解调用函数的预期结果。如果调用者忽略了签名/文档声明函数可以返回空值的事实,那么调用者有错,而不是函数。在这种情况下,返回空值是完全有效的 IMOH。
      • 当然,您也可以争辩说调用者可能不希望抛出异常,并且它不会费心用 Try Catch 包装调用。无论哪种方式,您都不能真正强迫调用者做任何事情。因此,将调用者的操作用作返回 null 或抛出异常的决定因素是一个有争议的问题。
      猜你喜欢
      • 2011-04-07
      • 2023-03-06
      • 1970-01-01
      • 2019-11-12
      • 2013-02-07
      • 2013-07-24
      • 1970-01-01
      • 1970-01-01
      • 2016-08-14
      相关资源
      最近更新 更多