我应该如何防守?

时间:2009-06-27 17:10:40

标签: c# exception-handling defensive-programming

我正在使用一个用于创建数据库连接的小例程:

之前

public DbConnection GetConnection(String connectionName)
{
   ConnectionStringSettings cs= ConfigurationManager.ConnectionStrings[connectionName];
   DbProviderFactory factory = DbProviderFactories.GetFactory(cs.ProviderName);
   DbConnection conn = factory.CreateConnection();
   conn.ConnectionString = cs.ConnectionString;
   conn.Open();

   return conn;
}

然后我开始研究.NET框架文档,看看各种事情的记录行为是什么,并看看我是否可以处理它们。

例如:

ConfigurationManager.ConnectionStrings...

documentation表示如果无法检索集合,则调用 ConnectionStrings 会抛出ConfigurationErrorException。在这种情况下,我无法处理此异常,所以我会放手。


下一部分是 ConnectionStrings 的实际索引,以查找 connectionName

...ConnectionStrings[connectionName];

在这种情况下,ConnectionStrings documentation表示如果找不到连接名称,该属性将返回 null 。我可以检查是否发生了这种情况,并抛出一个异常,让某人高兴他们给了一个无效的connectionName:

ConnectionStringSettings cs= 
      ConfigurationManager.ConnectionStrings[connectionName];
if (cs == null)
   throw new ArgumentException("Could not find connection string \""+connectionName+"\"");

我重复同样的练习:

DbProviderFactory factory = 
      DbProviderFactories.GetFactory(cs.ProviderName);

GetFactory方法没有关于如果找不到指定ProviderName的工厂会发生什么的文档。没有记录返回null,但我仍然可以防御, 检查 为null:

DbProviderFactory factory = 
      DbProviderFactories.GetFactory(cs.ProviderName);
if (factory == null) 
   throw new Exception("Could not obtain factory for provider \""+cs.ProviderName+"\"");

接下来是构建DbConnection对象:

DbConnection conn = factory.CreateConnection()

同样,documentation没有说明如果无法创建连接会发生什么,但我再次检查是否有空返回对象:

DbConnection conn = factory.CreateConnection()
if (conn == null) 
   throw new Exception.Create("Connection factory did not return a connection object");

接下来是设置Connection对象的属性:

conn.ConnectionString = cs.ConnectionString;

文档没有说明如果无法设置连接字符串会发生什么。它会抛出异常吗?它会忽略它吗?与大多数例外一样,如果在尝试设置连接的ConnectionString时出错,那么我无法从中恢复。所以我什么都不做。


最后,打开数据库连接:

conn.Open();

DbConnection的Open method是抽象的,所以它取决于从DbConnection下降的任何提供者来决定他们抛出的异常。抽象的Open方法文档中也没有关于如果出现错误我可能会发生什么的指导。如果连接有错误,我知道我无法处理它 - 我必须让它冒泡,调用者可以向用户显示一些UI,让他们再试一次。


public DbConnection GetConnection(String connectionName)
{
   //Get the connection string info from web.config
   ConnectionStringSettings cs= ConfigurationManager.ConnectionStrings[connectionName];

   //documented to return null if it couldn't be found
    if (cs == null)
       throw new ArgumentException("Could not find connection string \""+connectionName+"\"");

   //Get the factory for the given provider (e.g. "System.Data.SqlClient")
   DbProviderFactory factory = DbProviderFactories.GetFactory(cs.ProviderName);

   //Undefined behaviour if GetFactory couldn't find a provider.
   //Defensive test for null factory anyway
   if (factory == null)
      throw new Exception("Could not obtain factory for provider \""+cs.ProviderName+"\"");

   //Have the factory give us the right connection object
   DbConnection conn = factory.CreateConnection();

   //Undefined behaviour if CreateConnection failed
   //Defensive test for null connection anyway
   if (conn == null)
      throw new Exception("Could not obtain connection from factory");

   //Knowing the connection string, open the connection
   conn.ConnectionString = cs.ConnectionString;
   conn.Open()

   return conn;
}

摘要

所以我的四行函数变成了12行,并且需要5分钟的文档查找。最后我确实捕获了一个允许方法返回null的情况。但实际上我所做的就是将访问冲突异常(如果我试图在空引用上调用方法)转换为 InvalidArgumentException

我还发现了两种可能存在 null 返回对象的情况;但我再次只为另一个交易了一个例外。

从积极的方面来看,它确实遇到了两个问题,并解释了异常消息中发生的事情,而不是发生在路上的坏事(即降压停在这里)

但值得吗?这有点矫枉过正吗?这防御性节目是否出错了?

14 个答案:

答案 0 :(得分:31)

手动检查配置并抛出异常并不比让框架在缺少配置时抛出异常更好。你只是复制了框架方法中发生的前置条件检查,它使你的代码冗长而没有任何好处。 (实际上,您可能通过将所有内容作为基本Exception类来删除信息。框架抛出的异常通常更具体。)

编辑:这个答案似乎有点争议,所以有点详细说明:防御性编程意味着“为意外做好准备”(或“偏执狂”),其中一种做法是做出大量的前提条件检查。在许多情况下,这是一种良好的做法,但是,与所有实践一样,成本应权衡利益。

例如,它没有提供任何好处来抛出“无法从工厂获得连接”异常,因为它没有说明为什么无法获得提供者 - 以及如果提供者为null,则下一行将抛出异常。因此,前置条件检查的成本(开发时间和代码复杂性)是不合理的。

另一方面,检查验证连接字符串配置是否存在可能是合理的,因为该异常可以帮助告诉开发人员如何解决问题。无论如何,您将在下一行中获得的null异常不会告诉缺少连接字符串的名称,因此您的前置条件检查确实提供了一些值。例如,如果您的代码是组件的一部分,则该值非常大,因为组件的用户可能不知道该组件需要哪些配置。

对防御性编程的不同解释是,您不仅应该检测错误条件,还应该尝试从可能发生的任何错误或异常中恢复。我不相信这是一个好主意。

基本上你应该只处理你可以某些事情的异常。无论如何都无法恢复的异常应该只是向上传递给顶级处理程序。在Web应用程序中,顶级处理程序可能只显示一般错误页面。但是,在大多数情况下,如果数据库处于脱机状态或缺少某些关键配置,则没有太多事情要做。

这种防御性编程有意义的一些情况是,如果您接受用户输入,并且该输入可能导致错误。例如,如果用户提供URL作为输入,并且应用程序尝试从该URL获取某些内容,则检查URL看起来是否正确并处理可能由请求引起的任何异常非常重要。这允许您向用户提供有价值的反馈。

答案 1 :(得分:13)

嗯,这取决于你的观众是谁。

如果你正在编写你期望被许多其他人使用的库代码,谁也不会跟你谈论如何使用它,那么它就不会有点过分。他们会感激你的努力。

(也就是说,如果你这样做,我建议你定义比System.Exception更好的例外,以便让那些想要捕获一些例外而不是其他例外的人更容易。)

但是,如果你自己(或者你和你的伙伴)只是打算使用它,那么显然它太过分了,最终可能会因为你的代码的可读性而受到伤害。

答案 2 :(得分:7)

我希望我能让我的团队像这样编码。大多数人甚至都没有得到防御性编程的观点。他们做的最好的事情是将整个方法包装在try catch语句中,并让所有异常由通用异常块处理!

向你致敬Ian。我能理解你的困境。我自己经历过同样的事情。但你所做的可能会帮助一些开发人员进行几个小时的键盘攻击。

请记住,当您使用.net框架API时,您对它的期望是什么?什么看似自然?对你的代码做同样的事情。

我知道这需要时间。但是质量是有代价的。

PS:你真的不必处理所有错误并抛出自定义异常。请记住,您的方法仅供其他开发人员使用。他们应该能够自己找出常见的框架异常。这不值得麻烦。

答案 3 :(得分:6)

您的“之前”示例具有清晰简洁的区别。

如果出现问题,框架最终会抛出异常。如果你无法对异常做任何事情,你也可以让它传播到调用堆栈。

然而,有时候,在框架内部引发异常时,实际上并没有解释实际问题是什么。如果您的问题是您没有有效的连接字符串,但框架会抛出“无效使用null”之类的异常,那么有时最好捕获异常并使用更有意义的消息重新抛出它。 / p>

我确实检查了很多空对象,因为我需要一个实际的对象来操作,如果对象是空的,抛出的异常将是倾斜的,至少可以说。但是我只检查空对象,如果我知道会发生什么。某些对象工厂不返回null对象;他们抛出异常,在这些情况下检查null将无用。

答案 4 :(得分:3)

我认为我不会写任何空引用检查逻辑 - 至少,不是你做的那样。

我从应用程序配置文件获取配置设置的程序会在启动时检查所有这些设置。我通常构建一个静态类来包含设置,并在应用程序的其他地方引用该类的属性(而不是ConfigurationManager)。有两个原因。

首先,如果应用程序配置不正确,它将不起作用。当我尝试创建数据库连接时,我想在程序读取配置文件时比在将来的某个时刻知道这一点。

其次,检查配置的有效性不应该是依赖于配置的对象的关注点。如果您已经预先执行了这些检查,那么在整个代码中自己插入检查是没有意义的。 (当然也有例外 - 例如,长时间运行的应用程序,您需要能够在程序运行时更改配置并将这些更改反映在程序的行为中;在这样的程序中,您需要每当您需要设置时,请转到ConfigurationManager。)

我也不会对GetFactoryCreateConnection来电进行空引用检查。您如何编写测试用例来执行该代码?你不能,因为你不知道如何使这些方法返回null - 你甚至不知道可能使这些方法返回null。所以你已经取代了一个问题 - 你的程序可以在你不理解的条件下抛出NullReferenceException - 另一个更重要的条件:在同样神秘的条件下,你的程序将运行你没有的代码测试

答案 5 :(得分:1)

您的方法文档遗失了。 ; - )

每个方法都有一些已定义的输入和输出参数以及定义的结果行为。在你的情况下,例如:“在成功的情况下返回有效的开放连接,否则返回null(或者抛出XXXException,如你所愿)。记住这种行为,你现在可以决定你应该如何编程。

  • 如果您的方法应该公开详细信息的原因和失败的原因,那么就像您一样,检查并捕获所有内容并返回相应的信息。

  • 但是,如果您只是对开放的DBConnection感兴趣,或者只是对 null (或某些用户定义的异常)感兴趣,那么只需将所有内容包装在try / catch中并返回(或某些异常),以及其他对象。

所以我想说,这取决于方法的行为和预期的输出。

答案 6 :(得分:1)

通常,应该捕获特定于数据库的异常并将其作为更一般的内容重新抛出,例如(假设的)DataAccessFailure异常。在大多数情况下,更高级别的代码不需要知道您正在从数据库中读取数据。

快速捕获这些错误的另一个原因是它们通常在其消息中包含一些数据库详细信息,例如“No such table:ACCOUNTS_BLOCKED”或“User key invalid:234234”。如果这传播给最终用户,则在几个方面很糟糕:

  1. 混乱。
  2. 潜在的安全漏洞。
  3. 为公司形象感到尴尬(想象一下客户端阅读粗略语法的错误信息)。

答案 7 :(得分:1)

我的经验法则是:

  如果消息的话,请不要抓住   抛出的异常与之相关   呼叫者。

因此,NullReferenceException没有相关的消息,我会检查它是否为null并使用更好的消息抛出异常。 ConfigurationErrorException是相关的,所以我没有抓住它。

唯一的例外是GetConnection的“合同”不会在配置文件中检索连接字符串。

如果是这种情况,GetConnection应该与自定义异常签订合同,说明无法检索连接,那么您可以在自定义异常中包装ConfigurationErrorException。

另一种解决方案是指定GetConnection不能抛出(但可以返回null),然后在你的类中添加“ExceptionHandler”。

答案 8 :(得分:0)

我的编码方式与你的第一次尝试完全一样。

但是,该函数的用户将使用USING块保护连接对象。

我真的不喜欢像其他版本一样翻译异常,因为它很难找出它崩溃的原因(数据库已关闭?没有读取配置文件的权限等等。)。

答案 9 :(得分:0)

只要应用程序有AppDomain.UnexpectedException处理程序将exception.InnerException链和所有堆栈跟踪转储到某种日志文件(甚至更好),修改后的版本就不会增加太多值,捕获一个小型号,然后拨打Environment.FailFast

根据这些信息,可以合理地直接指出出错的地方,而无需通过额外的错误检查使方法代码复杂化。

请注意,最好处理AppDomain.UnexpectedException并调用Environment.FailFast而不是顶级try/catch (Exception x),因为对于后者,问题的原因可能会被隐藏进一步的例外。

这是因为如果你捕获一个异常,任何打开的finally块都会执行,并且可能会抛出更多异常,这会隐藏原始异常(或者更糟糕的是,它们会删除文件以试图还原一些状态,可能是错误的文件,甚至可能是重要的文件)。您永远不应该捕获表示您不知道如何处理的无效程序状态的异常 - 即使在顶级main函数try/catch块中也是如此。处理AppDomain.UnexpectedException和调用Environment.FailFast是一个不同的(也是更理想的)水壶,因为它会阻止finally块运行,如果你试图暂停你的程序并记录一些在没有进一步损坏的情况下提供有用信息,您绝对不希望运行finally块。

答案 10 :(得分:0)

不要忘记检查OutOfMemoryExceptions ...您知道,可能会发生。

答案 11 :(得分:0)

Iain的变化对我来说是明智的。

如果我使用的是系统并且使用不当,我希望获得有关误用的最大信息。例如。如果我在调用方法之前忘记在配置中插入一些值,我想要一个带有详细说明我的错误的消息的InvalidOperationException,而不是KeyNotFoundException / NullReferenceException。

这完全与上下文IMO有关。我在我的时间里看到了一些相当难以理解的异常消息,但有时候来自框架的默认异常非常好。

一般来说,我认为最好小心谨慎,特别是当你写的东西被其他人大量使用时,或者通常在调用图中深处,其中错误很难被诊断出来。

我总是试着记住,作为一段代码或系统的开发者,我比正在使用它的人更能诊断失败。有时候,几行检查代码+自定义异常消息可以累积地节省数小时的调试时间(并且还可以使您自己的生活更轻松,因为您不会被调到其他人的机器来调试他们的问题)。 / p>

答案 12 :(得分:0)

在我看来,你的“后”样本并不是真正的防御性。因为防御是检查你控制下的部分,这将是参数connectionName(检查null或空,并抛出ArgumentNullException)。

答案 13 :(得分:0)

为什么不在添加所有防御性编程后拆分你拥有的方法?你有一堆不同的逻辑块,需要单独的方法。为什么?因为那时你封装了属于一起的逻辑,你生成的公共方法只是以正确的方式连接这些块。

像这样的东西(在SO编辑器中编辑,所以没有语法/编译器检查。可能无法编译; - ))

private string GetConnectionString(String connectionName)
{

   //Get the connection string info from web.config
   ConnectionStringSettings cs= ConfigurationManager.ConnectionStrings[connectionName];

   //documented to return null if it couldn't be found
   if (cs == null)
       throw new ArgumentException("Could not find connection string \""+connectionName+"\"");
   return cs;
}

private DbProviderFactory GetFactory(String ProviderName)
{
   //Get the factory for the given provider (e.g. "System.Data.SqlClient")
   DbProviderFactory factory = DbProviderFactories.GetFactory(ProviderName);

   //Undefined behaviour if GetFactory couldn't find a provider.
   //Defensive test for null factory anyway
   if (factory == null)
      throw new Exception("Could not obtain factory for provider \""+ProviderName+"\"");
   return factory;
}

public DbConnection GetConnection(String connectionName)
{
   //Get the connection string info from web.config
   ConnectionStringSettings cs = GetConnectionString(connectionName);

   //Get the factory for the given provider (e.g. "System.Data.SqlClient")
   DbProviderFactory factory = GetFactory(cs.ProviderName);


   //Have the factory give us the right connection object
   DbConnection conn = factory.CreateConnection();

   //Undefined behaviour if CreateConnection failed
   //Defensive test for null connection anyway
   if (conn == null)
      throw new Exception("Could not obtain connection from factory");

   //Knowing the connection string, open the connection
   conn.ConnectionString = cs.ConnectionString;
   conn.Open()

   return conn;
}

PS:这不是一个完整的重构,只有前两个块。