软件构造第六章学习笔记

前言:软件构造第六章学习笔记

一.Correctness & Robustness

1.Correctness(正确性)

程序按照spec加以执行的能力,是最重要的质量指标。
规约要求的都做不到,怎样取信于client?因此正确性是软件构造时最重要的质量指标!!!

2.Robustness(健壮性)

系统在不正常输入或不正常外部环境下仍能够表现正常的程度
规约中无法涉及所有可能的输入的情况,那么规约没有规定到的程序要怎么做?这是健壮性考虑的事情:让程序尽可能地“容错”。
具体的容错大概是包括:
<1>处理未期望的行为和错误终止
<2>即使终止执行,也要准确/无歧义的向用户展示全面的错误信息
<3>错误信息有助于进行debug

3.面向健壮性编程的总体原则

<1>总是假定用户恶意、假定自己的代码可能失败:因此总要考虑极端情况,没有“不可能” ,同时封闭实现细节,限定用户的恶意行为
<2>把用户想象成白痴,可能输入任何东西 :因此返回给用户的错误提示信息要详细、准确、无歧义,尽最大可能让用户了解现在程序到底出现什么状况

4.二者的关系

<1>正确性要求永不给用户错误的结果,当出现规约要求以外的情况,更倾向于直接报错(这种处理可真是程序员的福音0.0)
<2>健壮性尽可能的保持软件运行而不是总是退出,向上面介绍一样地进行容错
下面这张图很好地说明了<1>与<2>这两点
在这里插入图片描述
<3>二者虽然在规约之外的情况处理上面有些不同之处,但其实二者并没有多少冲突,健壮性进行容错处理,也是规约要求以外的情况的处理方式之一,规约要求的保证完成,就仍然是正确的,并且健壮性良好的程序能更好地为client分担压力,不小心有了什么错误输入?没关系,程序可以友好地提示你。相对地,程序员要在程序中考虑更多的情况。
<4>一个程序是否可靠,是由正确性与健壮性共同考量的
<5>对外的接口,倾向于健壮,因为这是与用户主要交互的部分,尽可能为client分担压力;对内的实现,倾向于正确 ,尽快、尽早知道程序的问题是什么;同时在内外部之间做好隔离,防止“错误”扩散

二.Techniques for Robustness

这里的技术主要是利用错误与异常处理来提高程序的健壮性
要想利用它们,首先我们需要知道它们是什么(在java中)

1. Error & Exception

首先,在java中,Error和Exception是两个类,都继承自Throwable类。
在这里插入图片描述
对于图中用圆圈圈起来的部分你可以先不去管它,只看这个类的继承结构即可。它们都继承自Throwable类,用来表示出现的一系列不正常情况(Abnormal),并且衍生出了许多子类。

二者的区别主要在于他们表示的“不正常”来源是什么:
对于Error来说,每个error描述的异常情况是java系统内部运行时出现错误,比如系统内存容量不够,java虚拟机出错等等,这种错误是非常罕见的,并且我们也是对其无能为力的,我们唯一能做的,就是在发生这种错误时,想办法让程序优雅的结束。

而对于异常Exception:它表示程序执行中的非正常事件,导致程序无法再按预想的流程执行,可以对其进行捕获、处理,例如用户输入错误,指定文件找不到等等,具体还可细分为checked和unchecked两类。异常是我们程序中可能会出现的问题,因此为了提高健壮性,我们可以对各种异常进行处理,而error既然我们无能为力,那么就不再过多关注它

2.Classification of Exceptions

①.Unchecked Exceptions

这种异常是由于程序员源代码的设计存在故障,处理不当,是可通过更好地设计代码来避免的。
比如:
在这里插入图片描述
像上面这种数组越界访问导致的异常,完全是在源代码中可以避免出现的。
而这种Unchecked Exceptions的代表就是Exception的子类RuntimeException以及它的子类。对于这种异常,如果在代码中提前进行验证,这些故障就可以避免

此外,Unchecked Exceptions还包括java系统内部运行时出现的错误,即error,这是程序员无法修复的、无能为力的。
这就将类结构那张图中的圆圈部分解释清楚了,它们同属于Unchecked Exceptions。

对于Unchecked Exceptions,即使不处理,在编程和编译的时候,IDE与编译器均不会给出任何错误提示,但执行时出现就会直接导致程序失败。
对于代码中潜在bug引发的Unchecked Exceptions,我们要及早去修改源代码,避免这样的错误

②.Checked Exceptions

这种异常是非运行时异常,是程序员无法完全控制的外在问题所导致的。即使在代码中提前加以验证(文件是否存在),也无法完全避免失效发生(验证后通过但在读取的同时文件消失)。
在Exception的所有子类中,除去RuntimeException及其子类,剩余的全部对应于Checked Exception这一种。

而对于这种异常,编译器会帮助检查你的程序是否已抛出或处理了可能的异常 ,这也就对应于其名称中的"checked",对其进行的"check"是由编译器来完成的。必须捕获并指定错误处理器handler,否则编译无法通过
而这种所谓的捕获与处理就涉及到对异常的处理手段:
throws 、throw、try-catch(-finally)

③.Unchecked Exceptions or Checked Exceptions?

如果我们在程序中发现了某种异常情况,需要对其进行处理,并返回给client相关的信息以提示client,那么我们对于这种异常情况是该声明为Unchecked Exceptions还是Checked Exceptions呢?
在这里插入图片描述
这个表格对二者的使用情况作了简明扼要的概括:
<1>如果客户端对出现的这种异常无能为力,那么采用unchecked exception
<2> 如果client仅仅想看到异常信息,可以简单抛出一个unchecked exception
<3>如果客户端可以通过其他的方法恢复异常,那么采用checked exception
<4>错误可预料,但无法预防,但可以有手段从中恢复,此时使用checked exception
<5>Checked exception应该让客户端从中得到丰富的信息。

④.Conclusion

对上面的叙述进行总结,展示为下面的表格:
在这里插入图片描述

3.Checked Exceptions Handling

之前介绍了什么是异常、异常的种类、使用场景等,接下来主要陈述我们对异常如何进行处理。

①自定义创建异常类
一般情况下,如果jdk中的exception类能够描述你的程序发生的错误,那么就使用jdk提供的就好。
但有时这些exception类无法充分描述你的程序发生的错误,比如异常的名称太过笼统,而你想要通过异常名就能大致推得错误的种类,比如用TimeFormatException异常名来推知是时间格式发生错误,或者你想要异常类保存更多信息,从而帮助你进行恢复时,你可以自定义异常类。
如定义上述的TimeFormatException:

	public class TimeFormatException extends Exception 
	{ ... }

想要定义一种Checked Exception,那么就继承自Exception
反之,如果想要定义一种Unchecked Exception,那么就继承自 RuntimeException ,如:

	public class FooRuntimeException extends RuntimeException 
	{ ... }

一般情况下,我们对于异常类习惯上同时给出一个默认构造函数和一个包含详细消息的构造函数。
详细信息用于向我们报告详细错误信息,还原现场,应该包含“对该异常有贡献”的所有 参数和域的值。
出于安全考虑,千万不要在细节信息中包含密码、密钥以及类似的信息!
Exception类本身就包含这两种构造函数,如果你想偷懒啦,不想自己去设计这个异常类了,那么很多方法都可以直接调用Exception父类的方法就可以啦,如:

	public class FooException extends Exception { 
	public FooException() { 
		super(); 
	} 
	public FooException(String message) { 
		super(message); 
	} 
	public FooException(String message, Throwable cause) { 
		super(message, cause); 
	} 
	public FooException(Throwable cause) { 
	super(cause); 
	} 
	}

代码中有刚才没有提到的以另一种异常作为参数的构造方法,即public FooException(Throwable cause)和public FooException(String message, Throwable cause)这种就是以另一种异常的信息作为自己的信息进行构造和处理。

②throw语句:将自己程序中的异常抛出
在程序中使用throw语句可以将程序本身的异常抛出,这样自己就不用捕获、处理这些异常,而是将它们统统"丢给"调用它的"上一级"。如:

	if (!in.hasNext()) // EOF encountered 
	{ 
		if (n < len) 
			throw new EOFException(); 
	} 

这里是在读入数据时判断出一种异常情况,这时我们并没有对它进行处理,而是直接抛出给上一级调用者。

当你觉得抛出给上一级调用者很美妙的时候,也不要忘了,这也意味着你的程序如果调用了其他方法,那么也是少不了对你调用方法抛出异常的处理的。调用声明了checked异常的方法,则必须处理异常或继续传递.

当你throw的异常属于Checked Exception时,需要在方法的签名中用throws进行声明,如:
public boolean readFromFile() throws FileNotFoundException
同时也要在规约中进行声明:
@throws FileNotFoundException

③try-catch:对异常进行捕获、处理
一般的模型就是:

	try { 
		code 
		more code 
		more code 
	} catch (ExceptionType e) { 
		handler for this type 
	}

当try块中抛出在catch子句中指定的异常时,将忽略出现异常位置之后的代码 ,由catch子句进行异常处理。无异常抛出时,catch子句不执行 .
如果抛出的异常,在catch语句中没有匹配的异常处理,则被访问的程序退出,由client处理.
同时,本来catch语句下面是用来做exception handling的,但也可以在catch里抛出异常,这样可以更改exception的类型,更方便client端获取错误信息并处理;使client不依赖于无关不感兴趣的低层异常。
例如:

	try { 
		access the database 
	} catch (SQLException e) { 
		throw new ServletException("database error: " + e.getMessage()); 
	}

client可能对于访问数据库出错这种低层异常不感兴趣,那么我们将其转化为服务出错这种异常,同时我们也保留根原因,在信息中加入"database error"予以提示。

④try-catch-finally
我们前面提到过,当try块中抛出在catch子句中指定的异常时,将忽略出现异常位置之后的代码 ,由catch子句进行异常处理,那么如果try语句中存在那种必须要执行的代码怎么办?比如你打开了一个文件,本来在后面是要close的,但执行了catch之后就跳过了,这时怎么办?
你可能会想,在catch里面也加上呗,当然可以。但其实try-catch本身就将正常代码与异常部分比较相近地糅合到一起了,而你在catch里面再糅合一些正常代码,会使结构更加不清晰,这是我的理解。因此,这里又引入了finally

	InputStream in = new FileInputStream(. . .);
	try { 
		code that might throw exceptions 
	} catch (IOException e) { 
		show error message 
	 }
	finally { 
		in.close();
	} 

这里我们由于打开了文件需要关闭,因此将关闭的代码放在了finally中。对于Finally部分的代码,无论是否捕获异常都会被执行。 finally是一定执行的!!!
我们来看下面这个例子:

	static boolean decision() {
	 try { 
	 	return true; 
	 } finally { 
	 	return false; 
	 } 
	}

这个方法会返回什么呢?
是返回false的。即使是try中有return语句,finally也是一定会执行的,并且在try之后执行。
finally通常用于清理资源如关闭文件等必须执行的语句。

此外,对于这个问题,可能你会觉得finally在写代码时容易丢,忘了写怎么办?
还有一种方法,与其功能类似:Try-with-Resources(TWR)

	try (Scanner in = new Scanner(new FileInputStream("/usr/share/dict/words"), "UTF-8"); 
	PrintWriter out = new PrintWriter("out.txt")){
	while (in.hasNext()) 			
		out.println(in.next().toUpperCase());
}

在try后面加上一个括号,里面写上需要清理的资源,这样java在执行时会为你自动补齐一个finally,一定会执行里面的资源清理。

三、Assertion

Assertion即断言,在开发阶段的代码中嵌入,检验某些“假设”是否成立。若成立,表明程序运行正常,否则表明存在错误。
在代码中用assert语句直接对假设进行检验,在java中有两种形式,如下面的代码块展示:
1)assert + condition(检验的假设)

	public class AssertionTest{
		public void numberHandling(int num){
			……
			assert num>=0;
			……
		}
	}

当num满足assert要求的条件时可以继续执行后面的代码,当不满足时就会报错:AssertionError
2)assert condition(检验的假设) : message(错误提示信息)

	 public class AssertionTest{
  		public void numberHandling(int num){
   			……
   			assert (num>=0): "num is negative";
   			……
  		}
 	}

当num满足assert的条件时正常执行代码,否则会报错AssertionError,并且将"num is negative"提示信息显示出来。

这二者的区别只是有无提示信息,当assert的条件不满足时,出现AssertionError,所构造的message在发生错误时显示给用户,便于快速发现错误所在。

从前面的说明中,我们就可以发现断言的优点:
首先,断言即是对代码中程序员所做假设的文档化,代码中的断言部分可以很好地体现程序员的设计思想
其次,增强程序员对代码质量的信心:对代码所做的假设都保持正确
再者,当assert的条件不满足时,出现AssertionError,便于快速发现错误所在。

那么断言既然这么好,是不是意味着我们在代码中就应该处处用断言呢?
当然不是,既然断言要检验假设,那么必然会有判断的性能损失。
此外,既然当假设不满足时断言的行为是直接报错,那么其健壮性相对地就会比较差,其更多地是检验、维护代码正确性的工具,因此断言主要用于开发阶段,加入到内部代码中,避免引入和帮助发现bug,而在代码发布后实际使用时,assertion可以被disabled(java中对于断言使用的开关,关闭后所有断言失效),以此来维护性能。

优点和缺点我们都分析了,那么我们到底什么时候该用断言?

①检查内部不变量时,例如程序中要求一个变量要保持不变,那么我们可以用断言检查,因为这是一定要保持的,如果出现问题就应该是内部问题,因此我们用断言在开发时检测bug。

②检查表示不变量时,习惯用在checkRep中,这我们都很熟悉了。表示不变量是必须要保持的,采用断言的道理同上。

③检查控制流不变量,例如,我们的程序要求对于switch-case的分支中,到达default的情况是不应该发生的,那么我们可以在default处用断言判定错误,如图所示:
在这里插入图片描述
④检查方法的前置条件,这在方法中是要求必须为true的,那么我们可以采用断言保证,当不是true时,就相当于是规约之外的情况了,那么出现AssertionError自然也是允许的(只是健壮性较差)

⑤检查方法的后置条件,这是检查我们方法的执行结果,是自查行为,如果出错说明执行过程存在问题,尽快发现错误。

总结一下,断言主要用于开发阶段,检查程序的内部状态是否符合规约,避免引入和帮助发现bug。并且断言可以对程序的后置条件进行检查,同时程序参数如果来自于自己所写的其他代码,也可以使用断言判定前置条件来帮助发现错误。而在实际运行阶段,不再使用断言(开关关闭)。
外部参数的失败要使用Exception机制去处理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值