2022年春季学期
计算学部《软件构造》课程
Lab 2实验报告
3.1.1 Get the code and prepare Git repository· 1
3.1.2 Problem 1: Test Graph <String>· 1
3.1.3 Problem 2: Implement Graph <String>· 1
3.1.3.1 Implement ConcreteEdgesGraph· 2
3.1.3.2 Implement ConcreteVerticesGraph· 2
3.1.4 Problem 3: Implement generic Graph<L>· 2
3.1.4.1 Make the implementations generic· 2
3.1.4.2 Implement Graph.empty()· 2
3.1.5 Problem 4: Poetic walks· 2
3.1.5.2 Implement GraphPoet· 2
3.1.6 使用Eclemma检查测试的代码覆盖度··· 2
本次实验训练抽象数据类型(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并据此设计测试用例。
2.实验环境配置
简要陈述你配置本次实验所需环境的过程,必要时可以给出屏幕截图。
特别是要记录配置过程中遇到的问题和困难,以及如何解决的。
在这里给出你的GitHub Lab2仓库的URL地址(Lab2-学号)。
请仔细对照实验手册,针对三个问题中的每一项任务,在下面各节中记录你的实验过程、阐述你的设计思路和问题求解思路,可辅之以示意图或关键源代码加以说明(但千万不要把你的源代码全部粘贴过来!)。
3.1Poetic Walks
在这里简要概述你对该任务的理解。
3.1.1Get the code and prepare Git repository
如何从GitHub获取该任务的代码、在本地创建git仓库、使用git管理本地开发。
3.1.2Problem 1: Test Graph <String>
测试策略与对Graph.empty()静态方法的测试在GraphStaicTest.java中。由于该方法为静态方法,将只有一种实现方式,并且我们只需要运行一次测试。该部分测试已经给出,我们可以改变或添加内容,或不做改变。
在GarphInstanceTest.java中,对实例方法的测试使用emptyInstance()方法创建一个空图,并对Graph接口中的各个方法进行输入划分并测试。
对Graph接口中方法的测试策略大致可以阐述如下:
1) 将图划分为空图和非空图
2) 将图中用于测试的顶点分为两组,一组已经存在于图中,另一组不存在于图中。
3) 将图中用于测试的边根据端点在图中的存在性和边权的性质分组,根据端点存在性分为“两点存在”、“只有一点存在”和“两点不存在”三组;根据边权的性质将边分为“边权为0”和“边权为正整数”两组;
4) 测试方法的实现调用Graph接口提供的add(), remove(), set, sources, targets, vertices()等方法充分覆盖对输入集的划分。
3.1.3Problem 2: Implement Graph <String>
以下各部分,请按照MIT页面上相应部分的要求,逐项列出你的设计和实现思路/过程/结果。
3.1.3 .1 Implement ConcreteEdgesGraph
- 实现Egde类
定义私有局部变量source,target和weight,并设计规约
根据规约设计checkRep函数检查三个变量的值是否合法
依次实现Edge类的方法:
ToString函数,用一种安全的方式打印边的信息
2.实现ConcreteEdgesGraph
定义私有变量,用Set存点,List存边并设计规约
构造性方法,只需定义一个空图即可
根据规约设计checkRep函数:
实现ConcreteEdgesGraph中的其他方法:
1) public boolean add(L vertex):
遍历vertices集合,查找是否存在与点vertex相同的点。若存在,则返回false;若不存在,则在vertices集合中添加点vertex,然后返回true;
2) public int set(L source, L target, int weight):
遍历edges集合,若存在对应端点的边,则根据weight的值进行修改权值/删除该边的操作,并返回原来边的权值;若不存在对应的边,则判断weight的值是否为0,权值不为0时,则添加该边至edges集合中,并将不存在于vertices集合中的点添加到vertices集合中,然后返回0;权值为0时不做任何操作,直接返回0。
3) public boolean remove(L vertex):
若vertices集合中不存在该点,则返回false。存在时,将edges集合中所有以vertex为源点/终点的边全部移除,将vertices集合中vertex删除,返回true。
4) public Set<L> vertices():
为了保证数据信息不泄露,需要返回一个由vertices集合复制的新集合。(防御式拷贝)
5) public Map<L, Integer> sources(L target):
所有终点为target的点的边的源点作为键名、权值作为键值添加到新生成的Map中,最后返回该Map。
6) public Map<L, Integer> targets(L source):
所有源点为source的点的边的终点作为键名、权值作为键值添加到新生成的Map中,最后返回该Map。
7) public String toString():
函数需要将图的所有信息打印出来,即所有的点与边的信息。故先将所有的点的名称打印出来,随后遍历edges集合,循环调用Edge类的toString方法即可。
3.1.3.2 Implement ConcreteVerticesGraph
- 实现Egde类
Vertex中在HashMap中记录源点对应边的终点和权值,终点对应边的源点和权值。由于我们需要为每个Vertex变量定义特定的标识来区分不同的变量,因此增加L类型的变量name作为标记。
构造性方法:
根据规约设计checkRep函数检查合法性:
实现其他方法:
1)观察器:
2) public int setSources(L source, int weight):
权值为0:将这个点从map中删除,起到删除以该点为源点边的作用
权值不为0:将新的边的源点和权值键值对加入map中
若source点存在,返回值为其原键值(int型),否则返回null
3) public int setTargets(L target, int weight):
对终点进行如上操作
4) public boolean equals(Vertex<L> vertice):
定义标志变量name相同的两个点为同一个点,所以对name进行比较
5) public String toString():
根据对称性,打印该点的终点集信息即可。
2. 实现ConcreteVerticesGraph类:
根据规约设计checkRep方法:
实现其他方法:
1)public boolean add(L vertex):
遍历vertices集合,查找有无与点vertex相同的点即可。若存在,则返回false;不存在,则在vertices集合中添加点vertex,然后返回true;
2)public int set(L source, L target, int weight):
该方法主要分三种操作:修改已有的边,移除已有的边,添加新的边。因此,先遍历vertices集合,确定源点和终点是否存在。若源点存在,则在其终点集中进行setTargets操作;若终点存在,则在其源点集进行setSources操作。若某一端点不存在,则创建该点,设置对应的源点/终点集,然后在vertices集合加入新生成的点。返回值与对应setTargets/setSources操作返回值相同(checkRep()函数保证了setTargets/setSources操作返回值是相同的,返回任意一个即可)。
3) public boolean remove(L vertex):
先将edges集合中所有以vertex为源点/终点的边全部移除,然后根据hashSet类中remove()方法的定义及返回值,函数只需返回vertices.remove(vertex)即可。
4) public Set<L> vertices():
为保证数据信息不泄露,需要返回一个由vertices集合复制的新集合。
5) public Map<L, Integer> sources(L target):
函数需要返回所有直接指向target的点以及与之对应的边的权值。这里规定了返回值的类型为Map,故只需要生成一个Map然后遍历edges集合寻找所有以target为终点的边并将其源点作为键名、权值作为键值添加到新生成的Map中,最后返回该Map即可。
6) public Map<L, Integer> targets(L source):
函数需要返回所有source直接指向的点以及与之对应的边的权值。这里规定了返回值的类型为Map,故只需要生成一个Map然后遍历edges集合寻找所有以source为源点的边并将其终点作为键名、权值作为键值添加到新生成的Map中,最后返回该Map即可。
7) public String toString():
函数需要将图的所有信息打印出来,即所有的点与边的信息。故先将所有的点的名称打印出来,随后遍历edges集合,循环调用Edge类的toString方法即可。
3.1.4 Problem 3: Implement generic Graph<L>
这一部分要求我们将原有的Graph<String>转化为泛型定义,注意同时要修改empty的方法。
3.1.4.1 Make the implementations generic
我们可以在声明中改为Graph<L>即可
3.1.4.2 Implement Graph.empty()
3.1.5Problem 4: Poetic walks
3.1.5.1 Test GraphPoet
对程序进行测试,分为三种情况:空文件、单段文字、多段文字进行测试,并检验是否符合预期结果。
3.1.5.2 Implement GraphPoet
- 在读取.txt文件中的所有的单词,并将所有相邻的单词作为点添加到图中,边权记录相邻单词对出现的次数
- 读取后检查图是否合法
- 对目标字符串进行扩充:
首先要遍历给定文本的左右的单词,对于每一个单词,我们要找出该单词对应的点相邻点权值加和最大的单词,将其添加在原来的两个单词之间。遍历结束后,返回扩充后的字符串。
3.1.5.3 Graph poetry slam
3.1.6使用Eclemma检查测试的代码覆盖度
覆盖率较高,测试均被通过,符合预期结果。
3.1.7 Before you’re done
在这里给出你的项目的目录结构树状示意图。
3.2 Re-implement the Social Network in Lab1
我们需要利用在P1中已经实现的Graph类来实现我们在第一次实验中所设计的人际关系实验P3。我们不能改变现有的规约,而只能在此基础之上,利用Graph为我们提供的方法来进行设计。
相似的,我们应该首先确定Person的定义,其次在确定FriendshipGraph的具体方法,最后通过广搜来确定distance的大小。
3.2.1 FriendshipGraph类
我们将关系图声明为一个Graph类型的变量,由于已经泛化,我们可以将其中存储想要的Person类成员。首先先初始化,生成一个空图。
加点:不能重复,利用迭代器检查一遍
加边:利用graph类中的set方法,要注意这里是无向图,而我们的方法是有方向的,因此要将Target和Source互换调用两次,weight赋值为1即可。
返回graph的点的成员member:调用Graph.vertices()即可
最终是核心算法,利用广搜,可以计算最短路径,
3.2.2 Person类
首先我们为了保护数据,以下的数据均定义为private类型。
对于每一个Person类对象person,定义private final String name来储存他的姓名,其次用private Boolean isVisited来标记是否在搜索时被访问过。随后定义几个需要的观察者方法,来获取person的name和isVisited标记,并对标记初始化的函数initial和改变标记的函数visit。
3.2.3 客户端main()
实验要求直接使用Lab1中的main函数,运行一下观察结果的正确性。
3.2.4 测试用例
测试用例主要关注于FriendshipGraph中的函数,而main函数主要起了调用的功能,因此无需测试。
对于加点,我们要考虑在空图中加新点,在已有非空图中添加新点,新点的name已经出现会报错,并终止程序。
对于加边,我们要考虑边的起始点不能相同,权值必须是正的。
对于计算距离,我们要考虑图中有环、联通性等对于距离的影响,包括自己到自己的距离等等,测试各种可能出现的情况,并验证实际值是否与理论值相同。
测试覆盖度:
3.2.5 提交至Git仓库
如何通过Git提交当前版本到GitHub上你的Lab3仓库。
在这里给出你的项目的目录结构树状示意图。
请使用表格方式记录你的进度情况,以超过半小时的连续编程时间为一行。
每次结束编程时,请向该表格中增加一行。不要事后胡乱填写。
不要嫌烦,该表格可帮助你汇总你在每个任务上付出的时间和精力,发现自己不擅长的任务,后续有意识的弥补。
日期 | 时间段 | 计划任务 | 实际完成情况 |
3.28 | 15:00-17:00 | 完成graph两种实现 | 如期完成 |
3.29 | 18:00-20:00 | 完成poem部分 | 未按期完成,出现很多bug |
3.30 | 18:00-20:00 | 实现poem walk | 基本完成 |
4.3 | 13:00-15:00 | 重写FriendshipGraph | 进行顺利但是有点速度慢 |
中遇到的困难与解决途径
遇到的难点 | 解决途径 |
实验要求晦涩难懂,在实现功能之前直接让写测试有点无从下手 | 复习课上内容,深入理解ADT、OOT、AF等知识 |
测试要使代码的覆盖度接近100%,最开始不知道如何实现 | 应当对测试空间分类,根据已经划分好的测试空间来进行测试样例的编写 |
在处理poem语句的时候总是有问题 | Debug后发现忘记转化大小写,标点符号处理出错 |
在完成本实验的过程中,感受到了面向对象编程、泛型等的实际应用,同时对于Java语言的开发有了更多的理解。但是由于对java语言掌握的不是很好导致实验过程十分困难,进展缓慢,所以也在通过每次实验锻炼自己的开发能力。
6.2 针对以下方面的感受(必答)
1.面向ADT的编程和直接面向应用场景编程,你体会到二者有何差异?
面向ADT编程,具有很好的规范性,主要是通过各个模块之间互相调用来实现功能;直接面向应用场景编程,是利用各个方法之间的相互调用,以整个过程为单位进行编程。
2.使用泛型和不使用泛型的编程,对你来说有何差异?
泛型有着极好的容错性和可移植性,可以使我们不必考虑变量的类型对他们进行相同的操作。
但是泛型也会弱化变量的一些具体的、特有的功能和方法。
3.在给出ADT的规约后就开始编写测试用例,优势是什么?你是否能够适应这种测试方式?
如果先写代码再写测试,会受到编写程序的影响,陷入思维定势,很可能没有有效测出bug所在。我会逐渐适应的。
4.P1设计的ADT在多个应用场景下使用,这种复用带来什么好处?
避免了代码、程序的重复设计,大大地节省了软件的开发时间和成本。
5.为ADT撰写specification, invariants, RI, AF,时刻注意ADT是否有rep exposure,这些工作的意义是什么?你是否愿意在以后编程中坚持这么做?
这些过程是对程序安全性、健壮性,设计框架的条理性与逻辑性的保障,更加体现了面向对象编程的抽象与封装概念,在程序设计的过程中,我们应该保持这种习惯。
6.关于本实验的工作量、难度、deadline。
工作量较多,难度较难,deadline合理。
7.《软件构造》课程进展到目前,你对该课程有何收获和建议?
通过本课程,我已经逐渐地了解了面向对象编程的大体过程、框架与部分技巧。但对于大部分知识掌握不是很好,应用起来也很困难,要继续学习。