哈工大软件构造Lab2

 

2021年春季学期
计算学部《软件构造》课程

Lab 2实验报告

 

目录

 

1 实验目标概述··· 1

2 实验环境配置··· 1

3 实验过程··· 2

3.1 Poetic Walks· 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.1 Test GraphPoet· 8

3.1.5.2 Implement GraphPoet· 8

3.1.5.3 Graph poetry slam·· 9

3.1.6 使用Eclemma检查测试的代码覆盖度··· 9

3.1.7 Before you’re done· 10

3.2 Re-implement the Social Network in Lab1· 10

3.2.1 FriendshipGraph类··· 11

3.2.2 Person类··· 11

3.2.3 客户端main()· 11

3.2.4 测试用例··· 12

3.2.5 提交至Git仓库··· 12

4 实验进度记录··· 13

5 实验过程中遇到的困难与解决途径··· 13

6 实验过程中收获的经验、教训、感想··· 13

6.1 实验过程中收获的经验和教训··· 13

6.2 针对以下方面的感受··· 14

 

  1. 实验目标概述

本次实验训练抽象数据类型(ADT)的设计、规约、测试,并使用面向对象编程(OOP)技术实现 ADT。具体来说:

  1. 针对给定的应用问题,从问题描述中识别所需的 ADT;
  2. 设计 ADT 规约(pre-condition、post-condition)并评估规约的质量;
  3. 根据 ADT 的规约设计测试用例;
  4. ADT 的泛型化;
  5. 根据规约设计 ADT 的多种不同的实现;针对每种实现,设计其表示(representation)、表示不变性(rep invariant)、抽象过程(abstraction function)
  6. 使用 OOP 实现 ADT,并判定表示不变性是否违反、各实现是否存在表示泄露(rep exposure);
  7. 测试 ADT 的实现并评估测试的覆盖度;
  8. 使用 ADT 及其实现,为应用问题开发程序;
  9. 在测试代码中,能够写出 testing strategy 并据此设计测试用例。
  1. 实验环境配置

Eclipse和Git在上一次实验中已经配置好了。本次实验只需要在eclipse中配置EclEmma用来检查测试的代码覆盖度。在Help->Eclipse Marketplace中查找EclEmma,发现最初安装Eclipse就已经默认安装EclEmma了。这样,本实验所需要的所有环境就都配置好了。

在这里给出你的GitHub Lab2仓库的URL地址(Lab2-学号)。

https://github.com/ComputerScienceHIT/HIT-Lab2-1190200817

  1. 实验过程

请仔细对照实验手册,针对三个问题中的每一项任务,在下面各节中记录你的实验过程、阐述你的设计思路和问题求解思路,可辅之以示意图或关键源代码加以说明(但千万不要把你的源代码全部粘贴过来!)。

    1. Poetic Walks

任务要求实现两种不同形式的图,给出了一个图接口来规定图所包含的各种方法,两种不同的实现需要继承这个接口,实现给定方法,同时需要满足泛型要求。在完成了两种实现后,任选一种实现形式完成自动扩展诗歌的任务。

      1. Get the code and prepare Git repository

使用git init初始化一个本地仓库,通过git clone将GitHub上的文件下载到这个仓库中。这时就可以利用IDE进行代码编写。每当认为某一处代码修改完成,可以让它成为一个新的版本时,利用git add添加相应代码,使用git commit将本地文件加入到本地仓库。当所有的代码都修改完毕,通过git push origin master将本地仓库push到GitHub上。

      1. Problem 1: Test Graph <String>

思路:对Graph<String>接口中的所有方法进行测试,设计相应的测试策略,按等价类划分的方法进行测试。

测试策略:(1)对add方法进行测试,等价类划分:加入的点已经存在于图中;加入的点不在图中。(2)对set方法进行测试,等价类划分:设置的边已经在图中,权值依次为大于零(即需要更新权值)、等于零(即删除该边)、小于零(是非法情况,不做任何操作);设置的边不在图中,但顶点在图中,权值依次为大于零、等于零、小于零;设置的边的顶点不在图中,权值依次为大于零、等于零、小于零。(3)对remove方法进行测试,等价类划分:要删除的点在图中,但不关联边;要删除的点在图中,且关联边;要删除的点不在图中。(4)对vertices方法进行测试,等价类划分:点集为空;点集非空。(5)对sources方法进行测试,等价类划分:作为参数的顶点存在源点(返回非空);作为参数的点不存在源点(返回为空)。(6)对targets 方法进行测试,等价类划分同(5),即是否存在目标点。在对每一个方法进行测试时,同时需要考虑其他方法的影响,因此在每个测试中都会同时测试一些其他方法。

结果:在实现了两种形式的图之后,运行编写的junit测试用例,通过。

 

      1. Problem 2: Implement Graph <String>
        1. Implement ConcreteEdgesGraph

(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进行测试,测试通过。

 

        1. 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进行测试,测试通过。

 

      1. Problem 3: Implement generic Graph<L>
        1. Make the implementations generic

由于在针对String类型实现Graph时没有用到String类的特殊性质,所有相等的比较都是用equals实现,所有转化为字符串都是用toString实现,也没有用到String可比较的性质。因此,可以直接将刚才针对String类实现的ConcreteEdgesGraph和ConcreteVerticesGraph中的所有String更换为L即可。同时,Edge类和Vertex类也需要使用泛型,将其中所有的String改为L。在GraphStaticTest测试程序中,用和3.1.2中相同的测试策略,测试了泛型为Integer的情况,测试通过。

 

        1. Implement Graph.empty()

在Graph.empty()中,只需返回一个具体的Graph实例即可,可以在ConcreteEdgesGraph和ConcreteVerticesGraph任选其一。我选择了ConcreteVerticesGraph。

 

综上,关于Graph的实现全部完成,所有测试都顺利通过。

 

      1. Problem 4: Poetic walks

任务要求利用之前实现的图,将语料库文件转化为一种图结构,并且根据输入的字符串在图中搜索可以插入的单词,完成诗句的扩展。

        1. Test GraphPoet

对GraphPoet中的所有方法进行测试。GraphPoet中只有两个方法,构造方法GraphPoet()和产生诗的poem()。使用按等价类划分的方法对每个方法进行测试。

测试策略:(1)对构造方法,功能是读入语料库文件并产生相应的图。按照文件的形式划分等价类:文件为空;文件只有一行,且分隔符都是单个空格;文件有一行,但分隔符可能是连续空格;文件有多行但不存在空行;文件存在空行。针对以上情况设计语料库文件形式,测试是否能够正确生成相应的图结构。(2)对于poem()方法。按照输入字符串的形式和扩展形式划分等价类:输入字符串是空串;输入字符串存在连续空格;扩展后未插入单词;插入单词的过程中不存在比较行为;插入单词的过程中存在比较行为,测试是否能够正确扩展诗句。

        1. Implement GraphPoet

构造函数的实现:每次从文件中读入一行,用.split(“\\s+”)将读入的文本分割成单词,同时将所有单词转化为小写。对于分割后String数组中的每一个字符串,从它到它之后的字符串的边的权重加一,可以通过使用两个set()完成。

  同时需要注意保留行尾的字符串,它到下一行第一个单词的边的权重需要加一。此外还要特别考虑空行的情况。

poem()函数的实现:先将输入字符串用.split(“\\s+”)分隔成单词,从前向后依次遍历两个单词,对前面的单词调用targets()得到它所有指向的顶点,对后面的单词调用sources()得到所有指向它的顶点。得到的两个结果的交集就是这两个单词的所有中间顶点,找到使得两条边权值和最大的顶点就是需要插入的顶点。维护一个List按顺序保存结果字符串中的每一个单词,每得到一个要插入的顶点,就将其对应的单词加到List中,每次循环最后将后面的单词也加入List中。循环结束时,List保存的就是结果字符串中的每个单词,将它们连接起来,中间由单个空格分隔就得到扩展后的字符串。

使用前面设计的测试用例测试,通过。

 

        1. Graph poetry slam

语料库内容如下:

 

扩展代码:

 

扩展结果:

 

      1. 使用Eclemma检查测试的代码覆盖度

除GraphPoet和FriendshipGraph中的main函数没有覆盖,其他基本都覆盖到了。

 

      1. Before you’re done

通过Git提交当前版本到GitHub上你的Lab2仓库:先使用git status确定未被添加的代码;再使用git add添加相应代码;使用git commit将本地文件加入到本地仓库。当所有的代码都修改完毕,通过git push origin master将本地仓库push到GitHub上。

 

在这里给出你的项目的目录结构树状示意图。(Eclipse中显示的项目目录结构)

 

    1. Re-implement the Social Network in Lab1

利用之前实现好的Graph<L>,重新实现Lab1中的Social NetWork,并且要尽可能复用已经实现的add()和set()方法,并能和之前一样通过测试。这让我们体会到实现好一个类型对之后的工作有很大的简化。

      1. FriendshipGraph

使用ConcreteVerticesGraph<Person>作为FriendshipGraph中的图结构,并且用private final修饰,避免表示泄露。

 

addVertex()方法直接使用Graph中的add()方法实现:

 

addEdge()方法直接使用Graph中的set()方法实现,由于此题不带权,因此权值均设置为1:

 

getDistance()没有太大变化,和之前一样利用广度优先搜索实现。

      1. Person

由于使用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类是不可变的。

      1. 客户端main()

客户端代码已经在Lab1中给出:

 

      1. 测试用例

对FriendshipGraph中的所有方法进行测试。使用按等价类划分的方法对每个方法进行测试。

测试策略:(1)对于addVertex()方法,测试添加重复的人和不重复的人的情况;(2)对于addEdge()方法,测试三种情况:添加朋友关系的人不在图中;添加自己到自己的朋友关系;正常添加朋友;(3)对于getDistance()测试三种情况:没有朋友关系的两个人之间的距离;有朋友关系的两个人之间的距离;自己到自己的距离。

使用按照测试策略设计的测试用例测试,通过。

 

      1. 提交至Git仓库

通过Git提交当前版本到GitHub上你的Lab2仓库:先使用git status确定未被添加的代码;再使用git add添加相应代码;使用git commit将本地文件加入到本地仓库。当所有的代码都修改完毕,通过git push origin master将本地仓库push到GitHub上。

项目的目录结构树状示意图见3.1.7

  1. 实验进度记录

请使用表格方式记录你的进度情况,以超过半小时的连续编程时间为一行。

日期

时间段

计划任务

实际完成情况

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并进行测试

完成

  1. 实验过程中遇到的困难与解决途径

遇到的难点

解决途径

撰写AF,RI,Safety from rep exposure时不知道从何写起

 

 

查看PPT中的示例以及在网上查找相关的示例理解撰写思路和方法,并逐渐掌握了撰写方法。

测试用例覆盖的情况不全面

 

 

仔细地设计测试用例,尽可能覆盖等价类中所有的情况。

  1. 实验过程中收获的经验、教训、感想
    1. 实验过程中收获的经验和教训

在实现使用泛型的类时,一定要事先完整地想好各个功能如何实现,特别是每个方法都要满足所有可能的类型,要特别小心不要利用某些类特有的性质。比如,在实现泛型为String的特殊情况时,使用了compareTo方法,这个方法不是所有类都有的,扩展到泛型时会产生错误,需要重新设计方法的实现。

    1. 针对以下方面的感受
  1. 面向ADT的编程和直接面向应用场景编程,你体会到二者有何差异?

面向ADT的编程需要从实际中进行抽象,并进行合理的设计,有很好的可扩展性和可复用性;而直接面向应用场景编程只针对特定应用,每次更换应用场景时都要重新编程,扩展性很差,只适合简单的应用场景。

  1. 使用泛型和不使用泛型的编程,对你来说有何差异?

使用泛型能够扩展应用范围,可以适用于不同的类,能够很好地应对变化,并且能提高可复用性。但使用泛型也使得功能的实现更加困难,必须考虑可能的所有类型,不能依赖任何特定类型,需要更全面细致的考虑。

  1. 在给出ADT的规约后就开始编写测试用例,优势是什么?你是否能够适应这种测试方式?

这使得测试时不需考虑方法的实际实现,不会让测试过度依赖具体的实现方法,这也体现了测试优先编程的优势,可以更加容易地发现错误,保证测试的有效性。我也在逐渐适应这种测试优先的编程方法,也体会到这种方法确实很有效。

  1. P1设计的ADT在多个应用场景下使用,这种复用带来什么好处?

可以提高代码的可复用性,减少大量重复的工作。

  1. P3要求你从0开始设计ADT并使用它们完成一个具体应用,你是否已适应从具体应用场景到ADT的“抽象映射”?相比起P1给出了ADT非常明确的rep和方法、ADT之间的逻辑关系,P3要求你自主设计这些内容,你的感受如何?

正在逐渐适应从具体应用场景到ADT的“抽象映射”,并逐渐掌握抽象的方法。在P3自主设计时,仍然需要参考P1给出的逻辑关系,仿照P1进行设计,我也在自主设计的过程中逐渐掌握设计思路和方法,并尽可能地进行独立设计。

  1. 为ADT撰写specification, invariants, RI, AF,时刻注意ADT是否有rep exposure,这些工作的意义是什么?你是否愿意在以后编程中坚持这么做?

这些工作使得客户端了解各方法的功能但无法得知内部具体实现,同时可以防止内部变量被客户端恶意修改。时刻检查表示不变量,可以更好地保证代码的正确和安全。虽然这些工作有些麻烦,但却是好的软件必须具备的,因此我愿意在以后编程中坚持这么做。

  1. 关于本实验的工作量、难度、deadline。

工作量不大,难度不太大,但是撰写specification, invariants, RI, AF以及testing strategy会使用较长时间。deadline很合适,给了三周时间,完全可以完成。

  1. 《软件构造》课程进展到目前,你对该课程有何体会和建议?

逐渐理解了软件构造过程中独有的思路和方法,也逐渐适应了与之前完全不同的编程过程。建议实验内容还是翻译成中文,有些英文读起来会有歧义。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值