10 Software Construction for Robustness

Software Construction for Robustness

6.1 Robustness and Correctness

健壮性与正确性

健壮性:系统在不正常输入或不正常外部环境下仍能够表现正常。

而且即使因为意外终止执行了,也要向用户展示准确的错误信息

面向健壮性的编程要求封闭实现细节,以达到限定用户的恶意行为的目的,并且要考虑到各种各样大的极端情况,假设用户可以做任何事情。

目的是让用户变得更容易:出错也可以容忍,因为程序内部已有容错机制。

对自己的代码要保守,对用户的行为要开放

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

面向正确性的编程要求永不给用户错误的结果

目的是让开发者变得更容易用户输入错误,直接结束。(不满足precondition的调用)

正确性倾向于直接报错(error),健壮性则倾向于容错(fault-tolerance)

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

提升健壮性和正确性的一般步骤:
Step 0:开发assertions, defensive programing, code review, formal validation
Step 1:发现错误Memory dump, stack traces, execution logs, testing
Step 2:错误定位bug localization, debug
Step 3:修复code revision

6.2 Error and Exception Handling

提高健壮性:错误与异常处理

1. Java中的错误和异常
Java中错误和异常的派生关系图:
在这里插入图片描述
Error:与代码无关,程序员通常无能为力,一旦发生,想办法让程序优雅的结束。也可以通过对外部环境的配置解决问题,如用户输入错误、设备错误、物理限制等。

Exception:一定是程序导致的问题,可以捕获、可以处理。

由于程序员对Error通常无法预料无法解决,因此重点关注可被解决的Exception
2. 异常
异常:程序执行中的非正常事件,程序无法再按预想的流程执行。

程序除了return正常退出之外还可以通过throws抛出异常来非正常退出。

在上一部分中的图中可以看到,Java中Exception可以被分为两个部分,蓝色的运行时异常和绿色的其他异常

  1. 运行时异常:由程序员在代码里处理不当造成,在源代码中引入了故障,而如果在代码中提前进行验证,这些故障就可以避免。动态类型检查的时候会发现这种异常,而一旦出现,代码就必然有错误,可以通过调试解决。

  2. 其他异常:由外部原因造成,程序员无法完全控制的外在问题所导致的,即使在代码中提前加以验证,也无法完全避免失效发生。

Checked exceptionUnchecked exception(Error + RuntimeException)
Basic必须被显式地捕获或者传递 (try-catch-finally-throw),否则编译器无法通过,在静态类型检查时就会报错可以不处理,编译没问题,但执行时出现就导致程序失败,代表程序中的潜在bug
Class of Exception继承自Exception类 上图中的绿色部分继承自RuntimeException类和Error类 上图中的蓝色部分
Handling从异常发生的现场获取详细的信息,利用异常返回的信息来明确操作失败的原因,并加以合理的恢复处理简单打印异常信息,无法再继续处理
Appearance代码看起来复杂,正常逻辑代码和异常处理代码混在一起清晰,简单

选取checked exception还是unchecked exception可遵循下面的原则

  1. checked exception:如果客户端可以通过其他的方法恢复异常,而对开发者来说错误可预料但不可预防,它的出现已经脱离了程序能够掌控的范围。
  2. unchecked exception:如果客户端对出现的这种异常无能为力,而对开发者来说错误可预料可预防,它可以通过调整程序来避免出现。

通过throws声明checked exception检查性异常

  1. spec和throws声明中写清本方法会抛出的所有checked exception
  2. 非检查性异常不应该出现在throws或@throws。

遵守LSP原则。

抛出异常
使用throw关键字抛出异常,如:throw new EOFException();

在抛出异常的时候利用Exception的构造函数,将发生错误的现场信息充分的传递给client。

大部分情况下,JDK所提供的异常类都足够我们使用,如果JDK提供的exception类无法充分描述程序发生的错误,可以创建自己的异常类。
定义一个checked exception
自定义checked exception 可从 Exception 或从 Exception 的子类(例如 IOException)派生。
习惯上同时提供一个默认构造函数和一个包含详细消息的构造函数。

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); }
    ... //也可以定义其他的方法,甚至增加rep,像创建一个普通类一样创建它
}

然后就可以像使用JDK提供的异常类一样使用了。

当然也可以自定义Unchecked exception,但并不推荐这么做

处理checked exceptions
可以使用try-catch语法对抛出的异常进行处理,也可以用throws语法将异常抛给上一级调用,然后在上一级中使用try-catch处理。如:

public static void main(String args[]) {
	FileInputStream fis = null;
	try {
		fis = new FileInputStream("sample.txt");
		int c;
		while ((c = fis.read()) != -1)
			System.out.print((char) c);
		fis.close();
	} catch (FileNotFoundException e) { //处理
		e.printStackTrace();
	} catch (Exception e) { //多个catch
		e.printStackTrace();
	}
}
//抛给上级方法,这里是直接抛给了系统
public static void main(String args[])  {
	try{
        fun();
    } catch (IOExeption e) {
        e.printStackTrace();
    }
}
public static void fun() throws IOException {
    FileInputStream fis = null;
	fis = new FileInputStream("sample.txt");
	int k;
	while ((k = fis.read()) != -1)
		System.out.print((char) k);
	fis.close();
}

所以,try-chtch所捕获到的异常可能有两个来源:
一、自己内部的代码产生的
二、调用了其他的方法,并且该方法未处理抛给了本方法

Unchecked exceptions也可以使用throws声明或try-catch进行捕获,但大多数时候是不需要的,也不应该这么做——掩耳盗铃,对发现的编程错误充耳不闻。
因为Unchecked exceptions的目的就是为了发现错误,从而消除潜在的bug。

如果父类型中的方法没有抛出异常,那么子类型中的方法必须捕获所有的checked exception

Checked exception应该让客户端从中得到丰富的信息。
而要想让代码更加易读,倾向于用unchecked exception来处理程序中的错误

异常的再抛出
当捕获到的异常类型不希望让客户端看到的时候,就可以捕获它,然后重新抛出一个新的类型的异常。但是这么做的时候最好保留“根原因”。如:

try {
	... //access the database
}
catch (SQLException e) { //捕获到内部的异常类型
	Throwable se = new ServletException("database error"); //创建新异常类型抛出
	se.initCause(e); //把新异常的原因设定为本来的异常产生的原因
	throw se; //抛出
}
//客户端
Throwable e = se.getCause(); //这样就可以获得所有的原因了

finally
当异常抛出时,方法中正常执行的代码被终止,如果异常发生前曾申请过某些资源,那么异常发生后这些资源要被恰当的清理(不清理也行,Java提供了垃圾回收机制,只不过浪费点性能)。

所以形成了try-catch-finally结构。不管程序是否碰到异常,finally都会被执行

TWR:Try-with-Resources语法可以自动关闭资源,而免去了最后finally关闭的麻烦。如:

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());
}

LSP中的异常

  1. 如果子类型中override了父类型中的方法,那么子类型中方法抛出的异常不能比父类型抛出的异常类型更宽泛,异常不能逆变
  2. 子类型方法可以抛出更具体的异常,也可以不抛出任何异常,异常可以协变
  3. 如果父类型的方法未抛出异常,那么子类型的方法也不能抛出异常

目的还是为了能够让客户端能够用统一的方式处理不同类型的对象。
在这里插入图片描述

6.3 Assertions and Defensive Programming

提高正确性:断言与防御式编程

1. 断言
断言是为了在开发阶段检验某些应该成立的条件在此时是否成立,如果成立则表明程序正常,如果不成立则表明存在错误。断言即是对代码中程序员所做假设的文档化,在实际使用的时候assertion都会被disabled,就不会影响性能了。

不带信息的断言:assert condition;

带信息的断言:assert condition : message;

使用的地方

  1. 内部不变量:判断某个变量应该满足的条件
  2. 表示不变量:常用的checkRep()
  3. 控制流不变量:例如不想让程序走向switch-case的某个分支,则可以用断言直接在分支上assert false;
  4. 方法的前置条件:判断传入参数是否满足前置条件,不满足则fail fast
  5. 方法的后置条件:判断结果是否满足后置条件,程序一定要执行正确。

注意

  1. 不要把正常的功能语句和断言在一起,如assert list.remove(x);这样在禁掉断言的时候会把功能语句一并禁掉。
  2. 程序之外的事,不受控制,不要乱断言,只有涉及到内部执行逻辑的时候才使用assert
  3. Java缺省关闭断言,要记得打开(-ea)

Assertion vs. Exception

  1. 错误/异常处理是提高健壮性,处理外部行为断言是提高正确性,处理内部行为

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

  3. 来自于自己所写的代码(如post-condition),可以使用断言来帮助发现错误;来自于外部的参数(如pre-condition),使用异常处理

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

防御式编程基本思路

  1. 来自外部的数据源要仔细检查,例如:文件、网络数据、用户输入等

  2. 对每个函数的输入参数合法性要做仔细检查,并决定如何处理非法输入

  3. *Barricade隔离舱:类的public方法接收到的外部数据都应被认为是dirty的,需要处理干净再传递到private方法。“隔离舱”外部的函数应使用异常处理,“隔离舱”内的函数应使用断言。

6.4 Debugging

代码调试

1. Bug和Debugging

debug的一般过程:重现–>诊断–>修复–>反思

Debug占用了大量的开发时间,其中错误定位又占据了绝大部分的调试时间

2.诊断定位

诊断策略

  1. instrumentation 测量:也就是最基本的打印内容,把要观察的对象全部打印到控制台,当然,最后要把这些语句都删掉。另外,也可使用log功能统一管理。
  2. Divide and Conquer 分治:也就是二分法,通过把代码一次次的分割,确定bug的位置。传说中的防狼围栏算法。
  3. Slicing 切片:出错的应该是一个变量之类的东西,所以按照与这个变量对整个程序的语句做分类,只保留与该变量有关的与语句,所以大大见减小了错误定位的范围。
  4. Focus on difference 寻找差异
    4.1 充分利用版本控制系统,找出在哪个commit之后出现了bug症状。有可能不是上个版本引入的bug,而是之前的某个版本引进的,所以需要回推到该个版本。git bisect指令可以实现这个比较定位功能,它的实现也是通过二分法定位到引入bug的版本。
    4.2 Delta Debugging 基于差异的调试:通过比较各个测试用例所覆盖的语句的差异,看看未通过的测试用例与通过的测试用例的覆盖的语句多/少了哪些语句,那么这些语句就有可能是bug所在位置。
    4.3 查找其他方面的差异:软硬件环境、JVM参数配置、输入文件、…
  5. Symbolic Debugging 符号化执行,不需输入特定的值,使用“符号值”(而非“实际值”)作为输入,解释器模拟程序执行,获得每个变量的“符号化表达式”,从而可判断是否执行正确。
  6. Debugger 调试器:单步执行发现错误。

3. Logging(日志)

日志管理工具

  1. JDK logging
  2. Apache Log4j
  3. Apache Commons-logging
  4. SLF4J
  5. syslog-ng

java.util.logging

通过设定日志级别来确定要log哪些信息,也可以通过Handler将日志存储在不同的地方

日志级别(从高到低):SEVERE、WARNING、INFO、CONFIG、FINE、FINER、FINEST
设定级别logger.setLevel(Level.INFO);

Logger
可以设定全局的logger,但是会造成信息混乱的后果,于是需要定义自己的logger,然后在程序中使用它。

import java.util.logging.*;
private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp");
//often using class name as logger name

//或者用这种方式
public class LogTest {
	static String strClassName = LogTest.class.getName(); //get class name
	static Logger myLogger = Logger.getLogger(strClassName);
	// using class name as logger name
	...
	myLogger.info(“ XXXX ”);
}

注意:通常使用类名作为logger名

Logging Handlers(日志处理器)

Handler是日志的输出位置,缺省输出到控制台
日志处理器也需要设定日志级别。

Handler类型StreamHandler、ConsoleHandler、FileHandler、SocketHandler、MemoryHandler

设定处理器logger.addHandler(new FileHandler(“test.txt”));

日志的格式SimpleFormatter

设定格式fileHandler.setFormatter(new SimpleFormatter());

6.5 Testing and Test-First Programming

软件测试与测试优先的编程

软件测试是提高软件质量的重要手段,目的是为了确认软件是否达到可用级别。

测试分为:单元测试、集成测试、系统测试、验收测试、回归测试

测试可以说明软件是正确的,但不能说明软件没有错误。因为很明显不可能使用穷举法进行暴力,所以测试无法覆盖到所有的可能,所以就有可能有某个未覆盖到的特例可能造成bug。
1. 测试优先编程
先写spec,再写符合spec的测试用例,最后写代码。

Test-driven development (TDD) 测试驱动的开发

使用JUnit进行单元测试
单元测试:针对软件的最小单元模型开展测试,隔离各个模块,容易定位错误和调试

JUnit在测试方法前使用@Testannotation来表明这是一个JUnit测试方法。如果要在测试开始之前做一些准备则在准备方法前添加@Beforeannotation,如果要在测试结束后做一些收尾工作则在收尾方法前添加@Afterannotation。

JUnit使用的是断言机制来完成测试,常用的有三种测试方法:assertEquals()、assertTrue()、assertFalse()

建立的JUnit测试类要和被测试类保持相同的包结构,但存放在不同的文件夹下,而在IDE中将两个文件夹都设置为源文件夹,这样在逻辑上它们目录下的同名文件夹被IDE认为是同一个包。

2. 黑盒测试
黑盒测试 (Black-box Testing):对程序外部表现出来的行为的测试。用于检查代码的功能,不关心内部实现细节。

黑盒测试的测试用例是按照spec来设计的,所以我们为了用尽可能少的测试用例,尽快运行,并尽可能大的发现程序的错误,提出如下两种按照spec设计测试用例的设计思想:
1.等价类划分
基于等价类划分的测试:将被测函数的输入域针对每个输入数据需要满足的约束条件划分为等价类,从等价类中导出测试用例

这种思想基于假设:相似的输入产生相似的行为。因此我们可以从等价类中选取一个代表作为测试用例。

例如:对于一个输入数据为整数的情况来说,我们可以按照正负来划分,也可以按照奇偶来划分。

2.边界值分析
经验发现大量的错误发生在输入域的“边界”而非中央,所以我们要在等价类划分的基础上增加边界值分析的方法来作为补充。

而对于有多个参数的的例子来说,不同的参数有不同的等价类划分,也有不同的边界取值,因此一个问题就是如何组合两个等价类的取值。

第一种办法是取笛卡尔积:全覆盖。将所有维度上的所有取值都组合一次,每个组合都设计一个测试用例。当然,也会存在没有意义的组合。
第二种办法是覆盖每个取值:最少1次即可。显然笛卡尔积将会产生大量的测试用例,所以这种办法是每个维度上的每个值只需要有一个测试用例覆盖即可。我们为此付出的代价是有可能降低了测试覆盖度。

3. 白盒测试
白盒测试 (White-box Testing):对程序内部代码结构的测试。要考虑内部实现细节。

白盒测试的测试用例是根据程序执行的路径来设计的,同样为了用尽可能少的测试用例,尽快运行,并尽可能大的发现程序的错误,必须根据程序具体的执行过程来设计测试用例。

独立/基本路径测试:对程序所有执行路径进行等价类划分,找出有代表性的最简单的路径(例如循环只需执行1次),设计测试用例使每一条基本路径被至少覆盖1次。

覆盖度
黑盒测试也有覆盖度,但是由于我们不知道内部实现,因此覆盖度对黑盒测试的意义不大。所以覆盖度真正描述的是白盒测试的充分程度
代码覆盖度越低,测试越不充分,但要做到很高的代码覆盖度,需要更多的测试用例,测试代价高
评判代码覆盖度的几个方面:

  1. 函数覆盖:所有的方法是否都被测试一次
  2. 语句覆盖:所有的语句都被测试一次
  3. 分支覆盖:所有的分支都被测试一次
  4. 条件覆盖:所有的条件的真假组合都被执行一次
  5. 路径覆盖:所有的路径都被执行一次,这是测试中最强的覆盖标准

测试效果:路径覆盖>分支覆盖>语句覆盖
测试难度:路径覆盖>分支覆盖>语句覆盖

可以看出,测试效果越好,测试难度越大,所以我们要根据实际情况的需求,选取不同的侧重点,达到该条件下最好的测试效果。

4. 自动测试和回归测试
自动测试:由于手工测试的代价太高,因此有了自动测试(自动调用被测函数、自动判定测试结果、自动计算覆盖度),只是测试用例的自动执行,并非自动生成测试用例。
回归测试一旦程序被修改,重新执行之前的所有测试。因为没有办法保证修改后的程序没有引入原来没有的bug,因此即使修改前已经通过了的测试用例也需要再次进行测试。这也是为什么我们需要自动测试。

5. 记录测试策略
在测试类中要做好对等价类划分等测试策略的记录,测试策略(根据什么来选择测试用例)非常重要,需要在程序中以注释的形式记录下来。

目的:在代码评审过程中,其他人可以理解你的测试,并评判你的测试是否足够充分。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
基于vue健身小程序正是采用微信小程序和网络设计的新型系统,可以有效的把健身信息与网络相结合,为用户提供工作帮助和管理需求。本系统采用mysql数据库存储数据,兼容性更强,可跨越多种平台,采用的框架为ssm。主要设计的内容包括课程信息、教练信息、健身视频。教练可以上传健身视频,学员可以购买课程和预约教练以及在线充值、发帖。为了可以给用户更多的提醒,本系统中加入了当前登录角色的提示内容。管理员在系统中可以更新各种数据信息。本系统是信息化社会发展的必然产物,可以为用户提供更为高效的管理以及辅助,同时也可以改变健身房管理的局面,提高效率。 登录功能为管理员、教练和学员登录,在登录界面设计中包括用户名和密码、权限的检验。用户名和密码、权限的检验过程由数据库自动完成,此过程需要1秒左右。首先由用户填写账号和密码,选择权限,然后点击登录系统,数据库自行对用户名和密码进行对比,所填写数据正确方能进行登录,所填写数据错误则需要返回登录界面重新登录。首页界面是最直接的展示,用户可以对系统进行最直接的了解。在本功能界面里可以看到背景图片、功能导航栏,视频信息、课程信息、教练信息等。学员信息是健身房的重要组成部分,管理员可以添加学员信息,查询学员信息.。教练信息管理功能分为管理员管理教练信息和登记、查询教练信息,管理员可以看到教练的各项基本信息,可以删除教练的基本信息。系统里展示的健身视频都可以由管理员进行审核和添加管理,教练也可以发布视频。管理员可以输入视频名称和上传视频来实现健身视频的添加。管理员和教练可以上传培训课程,学员可以浏览课程信息。管理员和教练都可以管理预约信息,学员在看到教练后可以进行预约。管理员可以审核帖子信息。管理员、教练和学员都可以管理订单信息。学员在课程详情里可以购买课程。学员在教练详情里可以评价、收藏以及预约。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值