首页 > 代码库 > 第8章防范式编程上(代码大全3)

第8章防范式编程上(代码大全3)

<style></style>

  防御式编程并不是说让你在编程时持“防备批评或攻击”的态度——“它就是这么工作!”这一概念来自防御式驾驶。在防御式驾驶中要建立这样一种思维,那就是你永远也不能确定另一位司机将要做什么。这样才能确保其他人在做出危险动作时你也不会受到伤害。你要担负起保护自己的责任,哪怕是其他司机犯的错误。防御式编程的主要思想是:子程序应该不因为传入错误数据而被破坏,哪怕是由其他子程序产生的错误数据。更一般地说,其核心是要承认程序都会有问题,都需要被修改,聪明的程序员应该根据这一点来编程序

8.1 Protecting Your Program from invalid Inputs

   保护程序免遭非法输入数据的破坏

  在学校里你可能听说过“垃圾进,垃圾出(garbage in, garbage out)”这句话,这句话说的是软件开发领域“出门概不退换”原则:让用户自己操心自己的事。

  对已形成产品的软件而言,仅仅“垃圾进,垃圾出”还不够。不管进来什么,好的程序都不会生成垃圾,而是做到“垃圾进,什么都不出”、“进来垃圾、出去错误提示”或“不许垃圾进来”。按今天的标准来看,“垃圾进,垃圾出”已然成为缺乏安全性的差劲程序的标志。

  通常有三种方法来处理进来垃圾的情况。

  检查所有来源于外部的数据的值 当从文件、用户、网络或其他外部接口中获取数据时,应检查所获得的数据值,以确保它在允许的范围内。对于数值,要确保它在可接受的取值范围内:对于字符串、要确保其不超长。如果字符串代表的是某个特定范围内的数据(如金融交易ID或其他类似数据),那么要确认其取值合乎用途,否则就应该拒绝接受。如果你在开发需要确保安全的应用程序,还要格外注意哪些狡猾的可能是攻击你的系统的数据,包括企图令缓冲区溢出的数据、注入SQL命令、注入的HTML或XML代码、整数溢出以及传递给系统调用的数据,等等。

  检查子程序所有输入参数的值 检查子程序输入参数的值,事实上和检查来源于外部的数值一样,只不过是数据来自于其他子程序而非外部接口。

  决定如何处理错误的输入数据 一旦检测到非法的参数,你该如何处理它呢?根据情况的不同,你可以从十几种不同的方案中选择其一。

  防御式编码的最佳方式就是在一开始就不要在代码中引入错误。使用迭代设置、编码前先写伪代码、写代码前先写测试用例、底层设计检查等活动,都有助于防止引入错误。因此,要在防御式编程之前优先运用这些技术。所幸的是,你可以把防御式编程和其他技术结合起来使用。

8.2 Assertions

  断言

  断言(assertion)是指在开发期间使用的、让程序在运行时进行自检的代码(通常是一个子程序或宏)。断言为真,则表明程序运行正常,而断言为假,则意味着它已经在代码中发现了意料之外的错误。举例来说,如果是系统假定一份客户信息文件所含的记录数不能超过50 000,那么程序中可以包含一个断定记录数小于等于50 000的断言。只要记录数小于等于50 000,这一断言都会默默无语。然而一旦记录数超过50 000,它就会大声地“断言”说程序中存在一个错误。

  断言对于大型的复杂程序或可靠性要求极高的程序来说尤其有用。通过使用断言,程序员能更快第排查出因修改代码或者别的原因,而弄进程序的不匹配的接口假定和错误等。

  一个断言通常有两个参数:一个描述假设为真时的情况的布尔表达式,和一个断言为假时需要显示的信息。下面是假定变量denominator(分母)的值应为非零值时Java断言的写法:

package com.rollercoaster.codecomplete;/** * 断言的使用 * @author zhujinrong * */public class AboutAssertion {    /**     * 入口函数     * @param args     */    public static void main(String args[]) {        System.out.println("1 / 2 = " + divide(1, 2));        System.out.println("1 / 0 = " + divide(1, 0));    }    /**     * 计算除法     * @param a 除数     * @param b 被除数     * @return a / b     */    public static double divide(double a, double b) {        assert b != 0 : "b is unexpectedly equal to 0!";        return a / b;    }}

  打开断言之后,java断言部分, 参考另一篇文章:01 java断言assert初步使用:断言开启、断言使用

  运行结果如下:

  1 / 2 = 0.5
  Exception in thread "main" java.lang.AssertionError: b is unexpectedly equal to 0!
    at com.rollercoaster.codecomplete.AboutAssertion.divide(AboutAssertion.java:30)
    at com.rollercoaster.codecomplete.AboutAssertion.main(AboutAssertion.java:17)

  断言可以用于在代码中说明各种假设,澄清各种不希望的情形。可以用断言检查如下这类假定:

  •   输入参数或者输出参数的取值处于预期的范围;
  •   子程序开始(或者结束)执行文件或者流是处于打开(或关闭)的状态。
  •   子程序开始(或者结束)执行时,文件或流的读写位置处于开头(或结尾)处;
  •   文件或流已用只读、只写或者可读可写方式打开;
  •   仅用于输入的变量的值没有被子程序所修改;
  •   指针非空;
  •   传入子程序的数组或其他容器至少能容纳X个数据元素;
  •   表已初始化,存储真实的数值;
  •   子程序开始(或结束)执行时,某个容器是空的(或满的);
  •   一个经过高度优化的复杂子程序的运算结果和相对缓慢但代码清晰的子程序的运算结果相一致。

  正常情况下,你并不希望用户看到产品代码中的断言信息;断言主要是用于开发和维护阶段。通常断言只是在开发阶段被编译到目标代码中,而在生成产品代码时并不编译进去。在开发阶段,断言可以帮助查清相互矛盾的假定、预料之外的情况以及给子程序的错误数据等。在生成产品代码时,可以不把断言编译进目标代码里去,以免降低系统的性能

Building Your Own Assertion Mechanism

建立自己的断言机制

Guidelines for Using Assertion

使用断言的指导建议

  用错误代码来处理预期会发生的情况,用断言来处理绝不应该发生的状况。断言是用来检查永远不该发生的情况,而错误处理代码(error-handling code)是用来检查不太可能经常发生的非正常情况,这些情况是在写代码时就能预料到的,且在产品代码中也要处理这些情况。错误处理通常用来检查有害的输入数据。而断言是用于检查代码中的bug。

  避免把要执行的代码放到断言中。这样的话,一旦关闭断言,正常的逻辑也没有执行了。

  用断言来注解并验证前条件和后条件。

  对于高健壮性的代码,应该先使用断言再处理错误。对于每种可能出错的条件,通常子程序要么使用断言,要么使用错误处理代码来进行处理,但是不会同时使用二者。一些专家主张只须使用一种处理方法即可。

8.3 Error-handling Techniques

  错误处理技术

  断言可以处理代码中不应该发生的错误。那么又如何处理那些预料中可能要发生的错误呢?主要有以下几种处理方式:

  返回中立值 有时,处理错误数据的最佳做法就是继续执行操作并简单地返回一个没有危害的数值。比如说,数值计算可以返回0,字符操作可以返回空字符串,指针操作可以返回一个空指针。

  换用下一个正确的数据

  返回与前次相同的数据

  换用最接近的合法值

  把警告信息记录到日志文件中 在检测到错误数据的时候,你可以选择在日志文件(log file)中记录一条警告信息,然后继续执行。

  返回一个错误码 你可以决定只让系统的某些部分处理错误。其他部分则不在本地(局部)处理错误,只是简单地报告说有错误发生,并信任调用链上游的某个子程序会处理该错误。通知系统其余部分已经发生错误可以采用下列方法之一:

  •   设置一个状态变量的值
  •   用状态值作为函数的返回值
  •   用语言内建的异常机制抛出一个异常

  在这种情况下,与确定特定的错误报告机制相比,更为重要的是要决定系统里哪些部分应该直接处理错误,哪些部分只是报告所发生的错误。如果安全性很重要,请确认调用方的子程序总会检查返回的错误码。

  调用错误处理子程序或对象 另一种方法则是把错误处理都集中在一个全局的错误处理子程序或对象中。这种方法的优点在于能把错误处理的职责集中到一起,从而让调试工作更为简单。而代价则是整个程序都要知道这个集中点并与之紧密耦合。如果你想在其他系统中重用其中的某些代码,那就得把错误处理代码一并带过去。

  这种方法对代码的安全性有一定重要的影响。如果代码发生了缓冲区溢出,那么攻击者很可能已经篡改了这一处理程序或对象的地址。这样一来,一旦在应用程序运行期间发生缓冲区溢出,再使用这种方法就不再安全了。

  当错误发生时显示出错信息 这种方法可以把错误处理的开销减到最少,然而它也可能会让用户界面出现信息的信息散布到整个应用程序中。当你需要创建一套统一协调的用户界面时,或当你想让用户界面部分与系统的其他部分清晰的分开时,或者当你想把软件本地化到另一种不同的语言时,都会面临挑战。还有,当心不要告诉系统的潜在攻击者太多东西。攻击者有时能利用错误信息来发现如何攻击这个系统。

  用最妥当的方式在局部处理错误 一些设计方案要求在局部解决所遇到的错误——而具体使用何种错误处理方法,则留给设计和实现会遇到错误的这部分系统的程序员来决定。

  关闭程序 有些系统一旦检测到错误发生就会关闭。这一方法适用于人生安全攸关(safety-critical)的应用程序。

High-Level Design Implications of Error Processing

高层次设计对错误处理方式的影响

  既然有这么多的选择,你就必须注意,应该在整个应用程序里采用一致的方式处理非法的参数。对错误进行处理的方式会直接关系到软件能否满足在正确性、健壮性和其他非功能性指标方面的要求。一旦确定了某种方法,就要确保始终如一的贯彻这一方法。