软件构造课笔记:第12章——面向正确性与健壮性的软件构造

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

什么是稳健和正确性?

健壮性:系统在不 正常输入或不正常外部环境下仍能够表现正常的程度
处理未期望的行为和错误终止
要准确 / 即使终止执行,也 无歧义的向用户展示全面的错误信息
错误信息有助于进行 debug 

稳健性原则(Postel定律)

1.偏执狂:总是假定用户恶意、假定自己的代码可能失败
2.愚蠢:把用户想象成白痴,可能输入任何东西
3.对别人宽容点,对自己狠一点。对自己的代码要保守,对用户的行为要开放

原则:封闭实现细节,限定用户的恶意行为。考虑极端情况,没有不可能

正确性:程序按照规格加以执行的能力,是最重要 的质量指标!
稳健与正确:在天平的两端。
正确性:永不给用户错误的结果
健壮性:尽可能保持软件运行而不是总是退出
正确性倾向于直接报错错误健壮性则倾向于容错(容错)

健壮性:避免给用户太大压力, 帮助用户承担一些麻烦
健壮性: 让用户变得更容易:出错也可 以容忍,程序内部已有容错机 制
正确性: 让开发者变得更容易:用户输 入错误,直接结束。 (不满足 precondition 的调用)

鲁棒性与正确性的比较
安全关键型应用程序往往倾向于正确性而非稳健性。
-不返回结果总比返回错误的结果好。
消费者应用程序倾向于支持健壮性和正确性。
-任何结果通常都比关闭软件要好。
可靠性。系统在规定的条件下执行其所需功能的能力,无论何时需要,平均无故障时间很长。
可靠性=稳健性+正确性

提高稳健性和正确性的步骤
步骤0:使用断言、防御性编程、代码审查、形式验证等,以健壮性和正确性为目标对代码进行编程
步骤1:观察故障症状(内存转储、堆栈跟踪、执行日志、测试)
步骤2:识别潜在故障(错误定位、调试)
步骤3:修复错误(代码修订)

如何衡量稳健性和正确性?

外部:平均无故障时间(MTBF,平均失效间隔时间) 是运行期间系统固有故障之间的预测经过时间。
内部:根据KLOC,剩余缺陷率是指“软件发货后遗留的缺陷”:每千行代码中遗留的bug

Java中的错误和异常

内部错误:程序员通常无能为力,一旦发生,想办法让程序优雅的结束
异常:你自己程序导致的问题,可以捕获、可以处理

错误类型:用户输入错误,设备错误,物理限制

异常处理

什么是异常?

异常是指在程序执行过程中发生的异常事件,会中断程序的正常运行
序执行中的非正常事件,程序无法再按预想的流程执行
异常是代码将错误或异常事件传递给调用它的代码的一种特定方式。将错误信息传递给上层 调用者,并报告“案发现场”的信息如果无法以正常方式完成任务,Java允许每个方法都有一个替代的退出路径。回来之外的第二种退出途径
-该方法抛出一个封装错误信息的对象。
-该方法立即退出,并且不返回任何值。
此外,在调用该方法的代码处不会恢复执行;
相反,异常处理机制开始搜索可以处理这种特定错误条件的异常处理程序
若找不到异常处理程序,整个系统完全退出

异常情况的分类

运行时异常:由程序员在代码里处理不当造成,如果在代码中提前进行验证,这些故障就可以避免
其他异常:由外部原因造成,非运行时异常,是程序员无法完全控制的外在问题所导致的,即使在代码中提前加以验证(文件是否存在),也无法完全避免失效发生。

已检查和未检查的异常

如何处理异常?

发生异常时检查违例
您必须捕获并处理异常,或者通过声明您的方法抛出该异常来告诉编译器您无法处理它,
然后,使用您的方法的代码将不得不处理该异常(如果无法处理,则可以选择声明它抛出异常)。
-编译器将检查我们是否完成了两件事中的一件(捕获或声明)。编译器可帮助检查你的程序是否已抛出或处理了可能的异常
编译器不检查错误和运行时异常。错误表示应用程序外部发生的情况,例如系统崩溃。运行时异常通常是由应用程序逻辑中的错误引起的。在这种情况下,您不能做任何事情,但必须重新编写程序代码。所以编译器不会检查这些。这些运行时异常将在开发和测试期间发现。然后我们必须重构我们的代码以消除这些错误。

未检查的异常

程序编译不需要任何操作,但未捕获的异常将导致程序失败,不需要在编译的时候用try…catch等机制处理,可以不处理,编译没 问题,但执行时出现 就导致程序失败,代 表程序中的潜在bug

已检查的异常

必须捕获并指定错误 处理器handler,否则编译无法通过

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

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

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

不要创建没有意义的异常,client应该从 的信息( checked exception中获取更有价值 案发现场具体是什么样子),利用异常返回的信息来明确操作失败的 原因。– 如果client仅仅想看到异常信息,可以简单抛出一个 unchecked exception。Checked exception 应该让客户端从中得到丰富的信息。 要想让代码更加易读,倾向于用 unchecked exception 来处理程序中的错误

错误可预料,但无法预防,但可以有手段从中恢复,此时使用 错误可预 checked
如果做不到这一点,则使用 unchecked exception
错误可预料不可预防

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

异常设计注意事项
对特殊结果(即预期情况)使用已检查的例外情况
使用未检查的异常来发出错误信号(意外故障)

用throw声明已检查错误

通过throws声明已检查异常,如果Java方法遇到无法处理的情况,它可以抛出异常。
一个方法不仅会告诉Java编译器它可以返回什么值,还会告诉编译器什么可能出错。“异常”也是方法和客户的条件

程序员必 须在方法的spec中明确写清本方法会抛出的所有checked exception, 以便于调用该方法的client加以处理

如果一个方法可能抛出一个以上的已检查异常类型,必须在标头中列出所有异常类。

不要抛出错误和未检查的异常
不需要公布从Error继承的内部Java错误异常。
任何代码都可能引发这些异常,而这些异常完全超出了您的控制范围。
不应公布从RuntimeException继承的未检查异常。

考虑子类型多态性

如果子类型中 override 了父类型中 的函数,那么子类型中方法抛出的异常不能比父类型抛出的异常类型 更宽泛。子类型方法可以抛出更具体的异常,也可以不抛出任何异常。如果父类型的方法未抛出 异常,那么子类型的方法也不能抛出异常。
参见 LSP 原则:目标是子类型多态:客户端可用统一的方式处理不 同类型的对象,子类型可替代父类型。

利斯科夫替代原则
LSP是一种亚型关系的特殊定义,称为(强)行为亚型强行为子类型化
在编程语言中,LSP依赖于以下限制:
1.不能在子类型中加强前提条件。
2.不能在子类型中削弱后置条件。
3.超类型的不变量必须保留在子类型中。
4.子类型中方法参数的相反子类型方法参数:逆变
5.子类型中返回类型的协方差。子类型方法的返回值:协变
6.子类型的方法不应引发任何新的异常,除非这些异常本身是超类型的方法引发的异常的子类型。

如何抛出异常

利用 Exception的构造函数,将发生错误的现场信息充分的传递给 client 。
找到一个能表达错误的Exception类/ 或者构造一个新的Exception类
构造Exception类的实例,将错误信息写入然后抛出它
 一旦抛出异常,方法不会再将控制权返 回给调用它的client,因此也无需考虑返回错误代码

创建异常类

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

只需从Exception派生它,或者从Exception的子类(如IOException)派生它。
通常同时提供默认构造函数和包含详细消息的构造函数。
Throwable超类的toString方法返回一个包含该详细消息的字符串,这对于调试很方便。

异常信息包含更多“案发现场 信息”的异常类定义和辅助函数
抛出异常的时候,将 现场信息记入异常
在异常处理时,利用这些信息给用户更有 价值的帮助

捕获异常

异常发生后,如果找不到处理器, 就终止执行程序,在控制台打印出 stack trace 。
如果try块内的任何代码引发catch子句中指定的类的异常,那么
-程序将跳过try块中的剩余代码。
-该程序执行catch子句中的处理程序代码。
-如果try块中没有任何代码抛出异常,那么程序将跳过catch子句。
-如果一个方法中的任何代码抛出的异常类型不是catch子句中命名的异常类型,则该方法将立即退出。
-希望它的一个调用者已经为该类型提供了catch子句。

处理异常的另一个选择是:什么都不做只需将异常传递给调用者。
尽量在自己这里处理,实在不行就往上传 —— 要承担责任!
但有时候自己不知道如何处理,那么提醒上家,由 client 自己处理

注意:

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

重新抛出和连接异常

本来catch语句下面是 用来做exception handling的,但也可以在catch里抛出异常
这么做的目的是:更改 exception 的类型,更方便 client 端获取错误信 息并处理

final子句

当您的代码抛出异常时,它会停止处理方法中的剩余代码并退出该方法时,方法中正常执行的代码被终止,如果该方法已经获取了一些资源(文件、数据库连接等),而只有该方法知道这些资源,并且必须清理这些资源,则会出现问题。如果异常发生前曾申请过某些资源,那么异常发生后这些资源要被恰当的清理
一种解决方案是捕获并重新抛出所有异常。但是这个解决方案是乏味的,因为您需要在两个地方清理资源分配——在正常代码和异常代码中。
Java有一个更好的解决方案:finally子句。

分析堆栈跟踪元素

异常调用堆栈
当Java方法内部发生异常时,该方法会创建一个exception对象,并将exception对象传递给JVM(即方法“抛出”一个exception)。Exception对象包含异常的类型以及异常发生时程序的状态。
JVM负责找到一个异常处理程序来处理exception对象。它在调用堆栈中向后搜索,直到为exception对象的特定类找到匹配的异常处理程序(在Java术语中,它被称为“捕获”exception)。
如果JVM在调用堆栈中的所有方法中都找不到匹配的异常处理程序,它将终止程序。

分析堆栈跟踪元素
堆栈跟踪是程序执行过程中特定点上所有挂起的方法调用的列表。
您几乎可以肯定地看到,每当Java程序以未捕获的异常终止时,都会显示堆栈跟踪列表。
您可以通过调用Throwable类的printStackTrace方法来访问堆栈跟踪的文本描述。

一种更灵活的方法是 getStackTrace 方法,该方法可生成
一个 StackTraceElement 对象数组,您可以在程序中对其进行分析。

分析框架
▪ StackTraceElement 类具有获取文件名和行号以及执行代码行的类和方法名称的方法。
▪ toString 方法可生成包含所有这些信息的格式化字符串。

断言

第一道防线:让Bug成为不可能
对bug最好的防御是通过设计使它们不可能出现。最好的防御就是不要引入程序错误
-静态检查:通过在编译时捕获错误来消除许多错误。
-动态检查:Java通过动态捕获数组溢出错误,使它们不可能出现。如果您试图在数组或List的边界之外使用索引,那么Java会自动生成一个错误。——未检查的异常/运行时错误
-不可变性:不可变类型是指其值一旦创建就永远不会更改的类型。
-不可变值:按final,可以分配一次,但永远不会重新分配。
-不可变引用:通过final,这使得引用不可分配,但引用指向的对象可能是可变的或不可变的。

第二道防线:本地化Bug
如果我们不能防止错误,我们可以尝试将它们本地化到程序的一小部分,这样我们就不必太费力地寻找错误的原因。如果无法避免,尝试着将程序错误限制在最小的范围内
-当本地化到单个方法或小模块时,研究程序文本就可以发现错误。限定在一个方法内部,不扩散
-故障速度快:问题观察得越早(越接近其原因),就越容易解决。尽快失败,就容易发现、越早修复

断言:当不满足前提条件时,此代码通过抛出AssertionError异常来终止程序。阻止了调用者错误的影响传播。故障很快
检查前提条件是防御性编程的一个例子
检查前置条件是防御式编程的一种典型形式
-真正的程序很少没有bug。
-防御性编程提供了一种减轻bug影响的方法,即使你不知道它们在哪里。

断言什么和为什么断言

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

每个断言都包含一个布尔表达式,您认为在程序执行时该表达式将为true。如果不是这样,JVM将抛出一个AssertionError。
此错误表示您有一个需要修复的无效假设。断言错误意味着内部某些假设被违反了
该断言确认了您对程序行为的假设,增强了您对该程序没有错误的信心
强程序员对代码质量的信心:对代码所做的假设都保持正确

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

断言通常包含两个自变量,描述假设为真的布尔表达式,如果不是,则显示一条消息。
Java语言有一个关键字assert,它有两种形式:
-断言条件;
-断言条件:消息;
-如果布尔表达式的计算结果为false,这两个语句都会计算条件并抛出AssertionError。
-在第二条语句中,表达式被传递给AssertionError对象的构造函数,并转换为消息字符串。当断言失败时,描述会打印在错误消息中,因此可以用来向程序员提供有关失败原因的其他详细信息。
所构造的消息在发生错误时显示给用户,便于快速发现错误所在

断言什么和不断言什么?

1.内部不变量
2.表示不变量
3.控制流不变量
4.方法的前置条件
5.方法的后置条件
▪ 先决条件:方法论证要求
▪ 后置条件:方法返回值要求

控制流:覆盖全部:如果条件语句或开关未覆盖所有可能的情况则最好使用断言来阻止非法情况。

断言内容:更多场景
1.仅输入变量的值不会被方法更改
2.指针不是NULL
3.传递到方法中的数组或其他容器可以包含至少X个数据元素
4.表已初始化为包含实值
5.当方法开始执行(或完成)时,容器是空的(或满的)
6.高度优化、复杂的方法的结果与较慢但书写清晰的例程的结果相匹配

断言主要用于开发阶段,避免引入bug和帮助发现程序错误,实际运行阶段不再使用断言,这样断言就不会降低系统性能,使用断言的主要目的是为了在开发阶段调试程序、尽快避免错误

避免在断言中放入可执行代码!!
由于断言可能被禁用,因此程序的正确性永远不应该取决于是否执行断言表达式。
特别是,断言表达式不应该有副作用。

程序之外的事,不受你控制,不要乱断言,外部错误要使用 Exception 外部错误要 机制去处理

使用断言的指南

断言与异常?
断言通常涵盖程序的正确性问题。
-如果针对异常情况触发断言,则纠正措施不仅仅是优雅地处理错误,纠正措施是更改程序的源代码、重新编译并发布新版本的软件。
断言→正确性

例外情况通常包括程序的稳健性问题。
-如果错误处理代码用于解决异常情况,则错误处理将使程序能够优雅地响应错误。
错误/异常处理→稳健性

断言在大型复杂程序和高可靠性程序中尤其有用。
-它们使程序员能够更快地清除不匹配的接口假设、修改代码时出现的错误等等。

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

是否应断言前置/后置条件?
另一种观点是:不要在公共方法中使用断言进行参数检查。
在其他一些开发者眼里,不应该针对参数的 合法性使用断言。
原因:参数检查通常是方法的已发布规范(或合同)的一部分,无论启用还是禁用断言,都必须遵守这些规范:即使spec被违反,也不应通过assert直接fail,而是 应抛出具体的runtime异常

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

将断言和异常处理相结合以实现健壮性
断言和异常处理代码都可能用于解决相同的错误。
对于像Word这样的超大、复杂、长寿命的应用程序,断言是有价值的,因为它们有助于清除尽可能多的开发时间错误。但该应用程序是如此复杂(数百万行代码),并且经历了如此多代的修改,因此假设每一个可能出现的错误都会在软件发布之前被检测和纠正是不现实的,因此错误也必须在系统的生产版本中处理。开发阶段用断言尽可能消除漏洞,在发行版本里用异常处理机制处理漏掉的错误

防御性编程

什么是防御性编程?
防御性编程是一种防御性设计形式,旨在确保软件在不可预见的情况下继续发挥功能。
-防御性编程实践通常用于需要高可用性、安全性或安全性的地方。
这一想法可以被视为减少或消除了墨菲定律的影响。

防御性编程技术
1.保护程序免受无效输入的影响
2.断言
3.异常
4.特定的错误处理技术
5.路障
6.调试辅助工具
防御性编码的最佳形式是一开始就不插入错误。
您可以将防御性编程和其他技术结合使用。

从非法输入保护程序

对来自外部的数 据源要仔细检查,例如:文件、网络数据、用户输入等
对每个函数的输入 参数合法性要做仔细检查,并决定如何处理非法输入

设置路障

路障是一种遏制破坏的策略。
-原因类似于在船体中设置隔离隔间和在建筑物中设置防火墙。
-出于防御编程目的设置路障的一种方法是将某些接口指定为“安全”区域的边界。
-检查跨越安全区域边界的数据的有效性,如果数据无效,则做出合理回应。

定义软件中某些部分处理脏数据,某些部分处理干净数据,是一种有效的方法,可以减轻大部分代码检查坏数据的责任。

类的public方法接收到的外部数据都应被认为是dirty的,需要处理干净再传递到 private方法--隔离舱

另一种方法是操作间技术——数据在进入操作间之前要经过消毒。操作间内的任何东西都被认为是安全的。

“隔离舱”外部的函数应使用异常处理,“隔离舱”内的函数应使用 断言。

停止bug的工具

FindBugs是一个使用静态分析来查找Java代码中的错误的程序。Java语言静态代码分析工具
-它在Java字节码上运行。
潜在错误分为四类:最可怕、最可怕、令人不安和令人担忧。这是对开发人员关于其可能影响或严重性的提示。

SpotBugs是一个使用静态分析来查找Java代码中的错误的程序。
-它是FindBugs的精神继承者,在社区的支持下从它离开的地方继续前行。
它检查了400多种错误模式

  • 5
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值