防御式编程

Phoebe Li

Sept. 26

研讨目的

  • 什么是防御式编程
  • 为什么要进行防御式编程
  • 如何防御式编程

主要内容

  • 保护程序免遭非法数据的破坏
  • 断言
  • 错误处理技术
  • 异常
  • 隔离程序
  • 防御的姿态

基本概念

  • 防御式驾驶与防御式编程

主要思想:子程序应该不因传入错误数据而被破坏,哪怕是由其他子程序产生的错误数据。更一般的说,其核心想法是要承认程序都会有问题,都需要被修改。

Anything that can go wrong will go wrong.

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

  • 检查所有来源于外部的数据的数据的值
  • 检查子程序所有输入参数的值
  • 决定如何处理错误的输入数据

三种常见方法

2、断言

举例来说,如果系统假定一份客户信息文件所含的记录数不能超过50000,那么程序中可以包含一个断定记录数小于等于50000的断言。只要记录数小于等于50000,这一断言就会默默无语,然而一旦记录数超过50000,它就会大声的“断言”说程序中存在一个错误。

断言是指在开发期间使用的、让程序在运行时进行自检的代码(通常是一个子程序或宏)。

public static void main(String[] args) {
  int customerNum = 10000;
  assert customerNum <= 50000 : 
    "customer number is more than 50000.";
}

Node.js中的断言

断言(Assert)模块用于为应用编写单元测试,可以通过require('assert')对该模块进行调用。

var assert= require('assert');
exports.index = function(req, res){
assert.equal('1', '2', ['1和2是不相等的']);
//assert.ifError(true);
res.render('index', { title: 'Express' });
};

使用断言的指导建议

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

--word同时对一个条件使用断言和错误处理,以提高健壮性

  • 断言主要用于开发和维护阶段,而在生成产品代码时并不把断言编译进去

3、错误处理技术

  • 返回中立值
  • 换用下一个正确的数据
  • 返回与前次相同的数据
  • 换用最接近的合法值
  • 把警告信息记录到日志文件中
  • 返回一个错误值
  • 调用错误处理子程序或对象
  • 当错误发生是显示出错信息
  • 用最妥当的方式在局部处理错误
  • 关闭程序

健壮性与正确性

处理错误最恰当的方式要根据出现错误的软件的类别而定。

  • 正确性意味着永不返回不准确的结果,哪怕不返回也比返回不准确的结果好。
  • 健壮性意味着要不断尝试采取某些措施,以保证软件可以持续的运转下去,哪怕有时做出一些不够准确的结果。

一致性

应该在整个程序中采用一致的方式处理非法的参数,一旦确定了某种方法,就要确保始终如一地贯彻这一方法。

错误处理和断言

  • 错误处理代码是用来检查不太可能经常发生的非正常情况
  • 如, 检查有害的输入数据
  • 错误处理属于程序运行的“正常”情况
  • 断言是用来检查永远不该发生的情况
  • 用于检查代码中的bug
  • 触发了断言则应该修改程序的源代码并重新编译,然后发布软件的新版本

4、异常

异常是把代码中的错误或异常事件传递给调用方代码的一种特殊手段。

在Node.js 中 通常我们使用的try/catch/final异常捕获语句,对于异步编程而言并不一定适用。try/catch操作只能捕获当次事件循环内的异常,对callback执行时抛出的异常将无能为力,node在处理异常上形成了一种约定,将异常作为回调函数的第一个实参传回,如果为空值,则表明异步调用没有异常抛出。

 

async(function (err, results) {
  //TODO
});

异常的基本结构

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

try {
  ...
} catch (ExceptionType1 name) {
  ...
} catch (ExceptionType2 name) {
  ...
} finally {
  ...
}

:finally 一定会执行,一般用来进行清理工作,如关闭流,关闭连接,释放或销毁资源

在异步方法的编写中,另一个容易犯的错误是对用户传递的回调函数进行异常捕获

try {
  req.body = JSON.parse(buf, options.reviver);
  callback();
} catch (err){
  err.body = buf;
  err.status = 400;
  callback(err);
}
try {
  req.body = JSON.parse(buf, options.reviver);
} catch (err){
  err.body = buf;
  err.status = 400;
  return callback(err);
}
callback();

          这段代码的意图是捕获JSON.parse()中可能出现的异常,但却错误的包含了用户传递的回调函数,若回调函数有异常,将导致回调函数执行两次,这可能导致业务混乱。

node.js在0.8版本发布时增加了domain模块,该模块提供一种方式用来将不同的IO操作归总到一个组中。如果任何注册的时间触发器或者回调函数触发了error事件或者throw了error对象,domain对象将会被通知到。

以上是官方的一个例子,很明显当匿名函数throw er后,将会被d.on('error',function(er){})捕获,并做处理。

var d = domain.create();
d.on('error', function(er) {
  console.error('Caught error!', er);
});
d.run(function() {
  process.nextTick(function() {
    setTimeout(function() { // simulating some various async stuff
      fs.open('non-existent file', 'r', function(er, fd) {
        if (er) throw er;
        // proceed...
      });
    }, 100);
  });
});

           node.js的domain模块确实解决了很多重复代码的工作,在开发过程中建议将domain模块进行封装,单独写一个模块用来管理各种不同的错误处理,减少domain.create(),尽量使用1个或几个domain对象来处理整个应用的错误,便于管理

总结

使用异常处理的指导建议

  • 用异常通知程序的其他部分,发生了不可忽略的错误
  • 只有真正例外的情况下才抛出异常
  • 不能用异常来推卸责任
  • 避免在构造函数和析构函数中抛出异常,除非你在同一地方把它们捕获
  • 在异常消息中加入关于导致异常发生的全部信息
  • 避免使用空的 catch 语句
  • 了解所用函数库可能抛出的异常
  • 考虑创建一个集中的异常报告机制
  • 把项目中对异常的使用标准化

5、隔离程序

隔离程序: 使之包容由错误带来的损害,让程序的某些部分处理“不干净”的数据,某些部分处理“干净”的数据。

在隔离之外的假定所有数据都是不安全的,采用错误处理手段;在隔离之内的就假设数据都是安全的,使用断言来处理。

确定在产品代码中该保留多少防御式代码

  • 保留那些检查重要错误的代码
  • 去掉检查细微错误的代码
  • 去掉可以导致程序硬性崩溃的代码
  • 保留可以让程序稳妥地崩溃的代码
  • 为你的技术支持人员记录错误信息
  • 确认留在代码中的错误消息是友好的

6、防御的姿态

          软件的好坏与其健壮性有很大的联系,所有的软件开发人员都要对它有足够的重视,从一点一滴开始做起,不要忽视任何的细节,不能盲目依赖测试去发现bug,而是以测试驱动编程,不断地思考可能发生的问题以进行预防,这才是防御式编程。

防御式编程核对表

《代码大全》第八章211页

  • 一般事宜
  • 异常
  • 安全事宜

Thanks !

防御式编程

By Phoebe Li

防御式编程

  • 382