面向正确性与健壮性的软件构造

作者:syt

什么是健壮性和正确性

健壮性

系统在不正常输入或不正常外部环境下仍能够表现正常的程度。
面向健壮性的编程:
处理未期望的行为和错误终止。
即使终止执行,也要准确/无歧义地向用户展示全面的错误信息。
错误信息有助于进行debug。
避免给用户太大压力,帮助用户承担一些麻烦。

健壮性原则

偏执——总是假定用户恶意、假定自己代码可能失败。
把用户想象成白痴,可能输入任何东西——返回给用户的错误提示信息要详细、准确、无歧义。
对别人宽容点,对自己狠点。
对自己代码要保守,对用户行为要开放。
用户不应获得访问库、数据结构或指向数据结构的指针的权限——封闭实现细节,限定用户恶意行为。
考虑极端情况,没有“不可能”。

正确性

程序按照spec加以执行的能力,是最重要的质量指标。
正确性——永不给用户错误的结果。
健壮性——尽可能保持软件运行而不是总是退出。
正确性倾向于直接报错,健壮性倾向于容错。

健壮性与正确性对比

健壮性——让用户变得更容易,出错也可以容忍,程序内部已有容错机制。
正确性——让开发者更容易,用户输入错误,直接结束。
对外的接口倾向于健壮。
对内的实现倾向于正确。
安全关键型应用更关注正确性。
消费级应用更关注健壮性。
可靠性——系统在规定条件下、在需要时执行其所需功能的能力——即具有较长的平均无故障时间。
可靠性=健壮性+正确性。

提高健壮性与正确性的步骤

0.使用断言、防御式编程、代码评审、形式化验证等方法,以实现健壮性和正确性为目标编写代码。
1.观察故障现象(内存转储、堆栈跟踪、执行日志、测试)。
2.识别潜在故障(缺陷定位、调试)。
3.修复错误(代码修订)。

如何测试健壮性和正确性

MTBF外部观察角度

MTBF——平均失效间隔时间。
系统在运行过程中,其内在失效之间预计经过的时间,MTBF 计算为系统两次失效之间时间的算术平均值。
MTBF 的定义取决于如何定义系统失效。
平均故障间隔时间MTBF——可修复系统两次故障之间的预期时间。
平均故障前时间MTTF不可修复系统失效前的预期时间。

内部观察角度(间接)

残余缺陷率——每千行代码中遗留的bug的数量。
1-10标准、0.1-1高质量、0.01-0.1最高级别。

Python中的错误与异常

Python中的异常

Python中所有异常的基类是BaseExceptions;实际开发中用Exception体系。
Error内部错误——程序员通常无能为力,一旦发生,想办法让程序优雅结束。
Exception异常——自己程序导致的问题,可以捕获,可以处理。

错误类型

用户输入错误。
设备错误。
物理限制。

解决异常

什么是异常

程序执行中的非正常事件,程序无法再按预想的流程执行。
将错误信息传递给上层调用者,并报告“案发现场”的信息。
除return之外的第二种退出途径:
方法会抛出一个封装了错误信息的对象,该方法立刻退出,不返回任何值。
此外,执行流程不会回到调用该方法的那行代码继续执行。
异常处理机制开始搜索能够处理该特定错误条件的异常处理程序。若找不到异常处理程序,整个系统完全退出。

异常的好处

异常机制将“错误处理”与“正常流程”分离,代码更简洁。
错误发生时自跳转到except块,减少重复判断。
更容易维护和扩展。
异常处理让我们专注于“正常流程”,将错误处理与正常逻辑分离。
代码更健壮、更易读、更符合Python的设计哲学。

异常分类

通过 raise 声明受检异常

在Python中,如果函数可能遇到它无法处理的错误,可以选择“抛出”或“向上冒泡”异常。
“抛出”异常——使用raise语句。
“不处理异常”——让异常自然向上传播。

在规约里声明异常:
在 Python 中,如果函数在某些条件下可能无法完成其功能,可以在文档字符串(docstring)中使用“Raises”部分说明可能抛出的异常及其触发条件。
Python 不要求在函数签名中声明异常(没有类似 Java 的 throws),但好的文档说明有助于用户正确使用函数,并做好异常处理。

考虑子类型多态:
Python不像Java那样在语言层限制子类可以抛出的异常类型,但良好的设计需要遵循子类型替换原则(LSP),可以避免让调用者遭遇意外异常。
子类型替换原则/里氏替换原则(LSP):
LSP 是子类型关系的一种特定定义,称为(强)行为子类型化强行为子类型化。
在编程语言中,LSP 依赖于以下约束:
子类型中不能强化前置条件。
子类型中不能弱化后置条件。
超类型的不变量必须在子类型中得以保持。
子类型中方法参数的逆变 子类型方法参数: 逆变。
子类型中方法返回值的协变 子类型方法的返回值: 协变。
子类型的方法不应抛出新的异常,除非这些异常本身是超类型方法所抛出异常的子类型。

创建异常类

程序遇到标准异常无法准确描述的错误时,可以创建自定义异常类。
自定义异常类只需继承Exception或其子类。

断言

第一防御——使bug不可能

最好的防御就是不要引入bug。

第二防御——局部化bug

如果无法避免,尝试将bug限制在最小范围内。
限定在一个方法内部,不扩散。
断言——当前置条件不满足时,该代码会通过抛出 `AssertionError` 异常来终止程序。调用者的错误所产生的连锁影响将被阻止传播。Fail fast,避免扩散。
检查前置条件是防御式编程的一种典型形式——防御式编程提供了一种方式,即使你不知道缺陷在哪里,也能减轻其造成的影响。

什么是断言

断言——一种开发阶段使用的代码,用于检查程序运行时是否满足某些“假设”。
其用于验证程序状态,帮助尽早发现逻辑错误。

为什么断言

← 返回博客目录