欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

防御性编程

程序员文章站 2022-05-31 10:21:07
...

搜集了一些关于“防御性编程”资料,将其中一些思想备份下,学习


软件工程师的智慧,就是在于其是否开始意识到:使程序能用和使程序正确,这两者之间有什么样的差别
编写在常规情况下都能用的代码是很容易的,只要提供常规的输入集,这些代码就会给出常规的输出集。但是如果提供了一些意外的输入,这些代码可能就会崩溃
正确的代码是绝不会崩溃的,对于所有可能的输入集,它的输出都将是正确的,不过,所有可能输入的集合会常常大得惊人,并且难以测试
然而并不是所有的正确的代码都是优秀的代码,因为有些正确的代码的逻辑可能很难理解,其代码可能很不自然,并且可能几乎无法维护
因此,编写优秀的代码才应该是不懈追求的目标,优秀的代码是健壮的,高效的,也是正确的。当面对不常见的输入的时候,产品级的代码不会crash,也不会出现错误的结果,同时还满足所有其他的要求,包括线程安全、时间约束和重入等
经常面对风格奇异的遗留代码,那些由现在早已不在的代码猴子所编写的旧代码
面对各种各样的困难,怎么保证所编写的代码稳定耐用
答案就是 防御性编程


设想导致我们写出带有缺陷的软件,下面是一些很常见的设想:
这个函数 绝不会 被那样调用,传递的参数总是有效的
这段代码肯定会 一直 正常运行,它绝不会出现错误
如果吧这个变量标记为 仅限内部使用 就没有人会尝试访问这个变量了
在进行防御性编程的时候,我们不应该做任何设想,我们不应该设想“那不会发生”
经验告诉我们,唯一可以肯定的是:代码在某天会因为某种原因而出错,有人会做出愚蠢的举动。墨菲定律这样说:凡是可能会出错的事,准会出错。防御性编程通过遇见到 或者至少是预先推测到 问题的所在,断定代码中每个阶段可能出现的错误,并做出相应的防范措施,来防止这类意外的发生
这也许是有点偏执,但适度的偏执并没有什么坏处。coding从一开始就适度的偏执,可以使代码在很长的时间内更加健壮


防御性编程是一种细致、谨慎的编程方法,为了开发可靠地软件,需要设计系统中的每个组件,使其尽可能的保护自己,我们通过明确的在代码中对设想进行检查,击碎了未记录下来的设想,这是一种努力,防止 或者至少是观察 我们的代码以错误行为的方式被调用
防御性编程使我们可以尽早发现较小的问题,而不是等它们发展成灾难的时候才发现,当然防御性编程并不能排除所有的程序错误,但是问题所带来的麻烦将会减少,并易于修改,防御性程序员只是抓住飘落的雪花,而不是被埋葬在错误的雪崩中
防御性编程是一种防卫方式,而不是一种补救形式


对防御性编程的误解
防御性编程 并不是 检查错误:如果代码中存在可能出现错误的情况,无论如何都应该检查这些错误,这并不是防御性编程,这只是一种好的做法,是编写正确的代码的一部分
防御性编程 并不是 测试:测试代码并不是防御,而只是开发工作的另一份典型的部分。测试工作不是防御性的,这项工作可以验证代码现在是正确的,但不能保证代码在经理将来的修改之后不会出错。即便是拥有了全世界最好的测试工具,也还是会有人对代码进行更改,并是代码进入过去未测试的状态
防御性编程并不是调试:在调试期间,你可添加一些防御性代码,不过调试是在程序出错之后进行的,防御性编程首先是防止程序出错的措施 或者在错误以不可理解的方式出现之前发现它们


防御性编程可以节省大量的调试时间,使你可以去做更有意义的事情
编写可以正确运行、只是速度有些慢的代码,要远远好过大多数时间都正常运行,但是有时候会crash的代码
我们可以设计一些在版本构建中物理移除的防御性代码,以解决性能问题,总之我们这里所考虑的大部分防御性措施,并不具有明显的开销
防御性编程避免了大量的安全问题,这是现代软件开发中是一个重大的问题,避免这些问题可以带来很多好处


防御性编程技巧

  1. 使用好的编码风格和合理的设计
    我们可以通过采用良好的编程风格,来防范大多数编码错误,如选用有意义的变量名,或者谨慎的使用括号,在开始coding之前,先考虑大体的设计方案,从实现一套清晰的API、一个逻辑系统结构以及一些定义良好的组件角色与责任开始入手
  2. 不要仓促的编写代码
    使用闪电式编程方式的程序员会很快的开发出一个函数,马上把这个函数交给编译器来检查已发,接着运行一边看看能不能使用,然后进入下一个任务。这种方式充满了危险。相反,在写每一行时候都三思,可能会出现什么错误,是否已经考虑了所有可能出现的逻辑分支,放慢速度,有条不紊编程虽然看上去很平凡,但是的确是减少缺陷的好办法
  3. 不要相信任何人
    真正的用户:意外地提供了假的输入,或者错误地操作了程序
    恶意的用户:故意造成不好的程序行为
    客户端代码:使用错误的参数调用了你的函数,或者提供了不一致的输入
    运行环境:没有为程序提供足够的服务
    外部程序库:运行失误,不遵从你所依赖的接口协议
  4. 编码的目标是清晰,而不是简洁
    如果要你从简洁但是可能让人困惑的代码和清晰但是有可能比较冗长的代码中选择,一定要选择那些看上去和预期相符合的代码,即使它不太优雅。例如,将复杂的代数运算拆分为一系列的单独的语句,使逻辑更清晰。想一想,谁会是的代码的读者,这些代码也许需要一位初级程序员来维护,如果他不能理解代码逻辑,那么他肯定会犯一些错误,复杂的结构或不常用的语言技巧可以证明你在运算符优先级方面渊博的知识,但是这些实际上会扼杀代码的可维护性,请保持代码简单。不能维护的代码是不安全,举一个极端的例子,过于复杂的表达式会使编译器生成错误的代码,许多编译器优化的错误就是因此造成的
  5. 不要让任何人做他们不该做的修补工作
    内部的事情应该留在内部,私人的东西就应该用锁和钥匙保管起来,不要吧你的代码初稿公之于众,不管你多么有礼貌的恳求,主要稍不注意,别人就会篡改你的数据,然后尝试条用 仅用于执行 的例行程序。不要让他们这么做
    a. 在面向对象的语言中,通过将属性设为 private 来防止对内部类数据的访问
    b. 在过程语言中,仍可以使用面向对象的打包概念,将private 数据打包在不透明的类型背后,并提供可以操作它们的定义良好的公共函数
    c. 将所有变量保持在尽可能小的范围内,不到万不得已,不要声明全局变量,如果变量可以声明为函数内部的局部变量,就不要在文件范围上声明。如果变量可以声明为循环体内的局部变量,就不要在函数范围上声明
  6. 编译时候打开所有的警告开关
    大多数语言的编译器都会在你伤了它们感情的时候给出一大堆错误信息,当这些编译器碰到潜在的有缺陷的代码时候,它们也会给出各种各样的警告。通常情况下这些警告可以有选择地启用或禁用
    如果代码中充满了危险的构造,将会得到数页的警告信息,糟糕的是,通常的反应是禁用编译器的警告功能,或者干脆不理会这些信息,这两种做法都是不可取的
    在任何情况下都要打开你的编译器的警告功能,如果代码产生了任何警告信息,应立即修正代码,让编译器的报错声停下来。在启用了警告功能之后,不要对不能安静地完成编译的代码感到满意。警告的出现总归是有原因的,即使你认为某个警告无关紧要,也不要置之不理,否则,总有一天这个警告会隐藏一个确实重要的警告
  7. 使用静态分析工具
    编译器警告是对代码的一次有限的静态分析,即在程序运行之前执行代码的检查的结果,还有许多独立的静态分析工具可以使用,在日常编程工作中,应该包括使用这些工具来检查你的代码,它们会比编译器跳出更多的错误
  8. 使用安全的数据结构
    如果做不到,就安全地使用危险的数据结构。最常见的安全隐患大概是由缓冲溢出引起的。缓冲溢出是由于不正确的使用固定大小的数据结构而造成的。如果代码在没有检查一个缓冲的大小之前就写入这个缓冲,那么写入的内容总是可能会超出缓冲的末尾的
  9. 检查所有的返回值
    如果一个函数返回一个值,它这样做肯定是有理由的。检查这个返回值,如果返回值是一个错误的代码,就必须辨别这个代码并处理所有的错误,不要让错误悄无声息的侵入程序,忍受错误会导致不可预知的行为,这既适用于用户自定义的函数,也适用于标准库的函数,大多数难以察觉的错误都是因为程序员没有检查返回值而出现的,不要忘记,某些函数会通过不同的机制返回错误,不论合适都要在适应的级别上捕获和处理相应的异常
  10. 谨慎的处理内存 和其他宝贵的资源
    对于在执行期间所获取的任何资源,必须彻底释放。内存使这类资源最常提到的一个例子,但是并不是唯一的一个。文件和县城所也是必须小心使用的宝贵资源,做一个好管家
    不要因为觉得操作系统会在你的程序退出时候清除程序,就不注意关闭文件或释放内存。对于代码还会执行多长时间,是否会耗尽所有的文件句柄或者占用所有的内存,你对此一无所知,甚至不能肯定操作系统是否会完全释放你的资源,有的操作系统不是这样的。Java 和 .NET 使用垃圾回收机制来执行这些繁重的清洁工作,在运行时会不时的清扫,不过不要因此对安全性抱有错误的想法,你仍然需要思考,你必须显示地终止对那些不再需要,或者不会被自动清除的对象的引用:不要意外的保留对对象的引用。不太现今的垃圾回收器也很容易被循环引用蒙蔽(A引用B,B又引用A,除此之外没有对A和B的引用),这就会导致对象永远不会被清除;这就是一种难以发现的内存泄露形式
  11. 在声明位置初始化所有变量
  12. 尽可能推迟一些声明变量
    这样可以使变量的声明位置与使用它的位置尽量接近,从而防止它干扰代码的其他部分,这样做也是的使用变量的代码更加清晰,不再需要导出寻找变量的类型和初始化,在附近声明使用这些都变得非常明显。不要在多个地方重用同一个临时变量,即使每次使用都在逻辑上相互分离的区域中进行。变量重用会使得以后对代码重新完善的工作变得异常复杂,每次创建一个新的变量---编译器会解决任何有关效率的问题
  13. 使用标准语言库
    明确的定义你正在适应的是哪个语言版本,除非项目要求,否则不要将命运交给编译器,或者对该语言的任何非标准的扩展。如果该语言的某个领域还没定义,就不要依赖你所使用的特定编译器的行为,这样会产生非常脆弱的代码
  14. 使用好的诊断信息日志工具
    当编写新的代码时候,常会加入许多诊断信息,以确定程序的运行情况,在调试结束后是否应该删除这些诊断信息呐?保留这些信息对以后再次访问代码会带来很多方便,特别是如果在此期间可以有选择地禁用这些信息。有许多诊断信息日志系统可以帮助实现这种功能,这些系统中很多都可以使诊断信息在不需要的时候不带来任何开销,可以有选择的使它们不参加编译
  15. 谨慎地进行强制转换
    大多数语言都允许你将数据从一种类型强转或者转换为另一种类型,这种操作有时候比其他操作更成功,如果试着将一个64位的整数转换为较小的8位数据类型,那么其他56位会怎么样,你的执行环境可能会突然抛出异常,或者悄悄地将你的数据的完整性降级,因此程序就会表现出不正常的行为
  16. 细则
    低级别防御性代码的编写技巧有很多,这些技巧是日常编程工作的组成部分,包含在对现实世界的一种健康的怀疑当中,下面几条细则值得考虑:
    提供默认的行为
    遵从语言习惯
    检查数值的上下限
    正常设置常量

约束:
我们如何把编程时候做的一些设想 切实地与我们的软件联系起来,从而使它们不再成为随时会出现的问题?只需要编写一小段额外的代码来检查每种可能出现的情况。这段代码充当每个设想的记录,使设想从暗处走到了明处。通过这种方式,就把约束编入到了程序的功能和行为当中。
当约束被破坏了,遭到破坏的约束不仅仅是一个简单的容易找到和更正的运行时的错误,事实上我们已经无时无刻不在做这种检查和处理,它一定是存在于程序逻辑中的一个缺陷,我们对程序可能做出的反应有以下几种:

  1. 对问题视而不见,期望最终不会因此出现任何差错
  2. 做一些小改动,使程序得以继续运行,如打印一份诊断报告,或者将错误记录到日志中
  3. 直接把程序打入冷宫,不让它继续运行下去,以立即受控或者非受控的方式终止程序

在许多不同的场景中都需要运用约束:

  1. 前置条件:这些条件是在输入一段代码之前必须保持为真的条件,如果前置条件不成立,那是因为客户端代码的缺陷所致
  2. 后置条件:这些条件是编写一段代码之后必须保持为真的条件,如果后置条件不成立,那是因为提供者代码的错误所致
  3. 不变条件:这些条件是每当程序的执行到达一个特定点,如循环中,方法调用等等,时都保持为真的条件,如果不变条件不成立,则意味着程序逻辑存在错误
  4. 断言:断言是任何其他关于程序在给定位置状态的陈述

#如果没有语言的支持,实施上面的所列的前两个条件将非常困难,如果一个函数有多个退出点,那么插入后置条件就会非常麻烦。Eiffel在核心语言中支持前置和后置条件,并且也可以确保约束校验不带有任何副作用。虽然很乏味,但是代码中所表达的好的约束可以使你的程序更加清晰,也更加易于维护,由于约束构成了代码段之间一个不可改变的契约,所以这一技术也称作“契约式设计”(design by contract)

  1. 约束内容
    a. 检查所有的数组访问是否都在边界内;
    b. 在废弃的指针之前断言指针是非零的;
    c. 确保函数的参数有效;
    d. 在函数的结果返回之前对其进行充分的检查;
    e. 在操作对象之前证明他的状态是一致的;
    f. 警惕代码中会写下的注释“不应该执行到这里”的任何一段;
    g. 可读性是衡量程序质量的最佳标准,如果一个程序易于阅读,那么这个程序就可能是一个好的程序
    h. 在主要的函数中放置前置条件和后置条件,并且在关键的循环中放置不变条件就已足够
  2. 移除约束
    通常只有在程序构建的开发和调试阶段,才需要这种约束校验,当用约束使自己确信(无论实际对错)程序的逻辑是正确之后,理论上就应该移除它们,从而节省很多不必要的运行时的开销

进攻性编程:需要主动尝试打破代码中的常规,而不只是防止问题的发生,也就是,不是保护代码,而是主动进攻代码,这种策略也叫测试。严格的测试对软件考法具有不可低估的正面影响,可以极大地提高代码质量,并且使开发过程变得稳定起来
都应该成为进攻性程序员
笔者等项目时间宽松点的时候,在继续搜集进攻性编程资料,然后再整理


总结:
编写不仅正确而且优秀的代码非常重要,这需要积累下所有已作出的设想,这将会使得维护更加容易,也会使得错误减少。防御性编程是一种预想最坏的情况并为之做好准备的方法。这是一种可以防止简单的错误变得难以找到的错误的技术
与防御性代码一起使用编入代码的约束,可以使你的软件更加健壮。与其他好的编码习惯,如单元测试一样,防御性编程也是明智的并且比较早的多花一些额外的时间,以便在以后节省更多的时间,精力和成本,这样可以使整个项目免于crash


如果错误攻破了你谨慎的防御,你将需要一种策略来驱逐它们
防御性编程是编写安全的软件系统的关键技术
你必须记录前置和后置条件,不然别人怎么知道它们的存在?如果你指定了任何约束,就可以添加防御性代码来断言它们


何时进行防御性编程?
你是否在事情不顺利时候才开始这么做?或者在整理了一些你不理解的代码的时候才开始?这样是不对的,应该从始至终地使用这些防御性编程的技巧。成熟的程序员已经从经验中得到教训,在吃过不止一遍的苦头后才明白增加防御措施是明智的。
在开始编写代码的时候就应用防御性策略,比改进代码时才应用要容易的多。如果恨晚才试着将这些策略强加进去,就不可能做到万无一失。如果在问题出现后才开始添加防御性代码,实际上是在调试,被动地做出反应,而不是积极地防患于未然
然而在调试的过程中,甚至在添加新的功能时候,会发现一些希望验证的情况,这常常是添加防御性代码的好时机


糟糕的程序员:
不愿意去考虑他们的代码出错的情况
为继承才发布可能会出错的代码,并希望别人会找到错误
将关于如何使用他们代码的信息紧紧攥在手里,并随时都可能将其丢掉
很少思考他们正在编写的代码,从而产生不可预知的和不可靠的代码

优秀的程序员:
关心他们的代码是否健壮
确保每个设想都是显式地体现在防御性代码中
希望代码对无用信息的而输入有正确的行为
在编码的时候认真思考所编写的代码
编写可以保护自己不受其他人或者程序员自己 愚蠢伤害的代码


 不定期更新 不合适的地方 还请指点~ 感激不尽