目录
防御式编程来自防御式驾驶。在防御式驾驶中要建立这样一种思维,那就是你永远也不能确定另一位司机将要做什么。这样才能确保在其他人做出危险动作时你也不会受到伤害。你要承担起保护自己的责任,哪怕是其他司机犯的错误。防御式编程的主要思想是:子程序应该不因传入错误数据而被破坏,哪怕是由其他子程序产生的错误数据。更一般地说,其核心想法是要承认程序都会有问题,都需要被修改,聪明的程序员应该根据这一点来编程序。
本章就是要讲述如何面对严酷的非法数据的世界、在遇到 “绝不会发生”的事件以及其他程序员犯下的错误时保护你自己。
个人结论
我是服务的提供者(方法是public的,别人可能会调用我的接口、组件、方法),那么调用者就需要遵守我的规则:该传的参数必须要传。
只要是public的接口,那么参数校验是少不了的。web服务是一个领域、ser服务也是一个领域、repo服务又是一个领域,都得做校验。
“垃圾进,垃圾出”的程序显然不符合程序安全的标准。通常有三种方法来处理进来垃圾的情况。
1、检查所有来源于外部的数据的值 。当从文件、用户、网络或其他外部接口中获取数据时,应检查所获得的数据值,以确保它在允许的范围内。
2、 检查子程序所有输入参数的值 。检查子程序输入参数的值,事实上和检查来源于外部的数值一样,只不过数据是来自于其他子程序而非外部接口。第 8.5节“隔离程序,使之包容由错误造成的损害”阐述了一种实用方法可用于确定哪些子程序需要检查其输入数据。
3、决定如何处理错误的输入数据 。一旦检测到非法的参数,你该如何处理它呢?根据情况的不同,你可以从十几种不同的方案中选择其一,在本章后面第 8.3节“错误处理技术〞中会详细描述这些技术。
当提到‘输入’时,我们是处于「服务提供者」的角色,也就是别人来调用我的程序;当提到‘输出’时,我们是处于「服务调用者」的角色,也就是我们来调用别人的程序。
处于服务调用者角色的时候,我们也应该对获取到的数据进行校验。比较常见的是:对获取的数据进行非空检查。
当服务被调用的时候,可以优先进行日志输出,以保留服务被调用时的‘痕迹’。
断言可以用于在代码中说明各种假定,澄清各种不希望的情形。可以用断言检查如下这类假定:
当然,这里列出的只是一些基本假定,你在子程序中还可以包括更多可以用断言来说明的假定。
C++、Java等语言都支持断言。如果你想在原有的基础上,构建自己的断言机制,可以对原有的断言进行封装:

下面是关于使用断言的一些指导性建议。
1、用错误处理代码(try catch)来处理预期会发生的状况,用断言(assert)来处理绝不应该发生的状况。
2、避免把需要执行的代码放到断言中。
3、用断言来注解并验证前条件和后条件。 前条件(preconditions)和后条件(postconditions)是一种名为“契约式设计 (design by contract )” 的程序设计和开发方法的一部分(Meyer 1997)。前条件是调用方代码(服务的提供者)对其所调用的代码要承担的义务。后条件是子程序或类在执行结束后要确保为真的属性,后置条件是子程序或类(服务的调用者)对调用方代码所承担的责任。
断言可以用于处理代码中不应发生的错误。那么又该如何处理那些预料中可能要发生的错误呢?根据所处情形的不同,你可以:
处理错误最恰当的方式要根据出现错误的软件的类别而定。错误处理方式有时更侧重于正确性,而有时则更侧重于健壮性。
开发人员倾向于非形式地使用这两个术语,但严格来说,这两个术语在程度上是截然相反的。
既然有这么多的选择, 你就必须注意,应该在整个程序里采用一致的方式处理非法的参数。对错误进行处理的方式会直接关系到软件能否满足在正确性、健壮性和其他非功能性指标方面的要求。确定一种通用的处理错误参数的方法,是架构层次(或称高层次)的设计决策,需要在那里的某个层次上解决。
一旦确定了某种方法, 就要确保始终如一地贯彻这一方法。如果你决定让高层的代码来处理错误,而低层的代码只需简单地报告错误,那么就要确保高层的代码真的处理了错误!有些语言允许你忽略“函数返回的是错误码”这一事实—在 C++中,你无须对函数的返回值做任何处理——但千万不要忽略错误信息!检查函数的返回值。即使你认定某个函数绝对不会出错,也无论如何要去检查一下。防御式编程全部的重点就在于防御那些你末曾预料到的错误。
异常是把代码中的错误或异常事件传递给调用方代码的一种特殊手段。对出错的前因后果不甚了解的代码,可以把对控制权转交给系统中其他能更好地解释错误并采取措施的部分。
异常和继承有一点是相同的,即:审慎明智地使用时,它们都可以降低复杂度;而草率粗心地使用时,只会让代码变得几乎无法理解。下面给出的一些建议可以让你在使用异常时扬长避短,并避免与之相关的一些难题:
有些程序员用异常来处理错误,只是因为他所用的编程语言提供了这种特殊的错误处理机制。你心里应该自始至终考虑各种各样的错误处理机制:在局部处理错误、使用错误码来传递错误、在日志文件中记录调试信息、关闭系统或其他的一些方式等。
仅仅因为编程语言提供了异常处理机制而使用异常,是典型的“为用而用〞;这也是典型的“在一种语言上编程〞而非“深入一种语言去编程”的例子。(有关这两者的区别,请参阅第4.3节“你在技术浪潮中的位置”和第34.4节“以所用语言编程,但思路不受其约束”)
以防御式编程为目的而进行隔离的一种方法,是把某些接口选定为“安全”区域的边界。对穿越安全区域边界的数据进行合法性校验,并当数据非法时做出敏锐反映。
让软件的某些部分处理“不干净的”数据,而让另一些部分处理“干净的”数据,即可让大部分代码无须再担负检查错误数据的职责:

也同样可以在类的层次采用这种方法。类的公用方法可以假设数据是不安全的,它们要负责检查数据并进行清理。一旦类的公用方法接受了数据,那么类的私用方法就可以假定数据都是安全的了。
在输入数据时将其转换为恰当的类型 。 输入的数据通常都是字符串或数字的形式。这些数据有时要被映射为“是”或“否”这样的布尔类型,有时要被映射为像 Color_Red、 Color_Green 和 Color_Blue 这样的枚举类型。在程序中长时间传递类型不明的数据,会增加程序的复杂度和崩溃的可能性——比如说有人在需要输入颜色枚举值的地方输入了“是’〞。因此,应该在输入数据后立即将其转换到恰当的类型。
隔栏的使用使断言和错误处理有了清晰的区分。隔栏外部的程序应使用错误处理技术,在那里对数据做的任何假定都是不安全的。而隔栏内部的程序里就应使用断言技术,因为传进来的数据应该已在通过隔栏时被清理过了。如果隔栏内部的某个子程序检测到了错误的数据,那么这应该是程序里的错误而不是数据里的错误。隔栏的使用还展示了“在架构层次上规定应该如何处理错误”的价值。规定隔栏内外的代码是一个架构层次上的决策。