第十二章 面向正确性与健壮性的软件构造
第十二章 面向正确性与健壮性的软件构造
健壮性和正确性
健壮性
-
系统在不正常输入或不正常外部环境下仍能够表现正常的程度
-
面向健壮性的编程
- 处理未期望的行为和错误终止
- 即使终止执行,也要准确/无歧义的向用户展示全面的错误信息
- 错误信息有助于进行debug
-
对自己的代码要保守,对用户的行为要开放
-
原则
- 封闭实现细节 , 限定用户的恶意行为
- 考虑极端情况 , 没有“不可能”
正确性
- 程序按照 spec 加以执行的能力,是最重要的质量指标!
比较
- 正确性:永不给用户错误的结果
- 健壮性:尽可能保持软件运行而不是总是退出
- 正确性倾向于直接报错(error),健壮性则倾向于容错(fault-tolerance)
- 健壮性让用户变得更容易:出错也可
以容忍,程序内部已有容错机制 - 正确性:让开发者变得更容易:用户输入错误,直接结束。(不满足precondition的调用)
对外的接口,倾向于健壮;对内的实现,倾向于正确
- 在内外部之间做好隔离,防止“错误”扩散
可靠性=健壮性+正确性
评测健壮性和正确性
外部观察角度
-
Mean time between failures (MTBF ,平均失效间隔时间 )
- 平均故障间隔时间 (MTBF) 描述了可修复系统两次故障之间的预期时间,而平均故障时间 (mean time to failure MTTF) 表示不可修复系统的预期故障时间。
内部观察视角(间接)
- 残余缺陷率
Java中的错误和异常
所有的异常对象都继承java.lang.Throwable
Error
-
内部错误:程序员通常无能为力,一旦发生,想办法让程序优雅的结束
-
种类
-
用户输入错误
-
设备错误
- 网页不可用
- 打印机关闭
-
物理限制
- 磁盘不足
- 内存不足
-
-
通常不需要实例化,也可实例化,捕获
Exception
-
异常:你自己程序导致的问题,可以捕获、可以处理
-
return之外的第二种退出途径
-
两个分支
-
运行时异常
- 由程序员在代码里处理不当造成
- 类型转换,空指针,数组越界
-
其他异常
- 外部原因造成,程序员无法完全控制的外在问题所导致的,即使在代码中提前加以验证(文件是否存在),也无法完全避免失效发生
- 尝试越过文件末尾读取;尝试打开不存在的文件;尝试为不表示现有类的字符串查找 Class 对象
-
-
checked和unchecked
-
checked
-
必须捕获或者声明的异常
-
编译器会检查
-
从Exception派生出子类型
-
Java处理异常的三个操作
- Declaring exceptions (throws) 声明“本方法可能会发生XX异常”
- Throwing an exception (throw) 抛出XX异常
- Catching an exception (try, catch, finally) 捕获并处理XX异常
-
应该出现在规约的postcondition中,用@throws标记
-
-
unchecked
-
不需要捕获和声明
-
编译器不会检查错误和运行时异常
-
类似动态类型检测
-
举例
- 数组越界
- 空指针
- NumberFormatException :Integer.parseInt(“abc”)
- ClassCastException :类型强转错误
-
Unchecked 异常也可以使用 throws 声明或 try/catch进行捕获,但大多数时候是不需要的,也不应该这么做
-
不该出现在规约的postcondition中
-
即使通过throws声明,在调用者中也不会检查是否捕获或者声明这个异常
-
-
如何选择
-
如果客户端可以通过其他的方法恢复异常,那么采用checked exception;
- client应该从checked exception中获取更有价值的信息(案发现场具体是什么样子),利用异常返回的信息来明确操作失败的原因
- 如果client仅仅想看到异常信息,可以简单抛出一个unchecked exception:
-
如果客户端对出现的这种异常无能为力,那么采用unchecked exception;
- 尽量使用unchecked exception 来处理编程错误
- 充分利用Java API中提供的丰富unchecked exception
- 如果client端对某种异常无能为力,可以把它转变为一个 unchecked exception,程序被挂起并返回客户端异常信息
- 要想让代码更加易读,倾向于用unchecked exception来处理程序中的错误
-
-
比较
-
抛出异常的方法
- 找到一个能表达错误的Exception类/或者构造一个新的Exception类
- 构造Exception类的实例,将错误信息写入
- 抛出它
- 一旦抛出异常,方法不会再将控制权返回给调用它的client,因此也无需考虑返回错误代码
创建异常类
-
checked
- 继承自Exception及其子类
-
unchecked
- 继承自RuntimeException
处理异常
-
try catch
- 当catch中没有的时候,会抛向上一级
-
本来catch语句下面是用来做exception handling的,但也可以在catch里抛出异常
- 这么做 的 目 的是:更 改 exception 的 类型 ,更 方便 client 端获 取 错误信息并处理
但这么做的时候最好保留“根原因”
当异常被捕获时,可以获取到原始异常,它允许在子系统中抛出高级异常而不会丢失原始故障的详细信息
-
finally 语句
-
当代码抛出异常时,它会停止处理方法中剩余的代码并退出该方法,如果该方法获取了一些只有该方法知道的资源(文件、数据库连接等),并且必须清除该资源,否则会出现问题。
-
两种方法
-
一种解决方案是捕获并重新抛出所有异常。
- 但是这个方案很繁琐,因为你需要在两个地方清理资源分配——在正常代码和异常代码中。
-
Java 有一个更好的解决方案:finally 语句。
- 无论是否异常都会被执行
-
-
-
可以使用没有catch的语句
-
栈轨迹
- 打印的栈轨迹是从栈顶开始的
- 和栈帧类似
获取栈轨迹- 分析栈轨迹
断言
防止bug
-
第一道防线:让bug成为不可能
- 静态检测
- 动态检测
- immutable类型
- final修饰
-
第二道防线:定位bug
-
尽快失败,就容易发现、越早修复
- 断言
-
断言
-
检查前置条件是防御式编程的一种典型形式
-
断言 即 是对代码中程序员 所做 假设的 文档化 ,也不 会影响运 行时性能( 在实 际 使用时, assertion 都会被 disabled)
-
两种形式
- assert condition;
- assert condition : message;(message是字符串)
-
可以用来检测
-
Internal Invariants 内部不变量
-
Rep Invariants 表示不变量
-
Control-Flow Invariants 控制流不变量
- 在不会到达的地方assert false
-
Pre-conditions of methods 方法的前置条件
-
Post-conditions of methods方法的后置条件
-
-
断言主要用于开发阶段,避免引入和帮助发现 bug,实际运行阶段,不再使用断言,避免降低性能
-
程序的正确性不应取决于断言表达式是否被执行,因为断言可能会被禁用 -
程序之外的事,不受你控制,不要乱断言
- 文件/网络/用户输入等
- 断言只是检查程序的内部状态是否符合规约
- 外部使用异常
-
断言非常影响运行时的性能,Java缺省关闭断言
断言和异常
-
断言保证正确性
- 断言在大型复杂程序和高可靠性程序中特别有用。它们使程序员能够更快地清除不匹配的接口假设、修改代码时出现的错误等。
- 使用断言处理“ 绝不应该发生”的情况
-
异常保证健壮性
- 使用异常来处理你“预料到可以发生“的不正常情况
pre-/post-condition
- 如果参数来自于外部(不受自己控制),使用异常处理(public函数)
- 如果来自于自己所写的其他代码,可以使用断言来帮助发现错误( 例如 post-condition 就需要(private函数)
防御性编程
保护程序免受无效输入
- 对来自外部的数据源要仔细检查,例如:文件、网络数据、用户输入等
- 对每 个函 数的输入参 数 合 法性要 做仔 细 检查 ,并 决 定 如 何处理非法输入
Barricade 设置路障
- 类 的 public 方 法接 收到 的外部数 据都应被认 为是 dirty 的, 需 要处理 干净 再传递 到private 方 法 —— 隔离舱
- “隔 离舱 ”外部的 函 数 应 使用异常处理,“隔 离舱 ”内的 函 数 应 使用断言。
The SpotBugs tool
- 静态分析查找Java代码中的bugs