code2读书笔记-第八章 防御式编程
防御式编程 这一概念来自防御式驾驶。在防御式驾驶中要建立这样一种思维,那就是你永远也不能确定另一位司机要做什么。这样才能确保在其他人做出危险动作时你也不会受到伤害。防御式编程的主要思想是:子程序应该不因传入错误数据而被破坏,哪怕是由其他子程序产生的错误数据。
1. 保护程序免遭非法输入数据的破坏
对已形成产品的软件而言,仅仅“垃圾进,垃圾出”还不够。不管什么进来,好的程序都不会生成垃圾,而是做到“垃圾进,什么都不出”、“垃圾进,出去错误提示”或“不许垃圾进来”。通常有三种方法来处理进来垃圾的情况:
- 检查所有来源于外部的数据的值 当从文件、用户、网路或其他外部接口中获取数据时,应检查所获得的数据值,以确保它在允许的范围内。还要格外注意安歇狡猾的可能是攻击你的系统的数据,包括企图令缓存溢出的数据、注入的 SQL 命令、注入的 HTML 或 XML 代码、数据溢出以及传递给系统调用的数据,等等。
- 检查子程序所有出入参数的值 事实上和来源于外部的值一样,只不过数据时来自于其它子程序而非外部接口。
- 决定如何处理错误的输入数据 一旦检测到非法的参数,如何处理它,第3节“错误处理技术”会详细描述这些技术。
防御式编程的最佳方式就是在一开始不要在代码中引入错误。使用迭代设计、编码前先写伪代码、写代码前先写测试用例、底层设计检查等活动,都有助于防止引入错误。
2. 断言
断言(assertion)是指在开发期间使用的、让程序在运行时进行自检的代码(通常是一个子程序或宏)。一个断言通常有两个参数:一个描述假设为真时的情况的布尔表达式,和一个断言为假时需要显示的信息。
断言可以用于在代码中说明各个特定假设,澄清各种不希望的情形。可以用断言检查如下这类假定:
- 输入参数或输出参数的取值处于预期的范围内;
- 子程序开始(或者结束)执行时文件或流是处于打开(或关闭)的状态;
- 子程序开始(或者结束)执行时;文件或流的读写位置处于开头(或结尾)处;
- 文件或流已用只读、只写或可读可写方式打开;
- 仅用于输入的变量的值没有被子程序所修改;
- 指针非空;
- 传入子程序的数组或其他容器至少能容纳 X 个数据元素;
- 表已初始化,存储着真实的数值;
- 子程序开始(或结束)执行时,某个容器是空的(或满的);
- 一个经过高度优化的复杂子程序的运算结果和相对缓慢但代码清晰的子程序的运算结果相一致。
正常情况下,你不希望用户看到产品代码中的断言信息;断言主要用于开发和维护阶段。
使用断言的指导建议
- 用错误处理代码来处理预期会发生的状况,用断言来处理绝不应该发生的状况 错误处理通常用来检查有害的输入数据,而断言是用于检查代码中的 bug。
- 避免把需要执行的代码放到断言中
- 用断言来注解并验证前条件和后条件 使用前条件和后条件时,每个子程序或类与程序的其余部分形成了一份契约。也可以使用注释来说明前后条件,但断言却能动态地判断前后条件是否为真。
- 对于高建壮性的代码,应该先使用断言再处理错误
3. 错误处理技术
断言可以用于处理代码中不应发生的错误。那么又该如何处理那些预料中的能要发生的错误呢?下面就来详细说明这些可用的技术:
- 返回中立值 处理错误数据的最佳做法就是继续执行操作并简单地返回一个没有危害的数值。
- 换用下一个正确的数据 在处理数据流的时候,有时只需返下一个正确的数据即可。
- 返回与前次相同的数据 如果体温计读数软件在某次读取中没有获得数据,那么它可以简单地返回前一次的读取结果。
- 换用最接近的合法值 在有些情况下,你可以选择返回最接近的那个和法制,比如你检测到一个小于 0 的读取结果,你可以把它替换为 0.
- 把警告信息记录到日志文件中
- 返回一个错误码 你可以决定让系统的某些部分处理错误,其他部分不在本地(局部)处理错误,而只是简单的报告错误,可以使用下列方法通知系统其余部分已发生的错误:
- 设置一个状态变量的值
- 用状态值作为函数的返回值
- 用语言内建的异常机制抛出一个异常
- 调用错误处理子程序或对象 第一个方法是把错误处理都集中在一个全局的错误处理子程序或对象中。
- 当错误发生时显示出错误消息 这种方法可以把错误处理的开销减到最少,然而它也可能会让用户界面中出现的信息散步到整个应用中。
- 用最妥当的方式在局部处理错误
- 关闭程序 有些系统一旦检测到错误发生就会关闭。这一方法适用于人身安全相关的程序。
健壮性与正确性
错误处理方式有时更侧重于正确性,而有时更侧重于健壮性。正确性 意味着永不返回不准确的结果。然而,健壮性 则意味着要不断尝试采取某些措施,以保证软件可以持续地运转下去,哪怕有时做出一些不够准确的结果。人身安全攸关的软件往往更倾向于正确性;而消费类应用软件更注重健壮性而非正确性。
高层次设计对错误处理方式的影响
对错误处理的方式直接关系到软件能否满足在正确性、健壮性和其他非功能性指标方面的要求。确定一种通用的处理错误参数的方法,是架构层次(或更高层次)的设计决策,需要在那里的某个层次上解决。
4. 异常
异常时把代码中的错误或异常事件传递给调用方代码的一种特殊手段。对错误的前因后果不甚了解的代码,可以把对控制权转交给系统中其他能更好地解释错误并采取措施的部分。异常和继承有一点是相同的,即:审慎明确地使用时,它们都可以降低复杂度;而草率粗心地使用时,只会让代码变得几乎无法理解。下面给出一些建议:
- 用异常通知程序的其他部分,发生了不可忽略的错误 异常机制的优越之处在于它能提供一种无法被忽略的错误通知机制。
- 只在真正例外的情况下才抛出异常 异常需要你做出一个取舍;一方面它是一种强大的用来处理预料之外的情况的途径,另一方面程序的复杂度会因此增加。由于调用子程序的代码需要了解被调用代码中可能会排出的异常,因此异常弱化了封装性。
- 不能用异常推卸责任 如果某种异常可以在局部处理,那就在局部处理。
- 避免再构造函数和析构函数中抛出异常,除非你在同一地方把它们捕获 如果在构造函数中抛出异常,就不会调用析构函数,从而造成潜在的资源泄漏。
- 在恰当的抽象层次抛出异常 子程序应在其接口中展现出一致的抽象,类也是如此。抛出的异常也是程序接口的一部分,和其他具体的数据类型一样。
- 在异常消息中加入关于导致异常发生的全部信息
- 避免使用空的 catch 造句
- 了解所用函数库可能抛出的异常
- 考虑创建一个集中的异常报告机制 这个集中报告机制能够为一些与异常有关的信息提供一个集中的存储,如所发生的异常冲突、每个异常该被如何处理以及如何格式化异常消息等。
- 把项目中对异常的使用标准化
- 应该为到底可以抛出哪些种类的异常建立一个标准。
- 考虑创建项目的特定异常类,它可以用做项目中所有可能抛出的异常的基类。
- 规定在何种场合允许代码使用 throw-catch 语句在局部对错误进行处理。
- 规定在何种场合运行代码抛出不在局部进行处理的异常。
- 确定是否要使用集中的异常报告机制。
- 规定是否允许在构造函数和析构函数中使用异常。
- 考虑异常的替换方案
应对程序运行时发生的严重错误的最佳做法,有时就是释放所有已获得的资源并终止程序执行,而让用户去重新用正确的输入数据再次运行程序。
把异常当做正常处理逻辑的一部分的那种程序,都会遭受与所有典型的意大利面式代码同样的可读性和可维护性问题。
5. 隔离程序,使之包容由错误造成的损害
让软件的某些部分处理“不干净的”数据,而让另一些部分处理“干净的”数据,即可让大部分代码无须再负担检查错误数据的职责。
6. 辅助调试的代码
不要自动地把产品版的限制强加于开发版之上
尽早引入辅助调试的代码
采用进攻式编码
应该以这么一种方式来处理异常情况:在开发阶段让它展现出来,而在产品代码运行时让它能够自我恢复。– 进攻式编程
下面列出一些可以让你进行进攻式编程的方法:
- 确保断言语句能使程序终止运行。
- 完全填充分配到的所有内存。
- 完全填充已分配到的所有文件或流。
- 确保每一个 case 语句中的 default 分支或 else 分支都能产生严重错误。
- 在删除一个对象之前把它填满垃圾数据。
- 让程序把它的错误日志文件用电子邮件发给你,这样就能了解到在已发布的软件中还发生了哪些错误。
计划移除调试辅助的代码
- 使用类似 ant 和 make 这样的版本控制工具和 make 工具
- 使用内置的预处理器
- 编写你自己的预处理器
- 使用调试存根
7. 确定在产品代码中该保留多少防御式代码
- 保留那些检查重要错误的代码 你需要确定程序的哪些部分可以承担未检测出错误而造成的后果,而哪些部分不能承担。
- 去掉检查细微错误的代码 如果一个错误带来的影响确实微乎其微的话,可以把检查它的代码去掉。
- 去掉可以导致程序硬性崩溃的代码
- 保留可以让程序稳妥地崩溃的代码
- 为你的技术支持人员记录错误信息
- 确认留在代码中的错误信息是友好的
8. 对防御式编程采取防御的姿态
过度的防御式编程也会引起问题。如果你在每个能想到的地方用每一种想到的方法检查从参数传入的数据,那么你的程序将会变得臃肿而缓慢。更糟糕的是,防御式编程引入的额外代码增加了软件的复杂度。
要点
- 最终产品代码中对错误的处理方式要比“垃圾进,垃圾出”复杂得多。
- 防御式编程技术可以让错误更容易发现、更容易修改、并减少错误对产品代码的破坏。
- 断言可以帮助人尽早发现错误,尤其在大型系统和高可靠性的系统中,以及快速变化的代码中。
- 关于如何处理错误输入的决策是一项关键的错误处理决策,也是一项关键的高层设计决策。
- 异常提供了一种与代码正常流程角度不同的错误处理手段。如果留心使用异常,它可以成为程序员们知识工具箱中的一项有益补充,同时也应该在异常和其它错误处理手段之间进行权衡比较。
- 针对产品代码的限制并不适用于开发中的软件。你可以利用这一优势在开发中添加有助于更快地排查错误的代码。