第一节 多维视图和质量目标
软件构造多维度视图
红色标注为重点(考试会考选择题)
Moment 特定时刻的软件形态 Period 软件形态随时间的变化
AST (Abstract Syntax Tree) 抽象语法树
SCI (Software Configuration Item) 配置项
concurrent multithreads 并发多线程
内部质量/外部质量
外部质量因素影响用户,内部质量因素影响软件本身和它的开发者
外部质量取决于内部质量
软件的内部属性和外部属性(判断)
外部质量因素
正确性(Correctness)、健壮性(Robustness)(针对异常情况处理)、可扩展性(Extendibility)、可复用性(Reusability)、兼容性(Compatibility)、性能(Efficiency)、可移植性(Portability)(Java的优点之一)、易用性(Easy of use)、功能性(Functionality)、及时性(Timeliness)
质量目标之间冲突
不同质量因素折中,但”正确性“绝不能与其他质量因素折中
第二、十二节 测试、异常、健壮性
测试(Test)
测试用例 = 输入 + 执行条件 + 期望结果
写spec -> 写符合spec的测试用例 -> 写代码执行测试反复修改
TDD(Test-driven development)
好的测试用例的特性与好的测试的特性相似
*写测试用例时必须既要考虑有效输入也要考虑无效输入
单元测试
针对软件的最小单元模型开展测试,隔离各个模块,容易定位错误和调试
Junit assertEquals assertThat查看实际值是否满足指定条件
黑盒测试/白盒测试
黑盒测试:对程序外部表现出来的行为的测试(从spec导出测试用例,不考虑内部实现)
白盒测试:考虑内部实现细节(一般较早执行)
白盒测试一般由开发人员完成,黑盒测试一般由测试人员完成
白盒测试标准:独立/基本路径测试:对程序所有执行路径进行等价类划分,找出有代表性的最简单的路径(例如循环只需执行一次),设计测试用例使每一条基本路径被至少覆盖一次。
回归测试
一旦程序被修改,重新执行之前的所有测试
代码覆盖度
函数覆盖、语句覆盖、分支覆盖、条件覆盖、路径覆盖
分支覆盖和条件覆盖:分支覆盖 a && b – true/false 条件覆盖:a True a False b True b False
语句覆盖:只需要让 a && b 语句执行一遍即可
条件覆盖和分支覆盖之间没有包含关系
测试效果:路径覆盖 > 分支覆盖 > 语句覆盖(测试难度也是这个顺序)
*等价类划分(重点)
将被测函数的输入域划分成为等价类,从等价类中导出测试用例
例:乘法计算 BigInteger × BigInteger -> BigInteger 函数,可从正/负角度进行等价类划分,同时考虑边界条件—— 0,1,-1,很小的正整数,很小的负整数,很大的正整数,很大的负整数
(注:等价类划分时,错误数据也要考虑其中)?
BVA(Boundary Value Analysis)边界值分析:是对等价类划分方法的补充
在等价类划分时,将边界作为等价类之一加入考虑
(等价类划分具体写法可参见习题课)
健壮性和正确性
可靠性 = 正确性 + 健壮性
健壮性:面向用户 正确性:面向开发者
private方法可只保证正确性,但面向用户的还需要保证健壮性
错误和异常(Error and Exception)
Error:不是由程序本身引起,由系统限制引起
Exception:自己程序导致的问题,可以捕获、处理
下面绿色的部分表示是由用户输入等引起的,是可预测的,在程序运行时处理
不需要实例化Error,也不需要捕获(捕获了也处理不了)
异常分为:运行时异常(RuntimeException)和其他异常
运行时异常是程序员代码里处理不当造成,其他异常由外部原因造成
Checked and unchecked exceptions
Unchecked exceptions = Error + RuntimeExceptions
两者区分:编译器是否能检查出(编译器不会检查Unchecked exception)
checked exception 必须捕获并指定错误处理器handler,否则编译无法通过
五个处理异常时使用的关键字:try,catch,finally,throws,throw
Unchecked异常也能用try/catch来进行捕获,但大多数时时不需要的,也不应该这样做——掩耳盗铃,对发现的编程错误充耳不闻!
尽量用unchecked exception来处理编程错误——使代码更易读
错误可预料,不可预防,但有手段从中恢复,用checked exception
(该表需要记住)
规约中需要包含所有该方法抛出的checked exception
异常的抛出需要满足LSP原则(协变):子类不能比父类抛出更多、更宽泛的异常
可自定义异常类
异常发生后如果找不到处理器,就终止程序,在控制台打印出 stack trace
异常只有两种处理方法:向上抛 / 捕获
如果父类型的方法没有抛出异常,那么子类型中的方法必须捕获所有的checked exception
try- catch -finally:无论是否出现异常,finally块中包含的语句都会被执行(一般为对资源的释放、管理等)
多个catch块不是依次顺序执行的,而是并发的,哪一个最匹配就执行哪一个
finally会在执行完try/catch块之后再执行
断言(assert)
可尽早发现bug,避免扩散
断言用在开发阶段,运行时可被一次性关闭。断言保证的是正确性。
异常可用于处理用户输入错误,用在release阶段,提高健壮性
assert可用来限定:内部不变量、表示不变量、控制流不变量、前置条件、后置条件等
第三节 构造过程与配置管理
软件的过程模型
(会考察软件使用哪一种开发模型)
两种基本形式:Linear-线性过程 Iterative-迭代过程
五种模型:瀑布过程、增量过程、V字过程、原型过程、螺旋模型
-
瀑布过程(Waterfall)
即简单的线性过程,无迭代。虽然管理简单但无法适应需求增加/变化。 -
增量过程(Incremental)
是多个瀑布的串行。要求每一段增量都是可运行的,第二个增量不能影响第一个增量(即要求接口必须简单清晰)。较容易适应需求的增加。 -
V字过程(V-Model)
是瀑布过程的扩展,主要强调每一个阶段都要进行测试。 -
原型过程(Prototyping)
在原型上持续不断地迭代,发现用户的需求变化。时间代价高,开发质量也高。
适用于用户需求不稳定的情况。 缺点:可能注重原型而忽略了系统的架构设计。
-
螺旋过程(Spiral)
多轮迭代,进行严格的风险分析。 适用于长周期、有风险的大程序。 -
敏捷开发(Agile development)
通过快速迭代和小规模的持续改进,快速适应变化。
要求:1. 强调交互 2. 不需要文档 3. 合作 4. 变化
适用于需求不稳定,快速开发(对高质量、高风险不适用)
SCM(软件配置管理)
软件配置管理:追踪和控制软件的变化。 软件配置项:软件中发生变化的基本单元
VCS(版本控制系统)
本地版本控制系统:仓库存储在开发者本地及其,无法共享和合作。
集中式版本控制系统:仓库存储于独立的服务器,支持多开发者之间协作。
分布式版本控制系统:仓库存储于独立的服务器 + 每个开发者的本地机器。(如Git)
*Git(重点)
Git的四个工作区域
工作区,即平时存放代码的地方。
暂存区,虚拟区域,无真实空间,用于临时存放改动。
本地仓库,安全存放数据的位置,这里有提交到所有版本的数据
远程仓库,托管代码的服务器,如Github
需要掌握根据Git文件的状态来判断处于哪一目录/区域中。
Object Graph
边 A -> B表示在版本B的基础上作变化形成了版本A(指向的对象是父对象)
除了最初的commit,每个commit都有一个指向父亲的指针
多个commit指向同一个父亲——分支
一个commit指向两个父亲——合并
Git中一个子对象只能有0,1,2个父对象,而一个父对象可以有多个子对象。
Git和传统版本控制工具的区别:Git存储的是变化后的文件,传统VCS存储版本之间的变化(行),很难创建分支。
Git一个文件可以存在在不同的版本中。
Git命令和版本图
git commit -a :把所有的change先add然后再commit
git fetch :从远程获取最新版本到本地,不会自动merge
git checkout -b:创建并切换分支
git remote add origin … :与远程仓库关联
注:git会强制在push之前fetch(如果远程端做了更改),然后再merge和push
第四节 数据类型和类型检验
基本数据类型/对象数据类型
静态/动态类型检查
(Java是静态类型检查,在编译阶段进行检查,Java不进行动态类型检测)
静态类型检查:语法、类名/函数名、参数数目、参数类型、返回值类型
动态类型检查:非法的参数值、非法的返回值、越界、空指针
注意List和List是在静态类型检测中报错。
Mutable/Immutable
Java可进行自动垃圾回收。 Immutable好处:安全,但浪费空间。
*final特性: final 限定的是引用不变(如果mutable改变值不会报错),final类无法派生子类,final方法无法被子类重写。
使用Mutable可获得更好的性能,也适合多个模块间共享数据,但不够安全!
Date也是mutable类!避免使用!
可以使用java.time包中的其他immutable类型的类:LocalDateTime, Instant等
immutable拷贝时间 O(n2)
传参数尽量用immutable类型(保证参数不变性),如果传mutable参数可先进行defensive copying(考试经常考)
必须通过类中的方法来改变类中的属性(防止信息泄露)
*Snapshot diagram(重点)
Immutable对象:用双线椭圆
不可变的引用(用final修饰的变量):用双线箭头
String s1 = new String(“abc”);
List list = new ArrayList();
list.add(s1);
s1 = s1.concat(“d”);
System.out.println(list.get(0));
String s2 = s1.concat(“e”);
list.set(0, s2);
System.out.println(list.get(0));
1
2
3
4
5
6
7
8
Array and Collections
Iterator
mutable类型,有两种方法:next()和hasNext(),next()方法是mutate的
需要注意,当用Iterator迭代List中元素,涉及到remove时,由于remove后List内元素索引会发生改变,会出现错误。
Collections
基本类型及其封装对象类型都是immutable的
List、Map、ArrayList等都是mutable的
可以利用Collections类提供的方法将mutable类包装成immutable
Collections.unmodifiableList Collections.unmodifiableSet Collections.unmodifiableMap
这种包装器得到的结果是不可变的,只能看,不能修改(其实就是disabled了一些mutate方法或者让其抛出异常)
这种”不可变“是在运行阶段获得的,编译阶段无法对此进行静态检查
虽然不能用包装后的对象对其进行修改,但依旧能用包装前的对象进行修改
第五节 设计规约
规约(Specifications)
规约不要给出任何方法的实现。
规约不能被程序进行检测 (×) ——函数描述可以被检测(参数类型),注释不能被检测。
规约注释包含:功能描述、输入数据限制、返回值
行为等价性(Behavioral equivalence)
一般站在用户(客户端)的角度看(可能会给一定前提),可根据规约判断是否行为等价
前置条件和后置条件
前置条件,关键词requires 后置条件,关键词effects
前置条件是对客户端的约束,在使用方法时必须满足的条件;后置条件是对开发者的约束,方法结束时必须满足的条件
如果前置条件满足,后置条件必须满足;如果前置条件不满足,后置条件想淦神魔都可以
Java中的规约
Java中的静态类型声明是一种规约,可据此进行静态类型检查static checking
方法前的注释也是一种规约,但需要人工判定其是否满足
前置条件在 @param中,后置条件在 @return和@throws中(@return中不能包含具体类型,如 @return boolean)
如果方法对输入的参数做了改变,一定要在规约中说明
*规约的强弱(本节重点)
判定标准
规约强度 S2 >= S1 : 前置条件更弱,后置条件更强
spec变强,即更放松的前置条件 + 更严格的后置条件
(考试常出无法比较的规约)
Diagramming specifications
每一个点代表一个方法的实现。如果某个具体实现满足规约,就落在其范围内;否则在其之外。
更强的规约,表示为更小的区域。(实现的自由度小,面积小)
第六节 抽象数据类型(ADT)
ADT 的特性:表示泄漏、抽象函数 AF 、表示不变量 RI
基于数学的形式对 ADT 的这些核心特征进行描述并应用于设计中
(本节常考大题,包括是否出现表示暴露,AF和RI等)
ADT是由操作定义的,与其内部实现无关
ADT四种操作类型
构造器(Creator):创建一个该类型的新对象(可能实现为构造函数或静态函数)
生产器(Producer):从一个类型的旧对象创建一个新对象(如String中concat方法)
观察器(Observer):返回一个不同类型的对象(如List中的size方法)
变值器(Mutator):改变对象属性的方法(如List中的add方法)
如果一个构造器是用静态方法来实现的,通常称为工厂方法 (factory method):如Java中String类的String.valueOf(Object obj)方法
Mutators通常返回void,但也可以返回非空类型,如Set.add()返回的类型为boolean
immutable类型的ADT无变值器(Mutator)
判断是哪种操作类型,首先需要确定是mutable还是immutable
注:Collections.unmodifiableList() 是 producer; 如果一个方法既改变了对象属性,也返回了不同类型的对象,它是变值器Mutator
*表示独立性(Representation Independence)
表示独立性:client使用ADT时无需考虑其内部如何实现,ADT内部表示的变化不应该影响外部spec和客户端。
上述实例就违反了表示独立性,因为public限定了这是一个instance variable,但是后面的final又限定了客户端不能够实际改变这个类的immutability属性
*不变性(Invariants)
由ADT来负责其不变量,与client端的任何行为无关
(考试必考表示泄露)
表示泄露出现情况:
public 类型的数据 -> private final
mutable 类型共享引用
不应该包含mutate方法
当复制代价很高时,可在规约中强加条件(但是不推荐!)
*Rep Invariant and Abstraction Function(RI and AF)
R : 表示空间 A:抽象空间(ADT开发者关注表示空间R,client关注抽象空间A)
R -> A 的映射:1. 满射:所有抽象值都要有一个rep value 2. 未必单射:一个抽象值可能有多个表示 3. 未必双射:不是所有的表示值都有对应的抽象值
抽象函数(AF):R和A之间映射关系的函数
表示不变性 RI:某个具体的“表示”是否是“合法的”(R -> boolean)
在ADT的规约里若出现"值",也只能是A空间的"值"
有益的可变性(Beneficent mutation)
(该部分大概率会出选择)
即immutable的属性是可变的,但是要保证用户角度是一样的
例如:[1, 2] 和 [2, 4]在A空间可均表示1/2
书写AF和RI
可用ADT的不变量来代替前置条件(相当于将复杂的precondition封装到了ADT内部)