这是我多年来一直在问自己的问题,但仍然没有得到令人满意的答案。
但是我认为,在进行参数验证时,您需要区分两种情况:
您是否正在验证参数以捕获逻辑编程错误?
if (foo == null) throw new ArgumentNullException("foo");
很可能就是一个例子。
您是否正在验证参数,因为它是一些外部输入(由用户提供,或从配置文件或从数据库中读取),它可能是无效的并且必须被拒绝?
if (customerDateOfBirth == new DateTime(1900, 1, 1)) throw …;
可能属于这种类型的参数检查。
(如果您要公开团队外部人员使用的 API,第 2 点也大致适用。)
我怀疑诸如单元测试、合同设计以及在某种程度上“早期失败”之类的方法主要集中在第一种类型的参数验证上。也就是说,它们试图检测逻辑编程错误,而不是无效输入。
如果是这种情况,那么我敢说您遵循哪种错误检测方法实际上并不重要。每个都有自己的优点和缺点。†在极端情况下(例如,当您完全信任自己编写无错误代码的能力时),您甚至可以完全放弃这些检查。
但是,无论您选择哪种方法来检测代码中的逻辑错误,您仍然需要验证用户输入等,因此需要区分两种参数检查。
†)一个业余爱好者在比较按合同设计、单元测试和“早期失败”的相对优缺点方面的不完整尝试:
(虽然你没有要求它......我只会提到一些关键的区别。)
早期失败(例如在方法开始时显式验证参数):
- 编写防卫等基本检查
null
很容易编写
- 可能会混淆对逻辑错误的防范和使用相同语法对外部输入的验证
- 不允许您测试方法的交互
- 不鼓励您严格定义(并因此考虑)您的方法的合同
单元测试:
- 允许您单独测试代码,而无需运行实际应用程序,因此可以更快地检测错误
- 如果发生逻辑错误,您无需跟踪堆栈即可找到原因,因为每个单元测试都代表代码的特定“用例”。
- 不仅可以测试单个方法,甚至可以测试多个对象之间的交互(想想存根和模拟)
- 编写简单的测试(例如防范
null
)比使用“早期失败”方法(如果您严格遵守 Arrange-Act-Assert 模式)更工作
按合同设计:
- 强制你明确地声明你的类的契约(尽管这也可以通过单元测试来实现——只是以不同的方式)
- 允许您轻松声明类不变量(必须始终成立的内部条件)
- 不像其他方法那样受到许多编程语言/框架的支持