微软异常处理块——难道它不是过度工程的完美例子吗?

19
自从微软引入应用程序块以来,我一直遇到使用异常处理应用程序块的人。最近我自己仔细研究了一下,并将基本功能总结如下(如果您已经知道它的作用,请跳过以下内容):

The exception handling application block aims to centralize and make fully configurable with config files the following key exception handling tasks:

  • Logging an Exception
  • Replacing an Exception
  • Wrapping an Exception
  • Propagating an Exception
  • etc.

The library does that by having you modify your try catch blocks as follows:

try
{
  // Run code.
}
catch(DataAccessException ex)
{
    bool rethrow = ExceptionPolicy.HandleException(ex, "Data Access Policy");
    if (rethrow)
    {
        throw;
    }
}

Based on what is specified in the app.config for the policy name (see here for docs), HandleException will either ...

  • throw a completely new exception (replace the original exception)
  • wrap the original exception in a new one and throw that
  • swallow the exception (i.e. do nothing)
  • have you rethrow the original exception

Additionally you can also configure it to do more stuff beforehand (e.g. log the exception).

现在我的问题是:我完全看不出来,使异常被替换、包装、忽略或重新抛出是否有益的可配置性。根据我的经验,这个决定必须在编写代码时做出,因为当您更改异常处理行为时,通常必须更改周围或调用代码。
例如,当您重新配置以便在特定点抛出的特定异常现在被忽略而不是重新抛出时,您的代码可能会开始表现不正确(在catch块之后可能会有不能执行的代码)。所有其他可能的异常处理更改也是如此(例如,替换->重新抛出,忽略->包装)。
因此,对我来说,底线是异常处理块解决了实际上不存在的问题。异常记录和通知部分是好的,但所有其他东西是否只是过度工程的完美例子?

好问题。+1。我还在http://stackoverflow.com/questions/438073#438230中添加了“异常排除联接”参数。你能看一下吗? - VonC
6个回答

13

如果您将异常用作控制流程,则应避免使用基于策略的异常处理。

但对于您希望视为不可恢复的异常(后台任务失败、套接字断开连接、文件已删除等),您可能希望拥有可配置的基于策略的异常处理。

例如,如果您正在开发 API,您可能希望使 API 中的每个函数仅引发标准异常 (ArgumentException 等),以及在出现内部非标准异常(例如 MyLibraryException)的情况下,引发自己的库特定异常。在这种情况下,重要的是某些事情没有正常工作。您不是在分解异常并找出哪里出了问题。您只是承认某些事情出了问题,并且您现在应该做某些事情。

那个“某些”事情应该是可配置的,因为您做什么并不重要。向用户显示消息框?模态还是非模态?记录异常?您想如何记录异常?调用日志记录 Web 服务?附加到日志文件?写入 Windows 事件日志?在数据库中插入条目?在两个数据库中插入条目?对于应用程序的其余部分来说,如何处理异常的选择与其完全无关。

(顺便说一句,这并不是我处理可配置的基于策略的异常处理的方式。我更倾向于 AOP 样式,例如在容器中注册异常记录器拦截器。)


我同意你可能想在应用程序崩溃之前配置会发生什么(通知用户、记录异常、向支持发送电子邮件等)。你似乎用“控制流”来指代所有其他内容(吞咽、重新抛出等),对吗? - user49572
不一定 - 这取决于软件。如果您将异常视为可恢复的,则将其视为控制流。如果您将它们视为不可恢复的,则可能会对此应用程序块感兴趣。 - yfeldblum
如果将异常视为可恢复的,那么当相应的插入语句失败时,发送更新语句到SQL Server就是控制流程。 - yfeldblum
通常情况下,你无法完全避免将一些异常视为可恢复的,对吧?一个例子是 System.IO.FileStream 构造函数,它可能会抛出许多异常,大多数应用程序都希望从中恢复。 - user49572
1
手头的任务可能是无法恢复的。整个应用程序可能希望恢复,并通知用户该任务失败,并询问用户是否要重试该任务。对于特定任务的上下文,异常是不可恢复的;对于应用程序而言,它是可恢复的。 - yfeldblum
按照这个逻辑,每个抛出的异常都是不可恢复的(因为它几乎总是中止了正在进行的操作),也许稍后会变得可恢复(即当您在调用堆栈的某个位置捕获它并决定可以对其进行处理时),对吗? - user49572

5

我在开发没有可恢复状态的函数时遇到了这个问题。我相信这种基于策略的异常处理实际上是有用的,并确保所有参与该项目的其他开发人员都遵守不可恢复异常的标准。

我同意上面的帖子,如果您将它们用于控制流程,则可能需要远离基于策略的异常。


4
我必须同意这个说法:“异常处理块解决了实际上并不存在的问题。”自从它发布以来,我已经使用了这个块,但我很少觉得使用它真正带来了多少好处。嗯,也许会有点头疼。 :)
在开始之前,我要承认我确实喜欢WCF服务边界的异常屏蔽功能。然而,这是最近添加的,不需要编码--只需要属性和配置,并且通常不是该块的销售点。这是微软在http://msdn.microsoft.com/en-us/library/cc309250.aspx中展示它的方式。

alt text

对我来说,上面的内容是流程控制。
try
{
  // Run code.
}
catch(DataAccessException ex)
{
    bool rethrow = ExceptionPolicy.HandleException(ex, "Data Access Policy");
    if (rethrow)
    {
        throw;
    }
}

当您将流程控制方面与上述代码相结合,您绝对不会使错误的代码看起来很糟糕。(别管 if (rethrow) 构造不提供太多抽象。) 这可能会使不熟悉策略定义的开发人员难以进行维护。如果配置文件中的 postHandlingAction 发生更改(这可能会在某个时候发生),您可能会发现应用程序表现出意外的方式,这些方式从未经过测试。
也许如果该块没有处理流程控制而只是允许您将处理程序链接在一起,我就不会觉得它那么令人反感了。
但也可能不是这样。事实上,我实际上不记得曾被要求做以下事情:
  • 在处理异常时添加新功能(例如,除了记录事件日志外,还发送电子邮件。实际上,如果您已经自己记录了异常,则记录块可以自行完成。:) )
  • 跨整个“策略”更改异常处理行为(吞噬、重新抛出或抛出新异常)。

我并不是说这种情况从未发生(我相信有人有过这样的经历!);我只是觉得异常处理应用程序块会使程序更难理解和维护,而且这通常会超过该块提供的功能。


2
我认为这并不是过度工程。实际上,异常可以通过一个中央处理程序传递的事实在我的工作中非常有用。我曾经遇到过一些开发人员会吞噬每一个异常——无论是否已处理——以便它不会冒泡到顶部,在最终用户面前呈现出令人惊慌的东西,或者在未被捕获时严重破坏服务/守护进程。
现在有了这个策略,我们可以随时开始记录日志,而不必重新启动或不必要地在整个应用程序中散布日志记录逻辑。现在我们可以监视异常等问题,而不必使应用程序下线。
如果你问我,这是很聪明的编程...

1
关于开发人员吃掉异常的问题:使用代码分析工具(例如FxCop)检查他们的代码,以确保他们不会仅仅吞咽所有异常,这难道不是一个更好的主意吗? - user49572
1
是的。是的,即使检测到了问题,也很难为已经投入生产的东西进行重新任务分配/测试。并不是我们不尽力去捕捉/预防,但没有哪个代码库能够免受急于求成的经理和愿意迎合他们的开发人员的影响。 - MikeJ

1
简单来说:如果你想在发布代码和调试代码中使用不同的异常处理方式,那么这将非常有用。

你有现实世界的例子吗?在调试模式下,您会重新抛出特定的异常,但在发布时却将其吞噬吗? - user49572
我一直在寻找这些真实世界的例子来证明它的有用性,但到目前为止还没有成功。 - Marcel Gheorghita

0
在我看来,使用异常处理块的真正价值始于你的catch块中的一行代码:
bool rethrow = ExceptionPolicy.HandleException(ex, "Data Access Policy");
只有一行代码 - 你能想象得到多么简单吗?在这一行代码背后,根据配置的策略,对策略进行分配/更新变得非常容易,而无需重新编译源代码。
正如所提到的,异常策略实际上可以执行许多操作 - 任何在你的策略中定义的操作。将复杂性隐藏在catch块中所示的一行代码后面,这就是我看到的真正价值所在。
我会说它很复杂,但并不过度设计。因此,我的想法是,在维护期间,当您需要通过轻松更改配置来更改处理/记录异常的方式时,将会看到巨大的回报,并且仍然具有相同的一行源代码。

1
好的,但是你是否曾经遇到过只需要更改异常传播(这个特定的异常在过去已经被重新抛出了,现在让我们将其包装在另一个异常中并在未来抛出),而不更改所有周围的代码的需求? - user49572

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