我应该如何防守?
我正在使用一个用于创build数据库连接的小例程:
之前
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框架文档,查看各种事情的logging行为,看看我能否处理它们。
例如:
ConfigurationManager.ConnectionStrings...
该文件说,如果ConnectionStrings无法检索集合,则会调用ConfigurationErrorException 。 在这种情况下,我无法处理这个exception,所以我会放手。
下一部分是ConnectionStrings的实际索引来查找connectionName :
...ConnectionStrings[connectionName];
在这种情况下ConnectionStrings文档说,如果无法find连接名称,该属性将返回null 。 我可以检查发生这种情况,并抛出一个exception,让高人,他们给了一个无效的connectionName:
ConnectionStringSettings cs= ConfigurationManager.ConnectionStrings[connectionName]; if (cs == null) throw new ArgumentException("Could not find connection string \""+connectionName+"\"");
我重复同样的练习:
DbProviderFactory factory = DbProviderFactories.GetFactory(cs.ProviderName);
如果找不到指定ProviderName
的工厂, GetFactory方法没有文档。 它没有logging返回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()
再次, 文档没有说如果它不能创build一个连接会发生什么,但我可以再次检查一个空返回对象:
DbConnection conn = factory.CreateConnection() if (conn == null) throw new Exception.Create("Connection factory did not return a connection object");
接下来是设置Connection对象的属性:
conn.ConnectionString = cs.ConnectionString;
文档不会说如果无法设置连接string会发生什么情况。 它是否抛出exception? 它忽略了吗? 与大多数例外情况一样,如果在尝试设置连接的ConnectionString时出现错误,我无法从中恢复。 所以我什么都不做
最后,打开数据库连接:
conn.Open();
DbConnection的Open方法是抽象的,所以它取决于从DbConnection递减的任何提供者来决定它们抛出的exception。 在抽象Open方法文档中也没有关于如果出现错误可以期望发生的事情的指导。 如果有错误连接,我知道我无法处理它 – 我不得不让它冒出来的地方,呼叫者可以显示一些用户界面,让他们再试一次。
后
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 (eg "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的情况。 但在实践中,我所做的只是将访问冲突exception(如果我试图对空引用调用方法)转换为InvalidArgumentExceptionexception。
我也抓住两个可能的情况下,可能是空返回对象; 但是我又一次只交易了一个例外。
从积极的方面来说,它确实遇到了两个问题,并解释了例外消息中发生的事情,而不是发生在路上的坏事情(即,在这里降压)
但是这值得吗? 这是否过度杀伤? 这个防守编程是错误的吗?
手动检查configuration并抛出一个exception并不比只是在丢失configuration时让框架抛出exception更好。 你只是复制在框架方法内部发生的先决条件检查,它使你编码冗长,没有任何好处。 (实际上,你可能会把所有的东西都作为基类的Exception类去除 ,框架抛出的exception通常更具体。)
编辑:这个答案似乎有点争议,所以一点阐述:防御性编程意味着“准备意外”(或“是偏执狂”),其中一个方法是做很多先决条件检查。 在许多情况下,这是一个很好的做法,但是所有的做法都应该权衡成本。
例如,它没有提供任何好处来抛出“无法从工厂获取连接”exception,因为它没有提供任何关于为什么无法获取提供者的信息 – 而且下一行将抛出一个exception,无论如何提供程序为空。 所以前提条件检查的成本(开发时间和代码复杂度)是不合理的。
另一方面,validation连接stringconfiguration是否存在的检查可能是合理的,因为例外可以帮助告诉开发者如何解决问题。 无论如何,你将在下一行得到的空例外不会告诉缺less连接string的名称,所以你的前提条件检查确实提供了一些价值。 例如,如果代码是组件的一部分,则该值非常大,因为组件的用户可能不知道组件需要哪些configuration。
防御性编程的一个不同的解释是,你不应该只是检测错误条件,你应该尝试从任何可能发生的错误或exception中恢复。 我不认为这是一个好主意。
基本上你应该只处理你可以做的事情。 无论如何,您无法恢复的exception应该向上传递给顶层处理程序。 在Web应用程序中,顶层处理程序可能只是显示一个通用的错误页面。 但是在大多数情况下,如果数据库处于脱机状态或缺less一些重要的configuration,则不需要做太多的事情。
有些情况下,这种防御性编程是有道理的,如果你接受用户input,而且input可能导致错误。 例如,如果用户提供了一个URL作为input,并且应用程序试图从该URL获取某些内容,那么检查该URL看起来是否正确并且处理可能来自该请求的任何exception是非常重要的。 这使您可以向用户提供有价值的反馈。
那么,这取决于你的观众是谁。
如果你正在编写你希望被许多其他人使用的库代码,谁也不会和你谈论如何使用它,那么这不是一个过度的技巧。 他们会感激你的努力。
(也就是说,如果你这样做了,我build议你定义比System.Exception更好的exception,以便让那些想要捕获你的exception而不是其他exception的人更容易。
但是如果你只是要自己使用它(或者你和你的好友),那显然是过度的,最后可能会让你的代码变得不可读。
我希望能让我的团队像这样编码。 大多数人甚至没有得到防守编程的点。 他们所做的最好的做法是将整个方法包装在一个try catch语句中,并让所有exception由genericsexception块处理!
给你的帽子伊恩。 我可以理解你的困境。 我自己也一样。 但是,你做了什么可能帮助了一些开发人员几个小时的键盘打击。
记住,当你使用.net框架API时,你期望什么呢? 什么似乎很自然? 对你的代码做同样的事情。
我知道这需要时间。 但质量是有代价的。
PS:你真的不必处理所有的错误,并抛出一个自定义的exception。 请记住,您的方法将只被其他开发人员使用。 他们应该能够自己找出共同的框架例外。 这是不值得的麻烦。
你的“之前”的例子有明确和简洁的区别。
如果有什么错误,框架最终会抛出一个exception。 如果你不能对exception做任何事情,你可以让它传播到调用堆栈。
然而,有时候,在框架内部抛出一个exception,并不能说明实际的问题。 如果你的问题是你没有一个有效的连接string,但是框架会抛出一个exception,例如“invalid use of null”,那么有时候最好是捕捉exception并用更有意义的消息重新抛出exception。
我会检查空对象很多,因为我需要一个实际的对象来操作,如果对象是空的,抛出的exception将倾斜,至less可以说。 但是我只检查空对象,如果我知道会发生什么。 一些对象工厂不返回空对象; 他们会抛出exception,而在这些情况下检查null将是无用的。
我不认为我会写任何空引用检查逻辑 – 至less,而不是你已经做到这一点。
从应用程序configuration文件中获取configuration设置的程序将在启动时检查所有这些设置。 我通常构build一个静态类来包含设置,并在应用程序的其他地方引用该类的属性(而不是ConfigurationManager
)。 有两个原因。
首先,如果应用程序没有正确configuration,就不会起作用。 当我尝试创build数据库连接时,我宁愿知道程序读取configuration文件的时刻。
其次,检查configuration的有效性不应该是依赖于configuration的对象的关注。 如果您已经在前面执行了这些检查,那么在您的代码中无处不在插入检查是没有意义的。 (当然也有例外,例如,长时间运行的应用程序需要在程序运行时更改configuration,并将这些更改反映在程序的行为中;在这样的程序中,您需要只要需要设置,就进入ConfigurationManager
。)
我也不会对GetFactory
和CreateConnection
调用进行空引用检查。 你将如何编写一个testing用例来运行该代码? 你不能,因为你不知道如何使这些方法返回null – 你甚至不知道有可能使这些方法返回null。 所以你已经replace了一个问题 – 你的程序可以在你不明白的条件下抛出一个NullReferenceException
,另一个更重要的问题是:在那些相同的神秘的条件下,你的程序会运行你没有testing过的代码。
您的方法文档丢失。 😉
每种方法都有一些定义的input和输出参数以及一个定义的结果行为。 在你的情况下是这样的:“如果成功返回有效的开放连接,否则返回null(或者抛出一个XXXException,如你所愿)。记住这个行为,你现在可以决定如何防御编程。
-
如果你的方法应该公开详细的信息为什么和什么是失败的,那么像你一样做,检查和抓住所有的一切,并返回相应的信息。
-
但是,如果您只是对打开的DBConnection感兴趣,或者对失败只是null (或某个用户定义的exception),那么只需将所有内容包装在try / catch中,并返回null (或某种exception),而其他对象则返回null 。
所以我会说,这取决于方法的行为和预期的输出。
一般来说,特定于数据库的exception应该被捕获并重新抛出,比如(假设的) DataAccessFailure
exception。 在大多数情况下,高级代码不需要知道您正在从数据库中读取数据。
快速捕获这些错误的另一个原因是,他们经常在消息中包含一些数据库详细信息,如“没有这样的表:ACCOUNTS_BLOCKED”或“用户密钥无效:234234”。 如果这种情况传播给最终用户,这在几个方面是不好的:
- 扑朔迷离
- 潜在的安全漏洞
- 尴尬为贵公司的形象(想象一个客户阅读错误信息粗糙的语法)
我的经验法则是:
如果抛出exception的消息与调用者相关,则不捕获。
因此,NullReferenceException没有相关的消息,我会检查它是否为空,并抛出一个更好的消息的exception。 ConfigurationErrorException是相关的,所以我不理解它。
唯一的例外是如果GetConnection的“契约”不必检索configuration文件中的连接string。
如果是这种情况GetConnection应该有一个自定义的exception,说连接无法检索,那么你可以包装ConfigurationErrorException在你的自定义exception。
另一个解决scheme是指定GetConnection不能抛出(但可以返回null),然后给你的类添加一个“ExceptionHandler”。
我会编写它就像你第一次尝试。
但是,该函数的用户将使用USING块来保护连接对象。
我真的不喜欢翻译像其他版本的exception,因为它很难找出为什么它打破了(数据库closures?没有权限读取configuration文件等)。
修改后的版本并没有增加太多的价值,只要应用程序有一个AppDomain.UnexpectedException
处理程序将exception.InnerException
链接和所有堆栈跟踪转储到某种日志文件(甚至更好,捕获一个小型转储),然后调用Environment.FailFast
。
从这些信息中可以明确地发现问题出在哪里,而不需要通过额外的错误检查来复杂化方法代码。
请注意,最好处理AppDomain.UnexpectedException
并调用Environment.FailFast
而不是使用顶级try/catch (Exception x)
因为对于后者,问题的原因可能会被进一步的exception所遮蔽。
这是因为如果你捕捉到一个exception,任何打开的finally
块都会执行,并且可能会抛出更多的exception,这将会隐藏原来的exception(或者更糟糕的是,它们会删除文件,试图恢复某些状态,可能是错误的文件,甚至可能是重要的文件)。 你不应该捕获一个exception,指出你不知道如何处理的一个无效的程序状态 – 即使在顶级main
函数try/catch
块中。 处理AppDomain.UnexpectedException
并调用Environment.FailFast
是一个不同的(也是更理想的)水壶,因为它finally
阻止了运行,如果你试图停止你的程序并logging一些有用的信息而不会造成进一步的损害,绝对不想运行你的finally
块。
不要忘记检查OutOfMemoryExceptions …你知道,它可能会发生。
伊恩的变化对我来说很明智。
如果我正在使用一个系统,并且使用不当,我想获得有关误用的最大信息。 例如,如果我忘记在调用方法之前将一些值插入到configuration中,我想要一个InvalidOperationException,其中包含详细描述我的错误的消息,而不是KeyNotFoundException / NullReferenceException。
这一切都是关于国际海事组织。 在我这个时代,我已经看到了一些相当难以理解的exception消息,但是其他时候来自框架的默认exception是完全正确的。
一般来说,我认为最好谨慎一点,特别是当你写的东西被其他人大量使用的时候,或者在错误难以诊断的调用图中。
我总是试着记住,作为一个代码或系统的开发者,我比一个正在使用它的人更能够诊断故障。 有时候,几行检查代码+一个自定义的exception消息可以累积地节省几个小时的debugging时间(同时也让你自己的生活更轻松,因为你不会被拖到别人的机器去debugging他们的问题)。
在我眼中,你的“之后”样本并不是真正的防御。 因为防御是检查你的控制下的部分,这将是参数connectionName
(检查null或空,并引发ArgumentNullException)。
在添加所有防御性编程之后,为什么不把你拥有的方法分开? 你有一堆不同的逻辑块,保证单独的方法。 为什么? 因为那么你封装了属于一起的逻辑,你所得到的公共方法只是以正确的方式连接这些块。
像这样的东西(编辑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 (eg "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 (eg "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:这不是一个完整的重构,只有前两个块。