3-防御式编程

来源:互联网 发布:三菱gxworks2软件下载 编辑:程序博客网 时间:2024/05/19 06:48

3-防御式编程

子程序应该不因传入错误数据而被破坏,哪怕是由其他子程序产生的错误数据.核心想法是要承认程序都会有问题,都需要被修改,聪明的程序员应该根据这一点来编程序.

墨菲法则是指“任何有可能出错的事终将出错。 (Anything that can go wrong willgo wrong.)”。引申为“所有的程序都有缺陷”,或“若缺陷有很多个可能性,则它必然会朝情况最坏的方向发展”。

3.1 防御式编程的目标

General quality - Reducing the numberof software bugs and problems.
Making source code comprehensible -source code should be readable &understandable so it is approved in a code audit(代码审计)
Making software behave in a predictablemanner despite unexpected inputs or useractions.(使软件以可预测的方式运行,尽管意外的输入或用户操作。)

3.2 不对用户输入做假设

防御式编程和非防御式编程的区别在于开发人员不对特定功能的调用或库的使用情况做想当然的假定

image
image

3.3 免受非法数据的破坏

  • 检查来自于外部资源( external sources)的数据值:例如来源于网络或文件的数据的值
  • 检查子程序所有输入参数的值:类似于检查外部资源的数据,只不过数据是来自于其它子程序
  • 决定如何处理错误的输入数据:针对不同的错误类型进行处理;有十几种不同的方案可以选择(详见后面,书为8.3)

3.4 断言(Assertions)

断言是一种布尔表达式,用于在程序中表明该处所要满足的条件,它实际就是在开发期间使用的、让程序在运行时进行自检的代码

assert jobQueue.size() == 0 : "processB: queue should have been empty.";assert connector != null : "merge: Connector null for " + rel;

3.4.1 断言的组成

  • 布尔表达式
  • 断言不成立时打印的消息 , 如 “processB: …”;

3.4.2 断言的使用方式

  • 一个断言通常含有两个参数

    1. 一个描述假设为真时的情况的布尔表达式
    2. 一个断言为假时需要显示的信息
  • 常见的两种形式(以Java为例)

assert Expression1assert Expression1:Expression2

3.4.3 用断言检查可能出现的情形

  • 输入参数或输出参数的取值超出预期的范围
  • 子程序开始( /结束)执行时文件或流处在关闭状态
  • 子程序开始 (/结束) 执行时,文件或流的读写位置处于开头(或结尾)处;
  • 文件或流没有被成功打开
  • 仅用于输入的变量的值被子程序所修改
  • 指针为空
  • 传入子程序的数组或其他容器超过能容纳的数据元素个数

3.4.4 断言的使用

  • 可以在任何时候启用和禁用断言验证
  • 开发阶段或进行测试时启用断言
  • 部署时禁用断言
  • 投入运行后,遇到问题时可以重新起用断言
  • 单元测试常常使用断言(测试框架)

3.4.5 启用断言

  • 断言在默认情况下一般是关闭的,要显式启用
    1. 静态编译时启用断言
    2. 运行时启用断言

Java示例:
- 编译时启用断言,需要使用source1.4标记
1. avac source1.4 XXX.java
- 运行时启用断言需要使用-ea参数
1. java –ea XXX

C++ 示例

#define ASSERT( condition, message )    if ( ! (condition) ) (    LogError( "Assertion failed: “, #condition, message ) ;    exit( EXIT_FAILURE );}

C#

image

VSTS

image

Java

image
image

3.4.6 使用断言

  • 用错误处理代码来处理预期会发生的状况,用断言来处理绝不应该发生的状况
  • 避免把需要执行的代码放到断言中
  • 用断言来注解并验证前条件和后条件
  • 对于高健壮性的代码,应该先使用断言再处理错误

3.4.7 错误处理和断言

  • 错误处理代码(error-handling code) 是用来检查不太可能经常发生的非正常情况

    1. 如检查有害的输入数据
    2. 错误处理属于程序运行的“正常”情况
  • 断言是用来检查永远不该发生的情况

    1. 用于检查代码中的bug
    2. 触发了断言则应该修改程序的源代码并重新编译,然后发布软件的新版本

3.4.8 断言中不能包含执行代码

  • Visual Basic示例:一种危险的断言使用方法
    Debug.Assert( PerformAction() )
    Couldn’t perform action

  • Visual Basic示例:安全地使用断言
    actionPerformed = PerformAction()
    Debug.Assert( actionPerformed )
    Couldn’t perform action

3.4.9 用断言来验证前置和后置条件

  • 前置条件是子程序或类的调用方代码在调用子程序或实例化对象之前要确保为真的属性
    1. 前置条件是调用方代码对其所调用的代码要承担的义务
  • 后置条件是子程序或类在执行结束后要确保为真的属性
    1. 后置条件是子程序或类对调用方代码所承担的责任

image

image

3.5 错误处理

3.5.1 错误处理技术

  • 返回中立值( 如数值返回0、字符串操作可以返回空字符串、 指针操作可以返回一个空指针)
  • 换用下一个正确的数据 ( 处理数据流时)
  • 返回与前次相同的数据 ( 其思想就是“重用上一次正确的结果”,如 windows系统崩溃后用上一次配置重启动)
  • 换用最接近的合法值( 当数值超出其正常设定的上下界时采用,如经度设置为(-180,180) 之间)
  • 把警告信息记录到日志文件中 (要对错误信息进行标示,或者将警告信息单独存放,以便快速查询定位)
  • 用语言内建的异常机制抛出一个异常 ( 返回一个错误码:设置一个状态变量的值,用状态值作为函数的返回值,用语言内建的异常机制抛出一个异常)
  • 调用错误处理子程序或对象:优点:能把错误处理的职责都集中到一起;代价:错误处理代码与整个程序紧密耦合
  • 在局部处理错误:这种方法的思想是希望将错误问题限制在一个特定的区域中,而不要扩散;缺点是会掩盖错误,而是其他部分无法获知
  • 具体错误处理的选择是一种平衡选择的结果:如局部处理错误和设计全局性错误处理子程序或对象是相互对立的设计思想,但没有绝对的好坏

image

3.5.2 异常处理机制

  • 异常是把代码中的错误或异常事件传递给调用方代码的一种特殊手段
  • 如果在一个子程序中遇到了预料之外的情况,但不知道该如何处理的话,它就可以抛出一个异常

image

3.5.3 异常处理基本机构

  • 子程序使用throw 抛出一个异常对象,再被调用链上层
  • 其他子程序的try-catch 语句捕获

3.5.4 异常处理机制所涉及的要素

  • 异常对象
  • 对异常对象的抛出
  • 对所抛出异常的捕捉

image

Java

image

class StaticallyThrownExceptionsIncludeSubtypes {    public static void main(String[] args) {        try {            throw new FileNotFoundException();            // Statement "can throw" FileNotFoundException.            // It is not the case that statement "can throw"            // a subtype or supertype of FileNotFoundException.        } catch (FileNotFoundException fnfe) {        // Legal in Java SE 6 and 7.        } catch (IOException ioe) {            // Legal in Java SE 6 and 7, “catch IOException” catches IOException and any subtype            // but compilers are / encouraged to throw warnings as of Java SE 7        } catch (Throwable t) {            // Legal in Java SE 6 and 7.    }}

3.5.5 finally的使用

  • try-catch-finally与try-catch的区别在于finally

    1. finally保证了在程序执行时无论有没有异常被抛出、捕捉, finally块都会被执行
  • finally一般被用来进行清理工作

    1. 在异常处理时通过finally块来执行任何清除操作
    2. 最常见的就是关闭流、关闭连接、释放或销毁资源等

3.5.6 为什么C++没有finally

  • C++提供了RAII机制可以取代
    finally
  • RAII: Resource Acquisitions Initialization( 资源获取即初始化) 是指当获得任何资源( 对象/内存/文件句柄等)的时候,都会在其对象的构造函数中获得,并且其析构函数中释放
  • RAII的发明人:由被誉为C++之父的Bjarne Stroustrup创建
class Student{    private:    char *pName;   public:    Student()    {     pName=new char[20];    }    ~Student()    {    delete[] pName;    }};// ** * * Destructor * * * * //Foam::polyMesh::~polyMesh(){    clearOut();    resetMotion();}
  • Finally是面向特定的应用考虑资源问题
    1. 针对特定的资源使用情况构造try-catch-finally块,并通过finally释放被分配的资源
    2. 典型的支持语言: Java、 C#、 Object Pascal
  • RAII是面向资源本身考虑分配和回收的问题
    1. 为每一类资源提供一个封装类以实现资源的管理
    2. 典型的支持语言: C++、 D

3.5.7使用异常的建议

  • 用异常通知程序的其他部分,发生了不可忽略的错误
    1. 异常机制的优越之处在于它能提供一种无法被忽略的错误通知机制
    2. 其他的错误处理机制有可能会导致错误在不知不觉中向外扩散
  • 只在真正例外的情况下才抛出异常
    1. 仅在其他编码实践方法无法解决的情况下才使用异常
    2. 调用子程序的代码需要了解被调用代码中可能会抛出的异常,因此异常弱化了封装性
  • 不能用异常来推卸责任
    1. 如果某种的错误情况可以在局部处理,那就应该在局部处理掉它
    2. 未被捕获的异常指的是确实在局部不能确定或者处理掉的异常
  • 避免在构造函数和析构函数中抛出异常,除非在同一地方把它们捕获
    1. 不要影响构造函数和析构函数的正常结束
  • 在恰当的抽象层次抛出异常

    1. 抛出的异常也是程序接口的一部分,和其他具体的数据类型一样
      image
    2. GetTax ld ( ) 代码应抛回一个与其所在类的接口相一致的异常
      image
  • 在异常消息中加入关子导致异常发生的全部信息

    1. 要从阅读该异常的程序员的角度来写异常消息
    2. 要确保该消息中含有为理解异常抛出原因所需的信息
  • 避免使用空的catch 语句
    1. 空catch语句就是所catch部分没有操作
try {    ...} catch (ExceptionType name) { }
  • 了解所用函数库可能抛出的异常
    1. 借助IDE了解所使用的库函数会抛出哪些异常
  • 把项目中对异常的使用标准化
    1. 当抛出多种类型的异常,如对象、数据及指针的话则考虑建立一个标准
    2. 创建项目的特定异常类:构造自己的异常类继承体
    3. 规定哪些异常在局部处理,哪些在全局处理
    4. 规定是否可以在构造或析构函数处理异常
    5. 确定是否要使用集中的异常报告机制

image

3.6 健壮性与正确性

  • 正确性( correctness)
    1. 指软件按照需求正确执行任务的能力
    2. 确保正确性意味着永不返回不准确的结果,哪怕不返回结果也比返回不准确的结果好
  • 健壮性( robustness)
    1. 指软件对于规范要求以外的输入情况的处理能力
    2. 确保健壮性意味着要不断尝试采取某些措施,以保证软件可以持续地运转下去,哪怕有时做出一些不够准确的结果
      image
      image
      image
      image

3.7 通过隔离包容错误

  • 隔离程序
    1. 把某些接口选定为“安全”区域的边界,对穿越安全区域边界的数据进行合法性校验
    2. 隔离可以将检验工作集中在特定的模块中,从而降低其它部分采用防御式编程的成本
  • 隐喻:
    1. 船体外壳上装备的隔离舱
    2. 手术室的消毒处理

3.7.1 隔离:错误处理 vs. 断言

  • 隔离部分包含了“脏数据”
    1. 隔离部分的程序应使用错误处理技术,在那里对数据做的任何假定都是不安全的
  • 通过隔离部分之后的是“干净数据”
    1. 隔栏内部的程序里就应使用断言技术,因为传进来的数据应该己在通过隔栏时被清理过了

3.8 辅助调试的代码

  • 辅助调试的代码也称为调试助手,用来帮助快速地检测错误
  • 应该尽量早的引入辅助调试的代码
  • 可以采用第三方提供的专用工具,也可以针对特定情况自己开发

3.9 产品版和开发版

不要自动地把产品版的限制强加于开发版之上
- 产品级的软件要求能够快速地运行,而开发中的软件则允许运行缓慢
- 产品级的软件要节约使用资源,而开发中的软件在使用资源时可以比较奢侈
- 产品级的软件不应向用户暴露可能引起危险的操作,而开发中的软件则可以提供一些额外的、没有安全网的操作

3.10 进攻式编程

进攻式编程是主动暴露可能出现错误的态度

在开发阶段让它显现出来,而在产品代码运行时让它能够自我恢复

3.10.1 常用方法

  • 确保断言语句使程序终止运行
  • 完全填充分配到的所有内存、文件或流
  • 确保每个case 语句的default分支或else 分支都能产生严重错误(如终止程序)
  • 在删除一个对象之前把它填满垃圾数据

image

3.11 如何使用辅助的调试代码

使用ant 和maven等类似的版本控制工具和make 工具
- 在开发模式下,让make 工具把所有的调试代码都包含进来一起编译
- 在产品模式下,又可以让make 工具把那些不希望包含在商用版本中的调试代码排除在外。

3.11.1 使用内置的预处理器

Example 8-13. C++ Example of Using the Preprocessor Directly to Control Debug Code#define DEBUG <-- 1 ...#if defined( DEBUG )// debugging code...#endif

3.11.2 更简约的形式

Example 8-14. Using the Preprocessor Macro to Control Debug Code#define DEBUG#if defined( DEBUG )    #define DebugCode( code_fragment ) { code_fragment }#else    #define DebugCode( code_fragment )#endif...DebugCode(    statement 1;    statement 2;    ...    statement n;);

3.11.3 使用调试存根

image
image
image

3.12 保留多少防御式代码

  • 保留那些检查重要错误的代码
  • 去掉检查细微错误的代码
  • 去掉可以导致程序硬性崩溃的代码 (用户无法保存必要数据)
  • 保留可以让程序稳妥地崩溃的代码 (用于诊断潜在的严重错误)
  • 为技术支持人员记录错误信息
  • 确认留在代码中的错误消息是友好的

1. 小结

1.1 防御式编程的主要思想

防御式编程是防御式设计(defensive design)的一种表现形式,它是为了保证对程序的不可预见的使用不会造成之后程序功能的破坏。
防御式编程的思想可以看成是为了尽量消除或降低墨菲法则(Murphy‘s Law)带来的影响。
防御式编程技术主要使用在那些容易发生错误输入以及错误的使用会带来灾难性后果的程序片段上。

1.2 墨菲法则(了解)

墨菲法则是指“任何有可能出错的事终将出错”。引申为“所有的程序都有缺陷”,或“若缺陷有很多个可能性,则它必然会朝往令情况最坏的方向发展”。

1.3 断言(重要)

断言是一种布尔表达式,用于在程序中表明该处所要满足的条件,它实际就是在开发期间使用的、让程序在运行时进行自检的代码

1.3.1 断言的使用方式

  • 一个断言通常含有两个参数

    1. 一个描述假设为真时的情况的布尔表达式
    2. 一个断言为假时需要显示的信息
  • 常见的两种形式(以Java为例)

assert Expression1assert Expression1:Expression2

1.3.2 使用断言

  • 用错误处理代码来处理预期会发生的状况,用断言来处理绝不应该发生的状况
  • 避免把需要执行的代码放到断言中
  • 用断言来注解并验证前条件和后条件
  • 对于高健壮性的代码,应该先使用断言再处理错误

1.3.3 错误处理和断言

  • 错误处理代码(error-handling code) 是用来检查不太可能经常发生的非正常情况

    1. 如检查有害的输入数据
    2. 错误处理属于程序运行的“正常”情况
  • 断言是用来检查永远不该发生的情况

    1. 用于检查代码中的bug
    2. 触发了断言则应该修改程序的源代码并重新编译,然后发布软件的新版本

1.4 错误处理(了解)

  • 用来处理那些预料中可能要发生的错误情况
  • 返回中立值(如数值返回0)
  • 换用下一个正确的数据
  • 返回与前次相同的数据
  • 换用最接近的合法值
  • 把警告信息记录到日志文件中
  • 用语言内建的异常机制抛出一个异常

1.5 异常处理(重要)

1.5.1 异常处理基本结构

子程序使用throw抛出一个异常对象,再被调用到链上层其他子程序的try-catch语句捕获

1.5.2 异常处理机制

  • 异常是把代码中的错误或异常事件传递给调用方代码的一种特殊手段
  • 如果在一个子程序中遇到了预料之外的情况,但不知道该如何处理的话,它就可以抛出一个异常

image

1.5.3 异常处理基本机构

  • 子程序使用throw 抛出一个异常对象,再被调用链上层
  • 其他子程序的try-catch 语句捕获

1.5.4 异常处理机制所涉及的要素

  • 异常对象
  • 对异常对象的抛出
  • 对所抛出异常的捕捉

1.5.5 改进使用不当的异常

  • 在恰当的抽象层次抛出异常
    1. 抛出的异常也是程序接口的一部分,和其他具体的数据类型一样
      image
    2. GetTax ld ( ) 代码应抛回一个与其所在类的接口相一致的异常
      image

1.5.6 finally的使用

  • try-catch-finally与try-catch的区别在于finally

    1. finally保证了在程序执行时无论有没有异常被抛出、捕捉, finally块都会被执行
  • finally一般被用来进行清理工作

    1. 在异常处理时通过finally块来执行任何清除操作
    2. 最常见的就是关闭流、关闭连接、释放或销毁资源等

1.6 通过隔离包容错误

  • 隔离程序
    1. 把某些接口选定为“安全”区域的边界,对穿越安全区域边界的数据进行合法性校验
    2. 隔离可以将检验工作集中在特定的模块中,从而降低其它部分采用防御式编程的成本

1.6.1 隔离:错误处理 vs. 断言

  • 隔离部分包含了“脏数据”
    1. 隔离部分的程序应使用错误处理技术,在那里对数据做的任何假定都是不安全的
  • 通过隔离部分之后的是“干净数据”
    1. 隔栏内部的程序里就应使用断言技术,因为传进来的数据应该己在通过隔栏时被清理过了

1.7 产品版和开发版

不要自动地把产品版的限制强加于开发版之上
- 产品级的软件要求能够快速地运行,而开发中的软件则允许运行缓慢
- 产品级的软件要节约使用资源,而开发中的软件在使用资源时可以比较奢侈
- 产品级的软件不应向用户暴露可能引起危险的操作,而开发中的软件则可以提供一些额外的、没有安全网的操作

1.8 进攻式编程

进攻式编程是主动暴露可能出现错误的态度

在开发阶段让它显现出来,而在产品代码运行时让它能够自我恢复

1.8.1 常用方法

  • 确保断言语句使程序终止运行
  • 完全填充分配到的所有内存、文件或流
  • 确保每个case 语句的default分支或else 分支都能产生严重错误(如终止程序)
  • 在删除一个对象之前把它填满垃圾数据

image

0 0