我该如何进行防御性编程?

38
我正在使用一个小程序来创建数据库连接:

之前

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...

文档说明称,如果无法检索到集合,则调用 ConnectionStrings 会抛出 ConfigurationErrorException。在这种情况下,我无法处理此异常,因此我将让它发生。
下一部分是实际索引ConnectionStrings以查找connectionName
...ConnectionStrings[connectionName];

在这种情况下,ConnectionStrings文档中说,如果找不到连接名称,则该属性将返回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()

再次,文档没有说明如果无法创建连接会发生什么,但我可以检查返回的对象是否为空值:
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方法是抽象的,因此取决于从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值。但在实践中,我只是将访问违规异常(如果尝试调用null引用上的方法)转换为InvalidArgumentException

我还捕获了两种可能存在null返回对象的情况;但同样,我只是将一个异常替换为另一个异常。

积极的一面是,它确实发现了两个问题,并解释了异常消息中发生了什么,而不是在后面出现问题(即责任到此为止)。

但这值得吗?这是否过度防御式编程?


2
这很不错。我唯一会做不同的事情是使用String.Format来构建消息,而不是附加字符串。我还会将消息放入资源中,以便进行本地化。 - John Saunders
3
哪种程序设计更易于调试?哪种更易于维护?增加的复杂性是否值得为其付出努力? - Nosredna
3
最简单的代码是没有任何错误检查的代码。而且没有任何形式的错误检查也更易于阅读。 - Ian Boyd
14个回答

0

修改后的版本并没有增加太多价值,只要应用程序有一个AppDomain.UnexpectedException处理程序,可以将exception.InnerException链和所有堆栈跟踪转储到某种日志文件中(或者更好的是,捕获一个minidump),然后调用Environment.FailFast

通过这些信息,很容易确定出问题所在,而不需要在方法代码中添加额外的错误检查。

请注意,最好处理AppDomain.UnexpectedException并调用Environment.FailFast,而不是使用顶级try/catch (Exception x),因为后者可能会被进一步的异常掩盖原始问题的原因。

这是因为如果你捕获了一个异常,任何打开的finally块都将执行,并且可能会抛出更多的异常,这将隐藏原始异常(或更糟糕的是,它们将删除文件以尝试还原某些状态,可能是错误的文件,甚至是重要的文件)。您永远不应该捕获指示无效程序状态的异常,即使在顶级main函数try/catch块中也是如此。处理AppDomain.UnexpectedException并调用Environment.FailFast是一种不同(更可取)的方法,因为它可以阻止finally块运行,如果您正在尝试停止程序并记录一些有用的信息而不会造成进一步的损害,那么您绝对不想运行您的finally块。

0

别忘了检查OutOfMemoryExceptions……你知道的,这种情况可能会发生。


1
我希望这是讽刺!问题是,有人可能会认真对待它。(幸运的是,在CLR 4.0中,他们将无法捕获这样的异常)。 - Daniel Earwicker
是的,这是讽刺,也许我需要澄清一下 :) - dr. evil

0

我认为Iain的更改看起来很合理。

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

在我看来,这一切都取决于上下文。我曾经看到过一些非常难以理解的异常消息,但有时来自框架的默认异常就足够了。

总的来说,我认为在写一些被其他人广泛使用或通常深入调用图中的代码时,最好保守一些,因为错误更难诊断。

我总是试图记住,作为代码或系统的开发人员,我比仅仅使用它的人更有能力诊断故障。有时候,几行检查代码+自定义异常消息可以累计节省数小时的调试时间(也可以使您的生活更轻松,因为您不必被拉到别人的机器上调试他们的问题)。


0
在我看来,你的“after”示例并不真正具有防御性。因为防御性应该是检查你控制的部分,也就是参数connectionName(检查是否为空或空字符串,并抛出ArgumentNullException)。

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