RJSJ 15: 面向健壮性的设计

正确性和健壮性

正确性

  • 行为要严格符合贵越重定义的行为
  • 永远不给错误的结果
  • 让开发者更容易;输入错误,直接结束
  • 对内的实现,倾向于正确

健壮性

指的是在异常情况下,软件能够正常运行的能力

  • 没有被规约覆盖的情况是“异常情况”,出现规约定义外的情形时,软件要做出恰当的反应
  • 尽可能让软件运行而不是总是推出
  • 让用户变得更容易;出错也可以容忍
  • 对外的接口倾向于健壮

e.g.

问题 正确性做法 健壮性做法
视频文件有坏帧 停止播放,提醒损坏 跳过坏帧,从下一正确的继续播放
用户输入奇怪的格式 提示输入错误 尝试用不同日期格式解析,告诉用户解析结果
代码的括号不匹配 编译错误 尝试补充不匹配的括号继续编译

健壮性原则(Postel`s Law)

  • 对自己的代码要保守,对用户的行为要开放
    • 严于律己,宽以待人
  • 总是假定用户恶意,家底自己代码可能失败
  • 吧用户想象成可能输入任何东西的傻瓜

软件中的问题

  • Fault,故障

    • bugs
    • 代码实现的错误
  • Error 错误

    • 不正确的内部运行状态
  • Failure

    • 运行时候表现出来的、外在的、和规约不一致的行为
  • Problem

    • 笼统/泛指各种不正确的情况
  • Mistake

    • 侧重描述人的行为存在失误
  • Defect

    • 泛指各类设计和实现中存在的问题,导致bug的根源
  • Bug

    • 同fault
  • Exception

    • 一种应对故障、处理错误的编程机制
  • Anomaly

    • 常用于算法领域描述与正常分布不一致的情况

Fault => Error => Failure

导致Failure的必要条件:

  • 可达性
    • 执行到包含Fault的代码
  • 感染性
    • 执行Fault的代码后,程序状态是Error
    • (错误状态和运行环境有关,在特定环境下才出错)
  • 传播性
    • Error能传递到程序的输出,被外界感知

Mistake 和 Defect

mistake指的是程序员的错误行为

defect是软件的内在性质,可能导致执行结果和预期(用户希望的结果)不一致

  • 程序的果实可能导致软件出现defect
  • 并非所有defect都是由于mistake导致的

提升正确性和健壮性的方法

  • 故障拒绝
    • 防御性变成、代码审查、形式化验证
  • 故障检测
    • 测试、调试、测试驱动开发
  • 容错
    • 在运行时通过一定手段消除影响
    • 冗余、备份、重试

防御性编程

  • 子程序不因传入错误数据而被破坏,哪怕是由其它子程序产生的错误数据

  • 其核心是承认程序都会有问题,都需要被修改,这是保护的基础

常用技术:

  1. 输入检查
  2. 断言
  3. 错误处理
  4. 异常处理
  5. 隔栏

输入检查

检查:

  • 文件
  • 用户输入
  • 网络
  • 其他外部接口
  1. 参数类型是否一致
  2. 参数值是否合法
  3. 长度要求

Assert

用来检查永远不应该发生的状况

断言只在开发阶段被编译到目标代码中,而在生成产品代码时不编译到产品代码中

C++中的Assert

  • 在头文件中定义的assert宏
  • false时候会停止运行
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <iostream>
#include <cassert>
using namespace std;
void func1(int n) {
	assert(n==1);
	cout << "func1 finish" <<endl;
}

int main() {
	func1(1);
	cout << "first" <<endl;
	func1(2);
	cout << "second"<<endl;
	return 0;
}
  • func1 finish

  • first

  • Assertion failed: n==1, file example1.cpp, line 6

技巧

  • 可用断言在函数开始处检查传入参数的合法性
  • 每个assert只检验一个条件
  • 不要在断言中使用改变环境的语句,因为assert仅在debug阶段生效,如果这么做,会使程序在真正运行时出错

错误处理

理想情况:希望在发生错误情况时,不只是简单地终止程序运行,而是能够反馈错误情况的信息,并且能够对程序运行中已发生的事情做些处理。

异常

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

  • 当一个函数出现自己无法处理的错误时,可以抛出异常,然后由该函数的直接或者间接调用者处理这个错误

  • 异常不是bug

    • “异常” 是在程序开发中必须考虑的一些特殊情况,是程序运行时就可预料的执行分支(注:异常是不可避免的,如程序运行时产生除 0 的情况、打开的外部文件不存在、数组访问的越界等)
    • “Bug”是程序的缺陷,是程序运行时不被预期的运行方式(注:Bug是人为的、可避免的;如使用野指针、动态分配内存使用结束后未释放等)

![截屏2025-05-18 10.46.46](截屏2025-05-18 10.46.46.png)

  • 异常的抛出规则

    • 异常时通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码

    • 被选中的处理代码的调用链是,找到与该类型匹配且离抛出异常位置最近的那一个catch

    • 抛出异常对象后会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会调用复制构造函数生成一个拷贝对象

  • 异常的匹配规则

    • 首先检查throw语句本身是否在try块内部,如果是,再在当前函数栈中查找匹配的catch语句。如果有匹配的直接跳到catch的地方执行
    • 如果没有匹配的catch块或者throw语句不在try块内部,则退出当前函数栈,在调用函数的栈中查找匹配的catch
    • 如果到达main函数,都没有匹配的catch,就会终止程序
    • 找到匹配的catch会直接跳到catch语句执行,执行完后,会继续沿着catch语句后面执行

![截屏2025-05-18 10.49.46](截屏2025-05-18 10.49.46.png)

  1. bad_alloc: new的时候内存不足
  2. out_of_range
  3. ……

用智能指针,放着catch后内存没有释放

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
template<class T>
class SmatrPrt{
private:
	T* _ptr;
public:
	//将外面申请的资源,托管给类的成员
	SmatrPrt(T* ptr = nullptr):
	_ptr(ptr) {}
	T& operator*(){
		return *_ptr;
	}
	T* operator->(){
		return _ptr;
	}
	//在对象析构时,自动释放资源
	~SmatrPrt(){
		if (_ptr){
			delete _ptr;
		}
	}
};

隔栏

如果所有的代码都做异常和错误处理,会使代码变得臃肿,可读性下降

隔栏(barricade)是在设计上简化错误处理的策略,将程序的外部和内部进行隔离

​ 把某些接口选定为安全区域的边界,对穿越安全区域边界的数据进行合法性校验,并当数据非法时进行处理

  • 隔栏的使用使断言和错误处理有了清晰的区分
    • 隔栏外部的程序使用错误处理技术,外部的数据是不安全的
    • 隔栏内部的程序就应该使用断言技术,因为传入程序内部的数据都已经过了隔栏的处理,应该是正确的,如果出错,则说明程序本身出错

防御式编程的使用考虑

  1. 防御式编程的矛盾
    1. 在产品开发阶段,希望显示出的错误越多越好,引入很多防御性的代码
    2. 在产品发布阶段,希望错误尽可能偃旗息鼓,尽量不要在使用中出现
    3. 如何权衡
  2. 过度的防御式编程也会引入问题
    1. 如果在程序的每一个想到的地方都进行参数检查、错误保护等,那么程序将变得臃肿而缓慢
    2. 更糟糕的是,防御式编程引入的额外代码增加了软件的复杂度,反而容易造成错误
Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy
发表了41篇文章 · 总计29.72k字