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

什么是健壮性(Robustness)和正确性(Correctness)

健壮性

健壮性:系统在不 正常输入或不正常外部环境下仍能够表现正常的程度
在面向健壮性的编程要求:

  1. 处理未期望的行为和错误终止
  2. 即使终止执行,也 要准确/无歧义的向用户展示全面的错误信息
  3. 错误信息有助于进行debug

健壮性原则(Postel’s Law):

  • 总是假定用户恶意、假定自己的代码可能失败
  • 把用户想象成白痴,可能输入任何东西
  • 因此,开发者需要使程序能够返回给用户的错误提示信息要详细、准确、无歧义,这样更容易解决问题
  • 总的来说对别人宽容点,对自己狠一点,对自己代码保守,对用户行为开放

健壮性开发的原则:

  • 封闭实现细节,限定用户的恶意行为
    • 通过隐藏内部细节,使得用户无法因为修改内部细节而引发错误
  • 考虑极端情况,没有“不可能”

正确性

正确性:程序按照spec加以执行的能力,是最重要的质量指标!

关于正确性和健壮性的比较:

  • 正确性:永不给用户错误的结果
    • 永不返回不准确的结果;没有结果比不准确的结果更好
  • 健壮性:尽可能保持软件运行而不是总是退出
    • 总是尝试做一些事情,让软件继续运行,即使有时会导致不准确的结果
  • 总结:
    • 健壮性增加了对常见和非关键错误的内置容忍度,而正确性则在遇到不完美的输入时抛出错误
    • 正确性倾向于直接报错(error),健壮性则倾向于容错(fault-tolerance)

健壮性和正确性的作用和应用的比较

  • 健壮性:让用户变得更容易:出错也可以容忍,程序内部已有容错机制
  • 正确性:让开发者变得更容易:用户输入错误,直接结束。(不满足precondition的调用)

对外的接口,倾向于健壮;对内的实现,倾向于正确

  • 对外的接口尽可能适应用户输入的垃圾
  • 但是对内的模型要尽可能的简单,100%有效,通过invariant和assertion做出安全假设,在不正确的情况直接抛出异常
  • 怎么理解?
    • 如果对外接口的健壮性不够好,用户就不想用了
    • 而对内的正确性不好,导致需要考虑各种边界情况等,导致内部结构变得非常复杂,程序员会不满意

对于特定领域的应用:

  • 安全领域的应用倾向于更加满足正确性
  • 面向消费者的应用倾向于满足健壮性
    • 输出不准确的结果好过于关闭应用

Reliability (可靠性)的衡量:Robustness + Correctness
系统在规定条件下执行所需功能的能力,即在需要的时候有较长的平均故障间隔时间

怎么提高健壮性和正确性?

步骤
  1. 使用assertion,defensive programing, code review, formal validation来编程
  2. 观察出现的错误
  3. 识别潜在错误
  4. 修复

请添加图片描述

如何衡量正确性和健壮性

外部角度

Mean time between failures (MTBF):平均失效时间

这个时间的计算取决于怎么定义failures

对于复杂的、可修复的系统,故障被认为是那些超出设计条件,使系统停止服务并进入待修复状态的故障。
在本定义下,对于可以留在或保持在未修复的状态,并且不会使系统停止服务的故障,不被认为是故障

MTBF描述可修复系统两次故障之间的预期时间,注意是可修复系统

mean time to failure (MTTF) denotes the expected time to failure for a nonrepairable system.

请添加图片描述

内部

Residual defect rates 残余缺陷率,每千行代码中遗留的bug的数量

![[Pasted image 20230410150257.png]]

Error Exception

java.lang.Throwable

请添加图片描述

Error:程序员通常无能为力,一旦发生,想办法让程序优雅的结束

Exception:你自己程序导致的问题,可以捕获、可以处理

关于Error

分类
  • 用户输入错误
  • 设备错误
  • 物理限制

一些常见的Error

  • VirtualMachineError:JVM遇到内部错误或资源限制无法运行时
    • OutOfMemoryError
      • Java虚拟机由于内存不足而无法分配对象时抛出,并且垃圾回收器不能再提供内存
    • StackOverflowError
      • 递归太深而发生堆栈溢出时引发
    • InternalError
      • JVM中发生了一些意外的内部错误
  • LinkageError:一个类依赖于另一个类;但是,在编译前一个类之后,后一个类发生了不兼容的变化
    • NoClassDefFoundError
      • 如果JVM或ClassLoader实例试图装入类的定义,但找不到类的定义时抛出

处理Exception(Error无能为力)

什么是Exception

异常:程序执行中的非正常事件,程序无法再按预想的流程执行

将错误信息传递给上层 调用者,并报告“案发现场”的信息

当一个方法抛出了一个错误,该方法立刻退出,不返回任何值,不会在调用该方法的代码处恢复,相反,异常处理机制开始寻找可以处理这种特定错误条件的异常处理程序

理解:当一个错误发生时,会退出当前方法,把错误信息层层上报,直到寻找到可以处理这种特定错误的异常处理程序

Exception分类

Runtime Exception

运行时异常:由程序员在代码里处理不当造成,是程序源代码中引入的故障所造成的

  • A bad cast

  • An out-of-bounds array access

  • A null pointer access

  • 您可以通过根据数组边界测试数组索引来避免ArrayIndexOutOfBoundsException

  • 如果你在使用变量之前检查它是否为null,就不会发生NullPointerException

  • 如果在代码中提前进行验证,这些故障就可以避免

Other Exceptions

其他异常:由外部原因造成,是程序员无法完全控制的外在问题所导致的,比如说,文件不存在

  • Trying to read past the end of a file
  • Trying to open a file that doesn’t exist
  • Trying to find a Class object for a string that does not denote an existing class
    关于文件是否存在
    你不能先检查文件是否存在,然后再打开它吗?
    实际上,文件可能会在你检查它是否存在后立即被删除。
    因此,“存在”的概念取决于环境,而不仅仅是您的代码
    即使在代码中提前加以验证(文件是否存在),也无法完全避免失效发生

Checked and unchecked exceptions

这是从异常处理机制的角度所做的分类 异常被谁check?——编译器、程序员

Checked Exceptions
当异常出现时

  • 你要么捕获并处理异常,要么告诉编译器你不能通过声明你的方法抛出那个异常来处理它
  • 然后使用你的方法的代码将不得不处理该异常 (如果不能处理异常,可以选择声明抛出异常)。
  • 编译器会检查我们是否做了两件事中的一件(catch或declare)
  • 必须捕获并指定错误 处理器handler,否则 编译无法通过 类似于编程语言中的 static type checking

Unchecked Exceptions
Errors and Runtime Exceptions are not checked by compiler

  • 错误表示发生在应用程序之外的情况,例如系统崩溃。Runtime exceptions通常是由于应用程序逻辑中的错误而发生的
  • 在这种情况下,您什么也做不了,只能重新编写程序代码。编译器不会检查这些。
  • 这些运行时异常将在开发和测试期间发现。然后我们必须重构代码以消除这些错误。
  • 不需要在编译的时候用try…catch等机制处理
  • 可以不处理,编译没 问题,但执行时出现 就导致程序失败,代 表程序中的潜在bug 类似于编程语言中的 dynamic type checking

对于checked exception:
Five keywords are used in exception handling:

  • try
  • catch
  • finally
    • 以上三个都是捕获并处理XX异常
  • throws
    • 声明“本方法可能会发生XX异常”
    • 作用于方法上
  • throw
    • 抛出XX异常

Unchecked异常也可以使用throws声明或try/catch进 行捕获,但大多数时候是不需要的,也不应该这么做——掩耳盗铃, 对发现的编程错误充耳不闻

当要决定是采用checked exception还是unchecked exception的时候,问一个问题:“如果这种异常一旦抛出,client会做 怎样的补救?”

  • 如果客户端可以通过其他的方法恢复异常,那么采用checked exception;
  • 如果客户端对出现的这种异常无能为力,那么采用unchecked exception;
  • 异常出现的时候,要做一些试图恢复它的动作而不要仅仅的打印它的信息

尽量使用unchecked exception来处理编程错误:因为unchecked exception不用使客户端代码显式的处理它们,它们自己会在出现的地 方挂起程序并打印出异常信息。

  • 充分利用Java API中提供的丰富unchecked exception,如 NullPointerException , IllegalArgumentException和 IllegalStateException等,使用这些标准的异常类而不需亲自创建新的 异常类,使代码易于理解并避免过多消耗内存

如果client端对某种异常无能为力,可以把它转变 为一个unchecked exception,程序被挂起并返回 客户端异常信息

请添加图片描述

  • 不要创建没有意义的异常,client应该从checked exception中获取更有价值 的信息(案发现场具体是什么样子),利用异常返回的信息来明确操作失败的 原因。

  • 如果client仅仅想看到异常信息,可以简单抛出一个unchecked exception:

  • 错误可预 料,但无法预防,但可以有手段从中恢复,此时使用checked exception。

  • 如果做 不到这一点,则使用unchecked exception

  • 如果读文件的时候发现文件不存在了,可以让用户选择其他文件;但是如果 调用某方法时传入了错误的参数,则无论如何都无法在不中止执行的前提下 进行恢复。

抛出checked异常

“异常”也是方法和 client端之间spec的一部分,在post-condition中刻画
在Javadoc中@throws写明异常,并且在方法中throws
unchecked异常不用写出

你的方法应该throws什么异常?

  • 你所调用的其他函数抛出了一个checked exception——从其他函数传来的异常
  • 当前方法检测到错误并使用throws抛出了一个checked exception——你自己造出的异常
  • 此时需要告知你的client需要 处理这些异常
    • 如果没有handler来处理被抛出的checked exception,程序就终 止执行

对于Error和RuntimeException的子类,不需要抛出

LSP

  • 如果子类型中override了父类型中 的函数,那么子类型中方法抛出的异常不能比父类型抛出的异常类型 更宽泛
  • 子类型方法可以抛出更具体的异 常,也可以不抛出任何异常
  • 如果父类型的方法未抛出 异常,那么子类型的方法也不能抛出异常

怎么抛出异常

![[Pasted image 20230521204135.png]]

利用Exception的构造函数,将发生错误 的现场信息充分的传递给client。

请添加图片描述

总结:

  • 找到一个能表达错误的Exception类/ 或者构造一个新的Exception类
  • 构造Exception类的实例,将错误信息写入
  • 抛出它
  • 一旦抛出异常,方法不会再将控制权返 回给调用它的client,因此也无需考虑返回错误代码

创建异常类

前提:如果JDK提供的exception类无法充分描述你的程序发生的错误,可以 创建自己的异常类

只需从Exception或Exception的子类(如IOException)中继承即可

习惯上同时给出默认构造函数和包含详细消息的构造函数
Throwable超类的toString方法返回一个包含详细消息的字符串,这对调试很方便。
请添加图片描述

还可以抛出unchecked异常:需要继承java.lang.RuntimeException

异常类可以包含更多信息,在自定义的异常类里多加几个参数,修改构造器传入的参数数量

捕获异常

异常发生后,如果找不到处理器, 就终止执行程序,在控制台打印出stack trace。

有异常,则跳过try剩余部分,找到异常对于的catch,执行catch。如果出现的异常没有catch能对于,则方法立刻退出。

异常的处理:

  • 尽量在自己这里处理,实在不行就往上传——要承担责任!
  • 但有些 时候自己不知道如何处理,那么提醒上家,由client自己处理

注意:

  • 如果父类型中的方法没有抛出异常,那么子类型中的方法必须捕获所有的checked exception
  • 子类型方法中不能抛出比父类型 方法更多的异常!

Rethrowing and Chaining Exceptions

本来catch语句下面是 用来做exception handling的,但也可以在catch里抛出异常

这么做的目的是:更改exception的类型,更方便client端获取错误信 息并处理

但这么做的时候最好保留“根原因”

请添加图片描述

强烈推荐使用这种包装技术。它允许您在子系统中抛出高级异常,而不会丢失原始故障的细节

finally Clause

当异常抛出时,方法中正常执行的代码被终止

如果异常发生前曾申请过某 些资源,那么异常发生后这些资源要被恰当的清理

finally会在任何时候被执行,不管程序是否碰到异常,finally都会被执行

In the following example, the program will close the file under all circumstances:

InputStream in = new FileInputStream(. . .); 
try { 
	// 1 
	code that might throw exceptions 
	// 2 
} 
catch (IOException e) { 
	// 3 
	show error message 
	// 4 
} 
finally { 
	// 5 
	in.close(); 
} 
// 6

异常情况:

  • 捕获异常,处理异常
    • 1,3,4,5,6
  • 捕获异常,抛出新异常
    • 1,3,5
  • 抛出一个未被任何catch子句捕获的异常
    • 1,5,Go back to the client

Analyzing Stack Trace Elements

您可以通过调用Throwable类的printStackTrace方法来访问堆栈跟踪的文本描述

请添加图片描述

更灵活的方法是getStackTrace方法,它产生一个StackTraceElement对象数组,您可以在程序中对其进行分析。
请添加图片描述

StackTraceElement类具有获取执行代码行的文件名和行号以及类和方法名的方法

toString方法产生一个包含所有这些信息的格式化字符串。

Assertions

what

Fail fast避免扩散,检查前置条件是防御式编程的一种典型形式

断言:在开发阶段的代码中嵌入,检验某些“假设”是否 成立。若成立,表明程序运行正常,否则表明存在错误。

每个断言都包含一个布尔表达式,您认为该表达式在程序执行时为真

  • If it is not true, the JVM will throw an AssertionError.
  • 出现AssertionError,意味着内部某些假设被违反了
  • 增 强程序员对代码质量的信心:对代码所做的假设都保持正确

断言即是对代码中程序员所做假设的文档化,也不会影响运行时性能 (在实际使用时,assertion都会被disabled)

一个断言通常有两个参数

  • 描述假定为真的布尔表达式
    • assert condition
  • 如果不是,则显示一条消息
    • assert condition : message
    • 所构造的message在发生错误时显示给用户,便于快速发现错误所在

What to Assert and What not to?

  • Pre-condition: method argument requirements
  • Post-condition: Method return value requirements
  • Control-flow: covering all
    • 如果条件语句或开关不能涵盖所有可能的情况,那么最好使用断言来阻止非法情况。

但是不要在这里使用assert语句,因为它可以被关闭。相反,在非法的情况下抛出异常,这样检查就会一直发生

default: throw new AssertionError("must be a vowel, but was: " + vowel);

断言主要用于开发阶段,避免引入 和帮助发现bug

实际运行阶段, 不再使用断言,避免降低性能

使用断言的主要目的是 为了在开发阶段调试程序、尽快避免错误

  • 由于断言可能被禁用,所以程序的正确性永远不应该依赖于断言表达式是否被执行
  • 特别地,断言表达式不应该有副作用

程序之外的事,不受你控制,不要乱断言

  • 文件/网络/用户输入等
  • 外部错误要 使用Exception机制去处理

默认断言是关闭的

  • Enable assertions
    • -enableassertions or -ea
  • Disable assertions
    • disableassertions or -da

Guidelines for Using Assertions

断言→Correctness
如果针对异常情况触发了一个断言,那么纠正操作不仅仅是优雅地处理错误——纠正操作是更改程序的源代码、重新编译并发布软件的新版本
错误/异常处理→Robustness
如果错误处理代码用于处理异常情况,则错误处理将使程序能够优雅地响应错误

使用异常来处理你“预料到可以发生”的不正常情况
使用断言处理 “绝不应该发生”的情况

在其他一些开发者眼里,不应该针对参数的合法性使用断言

  • 不管是否-ea, spec中的pre-/post-conditions都能够被保证
  • 即使spec被违反,也不应通过assert直接fail,而是 应抛出具体的runtime异常

如果参数来 自于外部(不受自己控制),使用异常处理
如果来自于自己所写的其他代码,可以使用断言来帮助发现错误(例如postcondition就需要)

您可以使用断言来测试非公共方法的前提条件,您认为无论客户端对类做什么,该前提条件都将为真。

你可以在公共和非公共方法中使用断言测试后置条件。

开发阶段用断言尽可能消除bugs 在发行版本里用异常处理机制处理漏掉的错误

防御式编程

眼观六路,耳听八方,一旦 其他车辆有对你产生危险的 症状,马上采取防御式行动

Techniques for defensive programming

  • Protecting programs from invalid inputs
    • 对来自外部的数 据源要仔细检查,例如:文件、网络数据、用户输入等
    • 对每个函数的输入 参数合法性要做仔细检查,并决定如何处理非法输入
  • Assertions
  • Exceptions
  • Specific error handling techniques
  • Barricade
    • 类的public方法 接收到的外部数据都应被认为是dirty的,需要处理干净再传递到 private方法——隔离舱
      • 一旦数据被类的公共方法接受,类的私有方法就可以假定数据是安全的。
    • operating-room technique
      • 数据在进入手术室之前是经过消毒的。手术室里的任何东西都被认为是安全的。
      • 关键的设计决策是决定把什么放在手术室里,把什么放在外面,把门放在哪里-哪些程序被认为是在安全区内,哪些在外面,哪些是消毒数据
      • 要做到这一点,最简单的方法通常是在外部数据到达时对其进行消毒,但是数据通常需要在多个级别上进行消毒,因此有时需要多个级别的消毒
    • 障碍物的使用使断言和错误处理之间的区别变得清晰。
      • “隔离舱”外部的函数应使用异常处理,“隔离舱”内的函数应使用 断言。
  • Debugging aids
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值