2022年春季学期
计算学部《软件构造》课程
Lab 2实验报告
3.1.1 Get the code and prepare Git repository
3.1.2 Problem 1: Test Graph <String>
3.1.3 Problem 2: Implement Graph <String>
3.1.3.1 Implement ConcreteEdgesGraph
3.1.3.2 Implement ConcreteVerticesGraph
3.1.4 Problem 3: Implement generic Graph<L>
3.1.4.1 Make the implementations generic
3.1.4.2 Implement Graph.empty()
本次实验训练抽象数据类型(ADT)的设计、规约、测试,并使用面向对象
编程(OOP)技术实现 ADT。具体来说:
l 针对给定的应用问题,从问题描述中识别所需的 ADT;
l 设计 ADT 规约(pre-condition、post-condition)并评估规约的质量;
l 根据 ADT 的规约设计测试用例;
l ADT 的泛型化;
l 根据规约设计 ADT 的多种不同的实现;针对每种实现,设计其表示
(representation)、表示不变性(rep invariant)、抽象过程(abstraction
function)
l 使用 OOP 实现 ADT,并判定表示不变性是否违反、各实现是否存在表
示泄露(rep exposure);
l 测试 ADT 的实现并评估测试的覆盖度;
l 使用 ADT 及其实现,为应用问题开发程序;
l 在测试代码中,能够写出 testing strategy 并据此设计测试用例。
2.实验环境配置
本次实验继续使用实验一时已经配置好的环境,不需要再次配置。
IDEA中自带用于统计JUnit测试用例的代码覆盖度的plugin,不需要再安装。
GitHub Lab2仓库的URL地址:
https://github.com/ComputerScienceHIT/HIT-Lab2-2021111824.git
3.实验过程
请仔细对照实验手册,针对三个问题中的每一项任务,在下面各节中记录你的实验过程、阐述你的设计思路和问题求解思路,可辅之以示意图或关键源代码加以说明(但千万不要把你的源代码全部粘贴过来!)。
3.1Poetic Walks
该任务主要为进行不同类型ADT的设计以及对ADT规约的测试,并练习进行测试优先的编程。在任务的后半部分还有对ADT的泛化。
3.1.1Get the code and prepare Git repository
(1)使用git clone指令从GitHub获取该任务的代码
(2)在本地创建git仓库
(3)使用git管理本地开发
与远程仓库连接
3.1.2Problem 1: Test Graph <String>
我们要对Graph接口上的方法进行测试
(1)empty方法的测试已经给出,不需再进行修改
(2)对于add、remove、set、vertices、sources、targets等方法的测试用例书写,采用以下等价类划分方法
测试用例全部通过
代码覆盖率
3.1.3 Problem 2: Implement Graph <String>
3.1.3.1Implement ConcreteEdgesGraph
首先,我们先应实现Edge类
(1)Edge中应含有三个私有变量source、target、weight分别代表源点、目标顶点以及边的权值
(2)然后设计checkRep函数:源点及目标定点不为空且权值不为负
(3)实现Edge的构造方法
(4)实现Edge的基本方法
(5)最后实现函数toString
然后,来实现ConcreteEdgesGraph类
(1)先定义顶点和边的集合
(2)然后设计checkRep函数:边的权值不为负且源点和目标点都在顶点集合中
(3)实现构造方法
(4)实现该类中的其他方法
Ⅰ. add
需要遍历vertices集合,查找有无与点vertex相同的点。若存在,则返回false;若不存在,则将点vertex加入vertices集合中,返回true。
Ⅱ. set
该方法主要包括三种操作:修改已存在边的权值,移除已存在的边,增加新的边。
先判断weight的值,若weight小于0,则报错退出;否则,进行以下步骤:
先遍历edges集合
若该边已经存在于edges中:1)weight > 0,修改该边权值,并返回原权值;
2)weight = 0,删除该边,并返回原权值;
若该边不存在于edges中: 1)weight > 0,将该边添加到edges集合中,并将 不存在于vertices集合中的顶点添加到vertices 集合中,然后返回0;
2)weight = 0,直接返回0。
Ⅲ. remove
若该顶点存在于vertices中,将edges集合中所有以vertex为源点或目标点的边全部移除,然后将该顶点从顶点集合vertices中移除,并返回true;否则返回false。
Ⅳ. vertices
返回由图的顶点构成的集合。为了防止信息泄露,使用防御式拷贝。
Ⅴ. sources
返回所有以target为目标顶点的点以及对应边的权值。返回值的类型为Map,所以需要构造一个Map,然后遍历edges集合寻找所有以target为目标顶点的边并将其源点作为键名、权值作为键值添加到Map中,最后返回该Map。
Ⅵ. targets
返回所有以source为源点的点以及对应边的权值。返回值的类型为Map,所以需要构造一个Map,然后遍历edges集合寻找所有以source为源点的边并将其目标顶点作为键名、权值作为键值添加到Map中,最后返回该Map。
(5)toString
先将vertices中所有的点打印出来,然后遍历edges集合,循环调用Edge类的toString方法,将边打印出来。
3.1.3.2 Implement ConcreteVerticesGraph
首先,我们应该先实现Vertex类
(1)Vertex的成员变量包括:顶点名称、源点集合和目标顶点集合
(2)给出构造方法
(3)设计checkRep函数:检验顶点名称不为空、不含有重复的顶点且编的权值非负
(4)Vertex中的其他方法
Ⅰ. get...
返回顶点名称、源点及目标定点集合(注意防御式拷贝)
Ⅱ.remove_source
若该点存在于源点集合中,则移除该顶点,并返回true;否则返回false。
Ⅲ.remove_target
若该点存在于目标顶点集合中,则移除该顶点,并返回true;否则返回false。
Ⅳ.add_source
该方法主要包括三种操作:修改已存在边的权值,移除已存在的边,增加新的边。
先判断weight的值,若weight小于0,则报错退出;否则,进行以下步骤:
先遍历sources中的键名
若该点已经存在于sources中: 1)weight > 0,修改该边权值,并返回原权值;
2)weight = 0,删除该边,并返回原权值;
若该点不存在于sources中: 1)weight > 0,将该点添加到sources键名中, 并将weight设置为其对应的键值,返回0;
2)weight = 0,直接返回0。
Ⅴ.add_target
该方法主要包括三种操作:修改已存在边的权值,移除已存在的边,增加新的边。
先判断weight的值,若weight小于0,则报错退出;否则,进行以下步骤:
先遍历targets中的键名
若该点已经存在于targets中: 1)weight > 0,修改该边权值,并返回原权值;
2)weight = 0,删除该边,并返回原权值;
若该点不存在于targets中: 1)weight > 0,将该点添加到targets键名中, 并将weight设置为其对应的键值,返回0;
2)weight = 0,直接返回0。
(5)toString
打印该点的信息
然后,我们再来实现ConcreteVerticesGraph类
(1)实现构造方法
(2)设计checkRep函数:判断顶点的名字不为空且不存在相同的顶点
(3)设计该类中其他方法
Ⅰ.add
遍历vertices集合,若存在vertex,则返回false;若不存在,则将vertex加入vertices中,并返回true。
Ⅱ.set
该方法主要包括三种操作:修改已存在边的权值,移除已存在的边,增加新的边。
先判断weight的值,若weight小于0,则报错退出;否则,进行以下步骤:
将source、target加入vertices集合中,然后调用Vetex类中实现的add_source与add_target方法即可。
Ⅲ.remove
遍历vertices集合,判断vertex是否在集合中,若存在,则调用Vertex中的remove_source与remove_target方法删除以vertex为顶点的边并将vertex从vertices集合中移除,然后返回true;否则,返回false。
Ⅳ.vertices
返回由图的顶点构成的集合。使用防御式拷贝,构建一个新的集合,将vertices集合中所有点的标识加入该集合中。
Ⅴ.sources
返回顶点target的源点集合,注意使用防御式拷贝。
Ⅵ.targets
返回顶点source的目标顶点集合,注意防御式拷贝。
(5)toString
先将所有的点的名称打印出来,随后遍历vertices集合,循环调用vertices类的toString方法。
3.1.4 Problem 3: Implement generic Graph<L>
将原有的Graph<String>转化为泛型
3.1.4.1 Make the implementations generic
我们只需将声明中的String替换为L即可
3.1.4.2 Implement Graph.empty()
3.1.5 Problem 4: Poetic walks
通过刚才实现的Graph类,来实现一个语句扩充问题
3.1.5.1Test GraphPoet
需要将情况划分为空文件、单行文字、多行文字进行测试
3.1.5.2 Implement GraphPoet
该类需要实现以下功能:
(1)读入文本文件,并将其抽象为一个带权有向图:
每个单词抽象为顶点、相邻两个单词之间抽象为边,且出现一次权值为1;
(2)对目标字符串进行扩充:
遍历给定文本的单词,对于每一个单词,我们要找出该点所指向的目标顶点集中权值最大的元素,将其添加在原来的两个单词之间。遍历结束后,返回扩充后的字符串。
3.1.5.3 Graph poetry slam
这里实现了自己添加的一个例子,成功将字符串扩充
3.1.6 使用Eclemma检查测试的代码覆盖度
代码覆盖率较高,方法都已基本实现
3.1.7 Before you’re done
如何通过Git提交当前版本到GitHub上你的Lab2仓库。
使用git add添加文件
git commit提交到本地
git push推送到GitHub
在这里给出你的项目的目录结构树状示意图。
3.2 Re-implement the Social Network in Lab1
我们需要利用在P1中设计的Graph类来实现第一次实验中所设计的Social Network。FriendShipGraph中的所有方法都需要用Graph来实现。
3.2.1 FriendshipGraph类
(1)声明一个Graph类型的变量,将泛型替换为<Person>类。然后初始化,生成一个空图。
(2)addVertex
用迭代器遍历顶点集合vertices,若存在顶点与person名称重复,则返回false;若不重复,则将person加入图中,并返回true。
(3)addEdge
使用Graph中的set方法来加入图中的边即可。
(4)getDistance
用广度优先算法搜索最短路径,只是将实验一中的方法用Graph实现即可。
3.2.2 Person类
由于FriendshipGraph中的方法都可以利用P1中的Graph实现,所以Person中只需存放姓名以及相关方法,为了防止数据泄露,要设置为private final关键字。
3.2.3 客户端main()
直接将Lab1中的main函数复制过来即可
3.2.4 测试用例
使用实验一中的测试方法
3.2.5 提交至Git仓库
使用git add添加文件
git commit提交到本地
git push推送到GitHub
在这里给出你的项目的目录结构树状示意图。
4 实验进度记录
请使用表格方式记录你的进度情况,以超过半小时的连续编程时间为一行。
日期 | 时间段 | 计划任务 | 实际完成情况 |
2023.3.21 | 16:00-21:00 | 编写Poetic Walks的problem1、problem2 | 编写时遇到困难,未能按时完成 |
2023.3.22 | 10:00-12:00 | 编写Poetic Walks的problem1、problem2 | 按时完成 |
2023.3.25 | 13:00-14:00 | 编写Poetic Walks的problem3 | 按时完成 |
2023.3.25 | 25:00-18:00 | 编写Poetic Walks的problem4 | 在编写Graph potery slam问题时遇到困难,未能按时完成 |
2023.3.26 | 9:00-11:00 | 编写Poetic Walks的problem4中的Graph potery slam | 按时完成 |
2023.3.28 | 15:30-18:30 | 编写Re-implement the Social Network in Lab1 | 按时完成 |
遇到的难点 | 解决途径 |
第一次自己编写ADT,对编写方法并不熟悉,遇到许多困难 | 通过查阅资料以及向身边的同学请教,最后基本掌握编写方法 |
在编写Poetic Walks中Graph potery slam问题时,对句子拓展问题理解有偏差,导致一直编写错误 | 向同学请教并且再次仔细分析该问题,最后顺利解决问题 |
编写测试用例时,代码的覆盖率总是不高,不能达到良好的对代码进行测试的结果 | 再次学习测试用例的编写方法,使用等价类划分,尽量考虑到边界值等容易忽略但又很容易发生错误的地方,提高测试用例的质量 |
在本次实验中,我学会了基本的ADT编写方法以及泛型使用的优越性,并且了解了在完成代码编写时规约的重要性。同时我也感受到自己对于JAVA语言理解掌握的不足,在实验过程中遇到很多困难,以后还应该加强学习。
6.2 针对以下方面的感受(必答)
(1)面向ADT的编程和直接面向应用场景编程,你体会到二者有何差异?
面向ADT编程,主要是通过各个模块之间互相调用来实现功能;
直接面向应用场景编程,是利用各个方法之间的相互调用,以整个过程为单位进行编程。
(2)使用泛型和不使用泛型的编程,对你来说有何差异?
泛型可以使我们不必考虑变量的类型对他们进行相同的操作,会简化编程过程,更加便利、提高编程效率。
但是不使用泛型可以实现一些更加具体的方法。
(3)在给出ADT的规约后就开始编写测试用例,优势是什么?你是否能够适应这种测试方式?
可以比较客观的来对后续的程序进行测试,比后续再进行编写更加节省时间并且准确性也更高。
我比较能够适应这种测试方法。
(4)P1设计的ADT在多个应用场景下使用,这种复用带来什么好处?
避免了大量的重复代码,大大提高了程序编写的效率,节约了开发时间以及开发成本。
(5)为ADT撰写specification, invariants, RI, AF,时刻注意ADT是否有rep exposure,这些工作的意义是什么?你是否愿意在以后编程中坚持这么做?
这些过程是对程序安全性、健壮性,设计框架的条理性与逻辑性的保障,它确保了程序可以对各种输入进行对应的处理,同时保证了非法数据不会破坏程序的进行。
在以后的编程过程中,我们应该保持这种良好的习惯。
(6)关于本实验的工作量、难度、deadline。
因为是第一次接触ADT的编写,对其掌握还不够深刻,所以对我来说本实验的工作量和难度都偏大,但是实验时间较为充裕,所以可以完成该实验。
(7)《软件构造》课程进展到目前,你对该课程有何收获和建议?
通过对《软件构造》的理论课学习以及在实验中的练习,我更好地掌握了面向对象的程序编写过程,并且有了一定的程序框架设计能力,也学习到了更多理论知识,自身能力有了很大的提升。