2021年春季学期
计算学部《软件构造》课程
Lab 2实验报告
3.1.1 Get the code and prepare Git repository· 2
3.1.2 Problem 1: Test Graph <String>· 2
3.1.3 Problem 2: Implement Graph <String>· 3
3.1.3.1 Implement ConcreteEdgesGraph· 3
3.1.3.2 Implement ConcreteVerticesGraph· 5
3.1.4 Problem 3: Implement generic Graph<L>· 7
3.1.4.1 Make the implementations generic· 7
3.1.4.2 Implement Graph.empty()· 7
3.1.5 Problem 4: Poetic walks· 8
3.1.5.2 Implement GraphPoet· 8
3.1.6 使用Eclemma检查测试的代码覆盖度··· 9
3.2 Re-implement the Social Network in Lab1· 10
本次实验训练抽象数据类型(ADT)的设计、规约、测试,并使用面向对象编程(OOP)技术实现 ADT。具体来说:
- 针对给定的应用问题,从问题描述中识别所需的 ADT;
- 设计 ADT 规约(pre-condition、post-condition)并评估规约的质量;
- 根据 ADT 的规约设计测试用例;
- ADT 的泛型化;
- 根据规约设计 ADT 的多种不同的实现;针对每种实现,设计其表示(representation)、表示不变性(rep invariant)、抽象过程(abstraction function)
- 使用 OOP 实现 ADT,并判定表示不变性是否违反、各实现是否存在表示泄露(rep exposure);
- 测试 ADT 的实现并评估测试的覆盖度;
- 使用 ADT 及其实现,为应用问题开发程序;
- 在测试代码中,能够写出 testing strategy 并据此设计测试用例。
Eclipse和Git在上一次实验中已经配置好了。本次实验只需要在eclipse中配置EclEmma用来检查测试的代码覆盖度。在Help->Eclipse Marketplace中查找EclEmma,发现最初安装Eclipse就已经默认安装EclEmma了。这样,本实验所需要的所有环境就都配置好了。
在这里给出你的GitHub Lab2仓库的URL地址(Lab2-学号)。
https://github.com/ComputerScienceHIT/HIT-Lab2-1190200817
请仔细对照实验手册,针对三个问题中的每一项任务,在下面各节中记录你的实验过程、阐述你的设计思路和问题求解思路,可辅之以示意图或关键源代码加以说明(但千万不要把你的源代码全部粘贴过来!)。
任务要求实现两种不同形式的图,给出了一个图接口来规定图所包含的各种方法,两种不同的实现需要继承这个接口,实现给定方法,同时需要满足泛型要求。在完成了两种实现后,任选一种实现形式完成自动扩展诗歌的任务。
使用git init初始化一个本地仓库,通过git clone将GitHub上的文件下载到这个仓库中。这时就可以利用IDE进行代码编写。每当认为某一处代码修改完成,可以让它成为一个新的版本时,利用git add添加相应代码,使用git commit将本地文件加入到本地仓库。当所有的代码都修改完毕,通过git push origin master将本地仓库push到GitHub上。
-
-
- Problem 1: Test Graph <String>
-
思路:对Graph<String>接口中的所有方法进行测试,设计相应的测试策略,按等价类划分的方法进行测试。
测试策略:(1)对add方法进行测试,等价类划分:加入的点已经存在于图中;加入的点不在图中。(2)对set方法进行测试,等价类划分:设置的边已经在图中,权值依次为大于零(即需要更新权值)、等于零(即删除该边)、小于零(是非法情况,不做任何操作);设置的边不在图中,但顶点在图中,权值依次为大于零、等于零、小于零;设置的边的顶点不在图中,权值依次为大于零、等于零、小于零。(3)对remove方法进行测试,等价类划分:要删除的点在图中,但不关联边;要删除的点在图中,且关联边;要删除的点不在图中。(4)对vertices方法进行测试,等价类划分:点集为空;点集非空。(5)对sources方法进行测试,等价类划分:作为参数的顶点存在源点(返回非空);作为参数的点不存在源点(返回为空)。(6)对targets 方法进行测试,等价类划分同(5),即是否存在目标点。在对每一个方法进行测试时,同时需要考虑其他方法的影响,因此在每个测试中都会同时测试一些其他方法。
结果:在实现了两种形式的图之后,运行编写的junit测试用例,通过。
-
-
- Problem 2: Implement Graph <String>
- Implement ConcreteEdgesGraph
- Problem 2: Implement Graph <String>
-
(1)设计Edge类:Edge类应该含有一条有向边的所有信息,即起点标签、终点标签以及权值。
关于Edge类的AF、RI和防止表示泄露的方式:
Abstraction function:AF(source)=该边的起点、AF(target)=该边的终点、AF(weight)=该边的权值
Representation invariant:source与target非空且weight为正数
Safety from rep exposure:将source,target,weight设置为private,外部无法直接引用
由于Edge需要是不可变类型,将起点标签、终点标签以及权值都设置为private final,并且Edge类中的方法不能修改这些属性。Edge中的方法包括:getSource():获得起点标签;getTarget():获得终点标签;getWeight():获得权值(由于int和顶点类型L都是不可变类型,因此正常返回不会影响其不可变性);Edge():构造方法;checkRep():检查表示不变量;同时重写了hashCode、equals和toString用于Edge类型判断相等。
(2)测试Edge类:对Edge类除checkRep()外的所有方法进行测试,对于getSource()、getTarget()、getWeight()、toString()、hashCode(),都是简单的返回结果,不存在特殊情况,无需划分等价类,对每个方法进行一次测试即可。对于equals()方法,只要划分成两个Edge实例相等和不相等两种情况。
(3)利用Edge实现ConcreteEdgesGraph:
关于ConcreteEdgesGraph类的AF、RI和防止表示泄露的方式:
Abstraction function:AF(vertices) = {vertex | vertex in vertices}、AF(edges) = {edges[i] | 0 <= i < edges.sizes()}
Representation invariant:edges中每条边满足长度大于零;起点终点都在vertices中;以一个点为起点另一个点为终点最多只有一条边(边不能重复)
Safety from rep exposure:将vertices和edges设置为private,外部无法直接引用、返回vertices时进行防御式拷贝
各方法的实现思路如下:
方法名 | 实现思路 |
add() | 判断要加入的顶点是否已经存在于vertices集合,若存在,直接返回false;不存在则向vertices中添加顶点,返回true |
set() | 参数source、target、weight,在edges中寻找是否有以source为起点、target为终点的边。若有,则记录权值并将这条边删去,当weight大于零时重新添加新边;若没有,判断weight的值,若weight大于零,则添加新边,同时可能需要向vertices添加新顶点,否则不做操作。最后,如果之前记录过权值,就将其返回,权值为负时返回-1,其他情况都返回0. |
remove() | 首先判断要删除的点是否在点集vertices中,如果不在,不做任何操作并返回false。若存在,在点集中将该点删除,同时遍历所有的边,只要某条边的起点或终点是要删除的点,就将这条边从edges中删除,最后返回true。 |
vertices() | 返回vertices的一份不可修改的拷贝。 |
sources() | 遍历所有的边,如果某条边的终点是输入参数,就将其放进结果映射中,最后返回结果映射。 |
targets() | 遍历所有的边,如果某条边的起点是输入参数,就将其放进结果映射中,最后返回结果映射。 |
toString() | 依次遍历点集vertices和边集edges,将所有信息组合成字符串,格式为”Vertices:…,Edges:…” |
(4)实现后,利用继承自GraphInstanceTest的ConcreteEdgesGraph进行测试,测试通过。
-
-
-
- Implement ConcreteVerticesGraph
-
-
(1)设计Vertex类:Vertex类应该含有与一个顶点有关的所有信息,即一个Vertex实例相当于一个邻接表。因此属性包括顶点的标签vertex和表示邻接关系的映射adjacent(其中adjacent的键是终点,值是这条边的权值)。为了避免表示泄露,使用private final修饰这两个属性。
关于Vertex类的AF、RI和防止表示泄露的方式:
Abstraction function:AF(vertex) = 顶点的标签、AF(adjacent) = 以该点为起点的所有边的集合
Representation invariant: vertex非空且每个边的权值大于0
Safety from rep exposure:将vertex和adjacent设置为private,外部无法直接引用、返回adjacent时进行防御式拷贝
Vertex中的方法包括:getVertex():获得顶点标签;getAdjacent():获得邻接表;set():改变邻接表中的某个条目,与Graph中的set相对应;remove():删除邻接表中的某个条目,与Graph中的remove相对应;Vertex():构造方法;checkRep():检查表示不变量;同时重写了hashCode、equals和toString用于判断相等。Vertex类型是可变的,因为set()方法和remove()方法都会改变Vertex内部的属性值。
(2)测试Vertex类:对Vertex类除checkRep()外的所有方法进行测试。其中getVertex()只是简单地获得标签,无需划分等价类;getAdjacent()划分为邻接表空/非空两种情况;equals()划分为两个Vertex实例相等/不相等;toString()划分为邻接表空/非空两种情况;测试set()、remove()方法的策略和GraphInstanceTest中对set()、remove()的测试策略一致,详见3.1.2.
(3)利用Vertex实现ConcreteVerticesGraph:
关于ConcreteVerticesGraph类的AF、RI和防止表示泄露的方式:
Abstraction function:AF(vertices) = {vertices[i] | 0 <= i < vertices.sizes()}
Representation invariant:vertices中不能有重复顶点
Safety from rep exposure:将vertices设置为private,外部无法直接引用
各方法的实现思路如下:
方法名 | 实现思路 |
add() | 判断要加入的顶点是否是vertices中某个Vertex实例的标签,若不是,直接返回false;否则以输入参数为标签新建一个Vertex并加入vertices,返回true |
set() | 参数source、target、weight,在vertices中寻找是否有以source为标签的Vertex实例。若有,继续在其邻接表中寻找是否存在一个键是target,存在则记录权值,当weight>0时更新,weight=0时删除,weight<0时不操作;否则在weight大于零时添加新边。若没有,只有在weight>0时新建一个以source为标签的Vertex并加入vertices中,同时添加一条新边。只有在之前记录权值的情况下返回权值,权值为负时直接返回-1,其他情况均返回零。 |
remove() | 首先判断要删除的点是否是vertices中某个Vertex实例的标签,如果不是,不做任何操作并返回false。若是,在vertices中将该点删除。同时遍历vertices中所有的Vertex实例,删除其邻接表中关于要删除边的信息,最后返回true。 |
vertices() | 遍历vertices,将每个点的标签加入返回集合即可。 |
sources() | 遍历所有顶点,如果其邻接表中有终点的信息,就将其放进结果映射中,最后返回结果映射。 |
targets() | 如果源点存在,返回源点的邻接表的一份不可修改拷贝;否则返回一个空映射。 |
toString() | 依次遍历点集vertices和边集edges,将所有信息组合成字符串,格式为”Vertices:…,Edges:…” |
(4)实现后,利用继承自GraphInstanceTest的ConcreteVerticesGraph进行测试,测试通过。
由于在针对String类型实现Graph时没有用到String类的特殊性质,所有相等的比较都是用equals实现,所有转化为字符串都是用toString实现,也没有用到String可比较的性质。因此,可以直接将刚才针对String类实现的ConcreteEdgesGraph和ConcreteVerticesGraph中的所有String更换为L即可。同时,Edge类和Vertex类也需要使用泛型,将其中所有的String改为L。在GraphStaticTest测试程序中,用和3.1.2中相同的测试策略,测试了泛型为Integer的情况,测试通过。
-
-
-
- Implement Graph.empty()
-
-
在Graph.empty()中,只需返回一个具体的Graph实例即可,可以在ConcreteEdgesGraph和ConcreteVerticesGraph任选其一。我选择了ConcreteVerticesGraph。
综上,关于Graph的实现全部完成,所有测试都顺利通过。
任务要求利用之前实现的图,将语料库文件转化为一种图结构,并且根据输入的字符串在图中搜索可以插入的单词,完成诗句的扩展。
对GraphPoet中的所有方法进行测试。GraphPoet中只有两个方法,构造方法GraphPoet()和产生诗的poem()。使用按等价类划分的方法对每个方法进行测试。
测试策略:(1)对构造方法,功能是读入语料库文件并产生相应的图。按照文件的形式划分等价类:文件为空;文件只有一行,且分隔符都是单个空格;文件有一行,但分隔符可能是连续空格;文件有多行但不存在空行;文件存在空行。针对以上情况设计语料库文件形式,测试是否能够正确生成相应的图结构。(2)对于poem()方法。按照输入字符串的形式和扩展形式划分等价类:输入字符串是空串;输入字符串存在连续空格;扩展后未插入单词;插入单词的过程中不存在比较行为;插入单词的过程中存在比较行为,测试是否能够正确扩展诗句。
-
-
-
- Implement GraphPoet
-
-
构造函数的实现:每次从文件中读入一行,用.split(“\\s+”)将读入的文本分割成单词,同时将所有单词转化为小写。对于分割后String数组中的每一个字符串,从它到它之后的字符串的边的权重加一,可以通过使用两个set()完成。
同时需要注意保留行尾的字符串,它到下一行第一个单词的边的权重需要加一。此外还要特别考虑空行的情况。
poem()函数的实现:先将输入字符串用.split(“\\s+”)分隔成单词,从前向后依次遍历两个单词,对前面的单词调用targets()得到它所有指向的顶点,对后面的单词调用sources()得到所有指向它的顶点。得到的两个结果的交集就是这两个单词的所有中间顶点,找到使得两条边权值和最大的顶点就是需要插入的顶点。维护一个List按顺序保存结果字符串中的每一个单词,每得到一个要插入的顶点,就将其对应的单词加到List中,每次循环最后将后面的单词也加入List中。循环结束时,List保存的就是结果字符串中的每个单词,将它们连接起来,中间由单个空格分隔就得到扩展后的字符串。
使用前面设计的测试用例测试,通过。
语料库内容如下:
扩展代码:
扩展结果:
-
-
- 使用Eclemma检查测试的代码覆盖度
-
除GraphPoet和FriendshipGraph中的main函数没有覆盖,其他基本都覆盖到了。
通过Git提交当前版本到GitHub上你的Lab2仓库:先使用git status确定未被添加的代码;再使用git add添加相应代码;使用git commit将本地文件加入到本地仓库。当所有的代码都修改完毕,通过git push origin master将本地仓库push到GitHub上。
在这里给出你的项目的目录结构树状示意图。(Eclipse中显示的项目目录结构)
利用之前实现好的Graph<L>,重新实现Lab1中的Social NetWork,并且要尽可能复用已经实现的add()和set()方法,并能和之前一样通过测试。这让我们体会到实现好一个类型对之后的工作有很大的简化。
使用ConcreteVerticesGraph<Person>作为FriendshipGraph中的图结构,并且用private final修饰,避免表示泄露。
addVertex()方法直接使用Graph中的add()方法实现:
addEdge()方法直接使用Graph中的set()方法实现,由于此题不带权,因此权值均设置为1:
getDistance()没有太大变化,和之前一样利用广度优先搜索实现。
由于使用ConcreteVerticesGraph<Person>实现,要求Person是不可变类型,定义Person的属性只有一个,即名字字符串,同时用private final修饰。
关于Vertex类的AF、RI和防止表示泄露的方式:
Abstraction function:AF(name) = 这个人的名字
Representation invariant:名字不能为null
Safety from rep exposure:将name设置为private,外部无法引用
Person类的方法包括:checkRep()检查表示不变量;getName()获得名字;Person()构造方法;重写的hashCode()、equals()、toString()使得便于判断重复;同时实现了compareTo()方法使得Person类是可比较的。可以发现,所有方法都无法更改Person内部的属性,因此Person类是不可变的。
-
-
- 客户端main()
-
客户端代码已经在Lab1中给出:
对FriendshipGraph中的所有方法进行测试。使用按等价类划分的方法对每个方法进行测试。
测试策略:(1)对于addVertex()方法,测试添加重复的人和不重复的人的情况;(2)对于addEdge()方法,测试三种情况:添加朋友关系的人不在图中;添加自己到自己的朋友关系;正常添加朋友;(3)对于getDistance()测试三种情况:没有朋友关系的两个人之间的距离;有朋友关系的两个人之间的距离;自己到自己的距离。
使用按照测试策略设计的测试用例测试,通过。
-
-
- 提交至Git仓库
-
通过Git提交当前版本到GitHub上你的Lab2仓库:先使用git status确定未被添加的代码;再使用git add添加相应代码;使用git commit将本地文件加入到本地仓库。当所有的代码都修改完毕,通过git push origin master将本地仓库push到GitHub上。
项目的目录结构树状示意图见3.1.7
请使用表格方式记录你的进度情况,以超过半小时的连续编程时间为一行。
日期 | 时间段 | 计划任务 | 实际完成情况 |
2021.5.21 | 18:30-20:30 | 设计GraphInstanceTest中的测试用例 | 完成 |
2021.5.22 | 19:30-22:00 | 实现ConcreteEdgesGraph<String>并进行测试 | 完成 |
2021.5.23 | 18:30-21:00 | 实现ConcreteVerticesGraph<String>并进行测试 | 完成 |
2021.5.23 | 21:00-21:30 | 将特定类型转换为泛型 | 发现前面实现中的问题并进行了修改,延期一小时完成 |
2021.5.24 | 20:00-22:30 | 设计GraphPoet测试用例并实现GraphPoet,进行测试 | 完成 |
2021.5.25 | 10:00-11:30 | 利用ConcreteVerticesGraph重新实现之前的FriendshipGraph并进行测试 | 完成 |
遇到的难点 | 解决途径 |
撰写AF,RI,Safety from rep exposure时不知道从何写起
| 查看PPT中的示例以及在网上查找相关的示例理解撰写思路和方法,并逐渐掌握了撰写方法。 |
测试用例覆盖的情况不全面
| 仔细地设计测试用例,尽可能覆盖等价类中所有的情况。 |
在实现使用泛型的类时,一定要事先完整地想好各个功能如何实现,特别是每个方法都要满足所有可能的类型,要特别小心不要利用某些类特有的性质。比如,在实现泛型为String的特殊情况时,使用了compareTo方法,这个方法不是所有类都有的,扩展到泛型时会产生错误,需要重新设计方法的实现。
- 面向ADT的编程和直接面向应用场景编程,你体会到二者有何差异?
面向ADT的编程需要从实际中进行抽象,并进行合理的设计,有很好的可扩展性和可复用性;而直接面向应用场景编程只针对特定应用,每次更换应用场景时都要重新编程,扩展性很差,只适合简单的应用场景。
- 使用泛型和不使用泛型的编程,对你来说有何差异?
使用泛型能够扩展应用范围,可以适用于不同的类,能够很好地应对变化,并且能提高可复用性。但使用泛型也使得功能的实现更加困难,必须考虑可能的所有类型,不能依赖任何特定类型,需要更全面细致的考虑。
- 在给出ADT的规约后就开始编写测试用例,优势是什么?你是否能够适应这种测试方式?
这使得测试时不需考虑方法的实际实现,不会让测试过度依赖具体的实现方法,这也体现了测试优先编程的优势,可以更加容易地发现错误,保证测试的有效性。我也在逐渐适应这种测试优先的编程方法,也体会到这种方法确实很有效。
- P1设计的ADT在多个应用场景下使用,这种复用带来什么好处?
可以提高代码的可复用性,减少大量重复的工作。
- P3要求你从0开始设计ADT并使用它们完成一个具体应用,你是否已适应从具体应用场景到ADT的“抽象映射”?相比起P1给出了ADT非常明确的rep和方法、ADT之间的逻辑关系,P3要求你自主设计这些内容,你的感受如何?
正在逐渐适应从具体应用场景到ADT的“抽象映射”,并逐渐掌握抽象的方法。在P3自主设计时,仍然需要参考P1给出的逻辑关系,仿照P1进行设计,我也在自主设计的过程中逐渐掌握设计思路和方法,并尽可能地进行独立设计。
- 为ADT撰写specification, invariants, RI, AF,时刻注意ADT是否有rep exposure,这些工作的意义是什么?你是否愿意在以后编程中坚持这么做?
这些工作使得客户端了解各方法的功能但无法得知内部具体实现,同时可以防止内部变量被客户端恶意修改。时刻检查表示不变量,可以更好地保证代码的正确和安全。虽然这些工作有些麻烦,但却是好的软件必须具备的,因此我愿意在以后编程中坚持这么做。
- 关于本实验的工作量、难度、deadline。
工作量不大,难度不太大,但是撰写specification, invariants, RI, AF以及testing strategy会使用较长时间。deadline很合适,给了三周时间,完全可以完成。
- 《软件构造》课程进展到目前,你对该课程有何体会和建议?
逐渐理解了软件构造过程中独有的思路和方法,也逐渐适应了与之前完全不同的编程过程。建议实验内容还是翻译成中文,有些英文读起来会有歧义。