防御式编程

防御式编程:这个概念其实来源于驾驶员,简单来说,当你开车上路的时候要时刻保持警觉,假设路上你遇到的每一辆车都有可能向你撞过来造成危险。在coding里面,要假设每个输入都不一定符合设计之初的假设,要使用一定的语句对输入进行限定。尽量做到”垃圾进,什么都不出“。
使用防御式编程常见的语句有:断言和错误处理语句

断言:
assert(condition)功能语句是一种可以保证其condition为真的语句,是一种在调试阶段可以发现问题的好工具.其用于保证内部子程序的某些变量必须为真的情况.

在实际的工作中,断言使用的情况并不少。使用断言的一个好处就是可是在开发的过程中尽早地暴露软件的缺陷。实际的开发中有一些看起来莫名其妙的错误是很难被复现的,而这些错误很有可能是因为软件中的某个部分并不在预期的范围内:

//这是一个很简单的按照工龄计算工资的函数
//@param age 员工的工龄

double cal_salary(int age){
    double salary;

    //拼命计算工资,而且工资与工龄挂钩... 

    return 0.5*age*salary;
}

假设老板某总一天突发奇想,用这个函数(这个函数还真奇葩的)来计算员工的工资,突然码农A跑过来说为什么我的工资是负的?我辛辛苦苦干了这么久没功劳也有苦劳啊,为什么,为什么。。。
仔细看一下这个计算工资的函数,其输入工龄在现实世界里面肯定是一个正数,但在这个函数里面却没有体现,一旦输入错误则导致输出一个负的工资,这显然与现实世界不相符。所以这个函数在开发的时候应该是这样的:

double cal_salary(int age){
    assert(age>0);
    double salary;

    //拼命计算工资,而且工资与工龄挂钩... 

    return 0.5*age*salary;
}

又譬如在面试中很有可能遇到的字符串拷贝函数问题,下面是一个常见的版本:

void strcpy(char *dst, const char *src){
    assert(dst && src);
    while( (*dst++ = *src++) != '\0')
}

注意:断言只能在调试模式下有效,在正式版的release下断言功能无效,故不要把函数调用语句放在断言内,而应该将某一个值放在断言内检查

除了断言,另一种防御式编程常见的手段是错误处理语句,这在代码里面更加常见

  断言与错误处理语句:
  错误处理语句用于处理已经预测得到的,可以预知的情况,而断言则处理绝对不应该发生的情况.错误处理语句用于检查有害的输入数据,而断言则用于检查代码中的BUG.前者处理反常情况时程序员可以从容应对,而后者出现问题时(程序触发断言时)则需要好好修改程序并重新编译了

对于大型,复杂,健壮性要求高的程序.应先使用断言再使用错误处理语句.因为大型程序的生命周期长,在放出一个release版本之后也会继续更新,故应使用断言.

其实在应用软件中,错误处理语句用的比断言会更为广泛,在今天的互联网时代,用户体验非常重要(其实一直都是很重要,只不过现在有了互联网换一个同类型软件的成本比以前大为下降,所以用户体验在提高用户忠诚这方面很有作用),我们总不能希望用户在使用软件的时候老出现崩溃需要重启软件吧?所以一旦软件的某个部分一旦出现小错误,可以用错误处理语句把错误控制在一个较小的范围内,就像现代轮船上面的隔离门一样:

现代轮船有水密舱,当轮船某个地方进水的时候可以将进水的区域隔离在一定范围内

在实际的敲代码过程中,我们可以利用错误处理语句这么来coding.还是利用上面的奇葩工资计算函数:

//还是这个奇葩的工资计算函数
double cal_salary(int age){
    double salary;

    //错误处理代码
    if(age<0)
        return 0;
    if(age==0)
        return basic_salary;

    //拼命计算工资,而且工资与工龄挂钩... 

    return 0.5*age*salary;
}

这样一来员工再也不会找老板的麻烦啦!

健壮性与正确性: 健壮性:不断尝试一些措施,使得软件无论如何都能运行下去,即使有一些不正确的结果.常见于消费类软件. 正确性:永远不返回不正确的结果,哪怕不返回结果也比返回错误结果好. 常见于人命攸关的软件,银行软件等

关于错误处理:在一整个程序的开发中,一旦确定了某种错误处理方法,就应该一如既往的在整个软件开发过程中使用此错误处理方法,如一旦确定由上层应用处理错误,则底层只需要报告错误类型以及给出错误信息即可,并且保证在整个软件开发过程中均使用此错误处理方法.

其实不论是错误处理语句,还是断言,其本身就是一种代码编写的习惯与风格,在实际项目中最好开始就讨论出来对于错误处理的原则,这样相对比较方便交流

关于异常:慎重的使用异常可以减低软件复杂度,这正是编程的主要要旨之一.异常是用来应付那些在其他编程方法都无法处理的情况.

注意:避免在构造函数和析构函数中使用异常.在构造函数中抛出异常则不会调用析构函数.而在析构函数中调用异常则可能会造成资源泄露

抛出异常的时候主要要在抽象层次一致的情况下抛出异常,例如:某读入用户数据的程序调用一接口,实现接口的代码需要打开磁盘上的文件,此时接口不适宜返回EOF文件异常,而应该将此异常映射到自定义异常,如无法读取用户数据之类的异常.这样子可以保证管理的协调性,同时,抛出异常需要给出异常出现的详细信息,例如,抛出一个数组上下界问题的异常,则需要在异常中说明数组的上界,下界,以及出现异常时试图访问数组中的哪个地方.用try…catch…语句时,若遇到无法处理的异常,至少要记录一下LogError()

其实在C语言里面也可以利用setjmp和longjmp两个函数做自己的异常处理机制,在工作中我遇到过这样的代码,日后有机会贴出思路.

隔离程序: 必要时,可以新建一系列接口,专门用于检查数据是否合法.接口一端的地方数据被视为是肮脏的.而另一端的数据被视为安全的可以随意调用的.

隔离程序在形式上更加像是办公楼里面的防火门:

//检验数据是否有效,若数据为非负则有效,否则返回-1
int verify_data(int data){
    if(data<0){
        return -1;
    }else { 
        return data;
    }
}

关于这几种技术的总结: 隔离层外部的地方(数据可能是肮脏的)应使用错误处理技术,而隔离层里面的地方应该使用断言,若断言检测出有错误的数据,则可以肯定是隔离层内部的子程序出了问题而不是外部程序出了问题.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章