哈工大软件构造Lab2(2022)
(防扒小助手)
本人CSDN博客:
本人博客园博客(同步CSDN):
如果对你有用的话欢迎点赞关注哟!
目录
3.1.1 Get the code and prepare Git repository
3.1.3 Problem 2: Implement Graph
3.1.4 Problem 3: Implement generic Graph
3.1.6 Before you’re done
3.2 Re-implement the Social Network in Lab1
1、实验目标概述
本次实验训练抽象数据类型(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、实验环境配置
(1)安装测试用例代码覆盖度插件
经过网上查阅资料了解到,在IDEA中已经集成了代码覆盖度插件JaCoCo,切换方式如下:
点击Edit Configuration,弹出如下窗口:
在Code Coverage一栏点击Modify勾选红框中的选项,则会弹出切换代码覆盖度工具的选项:
通过咨询软件构造课程老师与助教老师,了解到使用IDEA自带的代码覆盖度工具即可,不需要切换到JaCoCo,因此下文代码覆盖度测试均使用IDEA自带的coverage runner进行测试。
2、GitHub Lab2仓库的URL地址(Lab2-学号)
略。
3、实验过程
3.1 Poetic Walks
该任务主要是通过实现一个图的模块来练习ADT的规约设计和ADT的不同实现。
(1)完善Graph接口类,并运用泛型的思想,将String拓展为泛型L类;
(2)实现Graph接口类:以边和点两种方式实现接口;
(3)利用实现的Graph类,应用图的思想,实现GraphPoet类。如果输入的文本的两个单词之间存在桥接词,则插入该桥接词;若存在多个单一桥接词,则选取边权重较大者。
3.1.1 Get the code and prepare Git repository
从要求文件中对应网址下载得到实验代码,建立好project,进入目录,打开Git bush
依次输入:
git init
git remote add origin git@github.com:ComputerScienceHIT/HIT-Lab2-120L022408.git
git pull origin master
git add .
git commit -m “init”
git push origin master
3.1.2 Problem 1: Test Graph <String>
测试Graph的静态方法。
为了方便测试Graph的多种实现,在 GraphInstanceTest 中测试了实例方法。
编写测试用例主要利用等价类划分的思想进行测试,测试策略如下:
分别编写覆盖以上条件的测试用例。
运行GraphStaticTest得到测试结果如下:
3.1.3 Problem 2: Implement Graph <String>
3.1.3.1 Implement ConcreteEdgesGraph
(1)Edge类实现
定义两个private String类型的变量source和target存放每个边的起止点
定义一个private int类型的变量weight保存这条边的权重(长度)
private final String source, target;
private final int weight;
关于AF,RI和rep exposure:
① 构造器 constructor
构造方法,使用上述三个数据域声明一个新的边
public Edge(L source_new, L target_new, int weight_new)
{
this.source = source_new;
this.target = target_new;
this.weight = weight_new;
checkRep();
}
② 检查表示不变量 checkRep
检查表示不变量,其中source和target必须非空,weight必须大于0
public void checkRep()
{
assert source != null;
assert target != null;
assert weight > 0;
}
③ get方法
get_Source:返回source域
get_Target:返回target域
get_Weight:返回weight域
④ toString方法
返回一个字符串表明这条边是从哪个source到哪个target,其weight是多少。
public String toString()
{
return source.toString() + "->" + target.toString() + "\t权重为" + weight + '\n';
}
(2)ConcreteEdgesGraph实现
vertices和edges分别记录当前graph所含有的点和边
private final Set<String> vertices = new HashSet<>();
private final List<Edge<String>> edges = new ArrayList<>();
关于AF,RI和rep exposure:
① add方法
public boolean add(String vertex)
如果顶点不为空,添加一个顶点。如果在vertices的Set集合中成功添加了vertex,则返回true。
② Set方法
public int set(String source, String target, int weight)
输入source,target,weight,确定一条有向边。
具体做法:如weight!=0,移去可能已经存在的相同起始点的边,然后加入新的边,如weight=0,寻找可能已经存在的相同起始点的边,删去。
③ remove方法
public boolean remove(String vertex)
从vertices中删去给定的vertex点,遍历edges,寻找该vertex是否为某条边的起点或者终点,删去相应的边。在使用迭代器遍历时要使用iterator.remove方法保证安全性。
④ vertices方法
public Set<String> vertices()
返回vertices集合。注意做到safety from rep exposure ,使用Collections.unmodifiableSet()方法。
⑤ sources方法
public Map<String, Integer> sources(String target)
参数:target。根据传入的target参数寻找以targe为终点的边。返回一个键值对为(点,权重)的map。
实现:建立一个map,利用迭代器遍历edges,如果某个edge的edge.get_Target()和传入参数target相等,则将该边的source和weight存入map中。
⑥ targets方法
public Map<String, Integer> targets(String source)
参数:source。根据传入的source参数寻找以source为起点的边。返回一个键值对为(点,权重)的map。
实现:建立一个map,利用迭代器遍历edges,如果某个edge的edge.get_Source()和传入参数source相等,则将该边的target和weight存入map中。
⑦ 检查表示不变量 checkRep
思路:n个点,最多构成n*(n-1)条有向边,因此存在这种不可变的数学关系
⑧ toString方法
对每条边调用toString方法,整合起来。
public String toString()
{
String s = "";
for (Edge<L> e : edges)
{
s = s + e.toString();
}
return s;
}
(3)ConcreteEdgesGraphTest测试
JUnit测试结果如下:
测
试覆盖率:
3.1.3.2 Implement ConcreteVerticesGraph
(1)Vertex类实现
定义两个private String类型的变量source和target存放每个边的起止点
定义一个private int类型的变量weight保存这条边的权重(长度)
private final String name;
private final Map<String, Integer> sources;
private final Map<String, Integer> targets;
关于AF,RI和rep exposure:
① 构造器 constructor
构造方法,传入参数name创建新的点。
public Vertex(String name)
{
this.name = name;
sources = new HashMap<>();
targets = new HashMap<>();
}
② 检查表示不变量 checkRep
检查表示不变性,各边weight的值应该永远大于0。
③ get方法
get_Name:返回source域
get_Sources:返回weight域
get_Target:返回Targets域
④ set方法
set_Target:为当前点新增一个target,
如果weight为0,删去当前点的target,成功返回删去target的weight,不存在返回0;如果weight不为0,为当前点新增一个target,长度为weight,如果该点已存在,返回旧的weight,否则返回0
set_Source:为当前点新增一个source,
如果weight为0,删去当前点的source,成功返回删去source的weight,不存在返回0;如果weight不为0,为当前点新增一个source,长度为weight,如果该点已存在,返回旧的weight,否则返回0
⑤ remove方法
remove_Source:删去当前点的指定source
public int remove_Source(String source)
{
Integer weight = sources.remove(source);
return weight == null ? 0 : weight;
}
remove_Target:删去当前点的指定target
public int remove_Target(String target)
{
Integer weight = targets.remove(target);
return weight == null ? 0 : weight;
}
⑥ toString方法
返回一个字符串表明这个顶点的信息
public String toString()
{
return String.format("Vertex %s has %d sources and %d targets", this.get_Name().toString(), this.get_Sources().size(), this.get_Targets().size());
}
(2)ConcreteVerticesGraph实现
使用如下数据类型保存顶点的数据:
private final List<Vertex<String>> vertices = new ArrayList<>();
关于AF,RI和rep exposure:
① 检查表示不变量 checkRep
所有点的标识不能为空
private void checkRep()
{
assert vertices != null;
}
② add方法
public boolean add(String vertex)
参数:vertex,判断vertices中无重复点就加入
③ Set方法
public int set(String source, String target, int weight)
参数:source, target, weight。先将可能不在vertices中的source点和target加入vertices。随后遍历vertices,找到source对它增加一个target,找到target为它增加一个source,并设置距离。
④ remove方法
public boolean remove(String vertex)
参数:vertex。遍历vertices,如果当前点是vertex,删去(使用iterator.remove方法),如果不是,检查它的source和target是否包含vertex,如果有删去。
⑤ vertices方法
遍历vertices,找到每个点对应的string,添加进set即可。使用防御性拷贝:
public Set<String> vertices()
{
Set<String> set = new HashSet<>();
for (Vertex<String> v : vertices)
{
set.add(v.get_Name());
}
return set;
}
⑥ sources方法
public Map<String, Integer> sources(String target)
参数:target。根据传入的target参数寻找以targe为终点的边。返回一个键值对为(点,权重)的map。
实现:建立一个map,利用迭代器遍历edges,如果某个edge的edge.get_Target()和传入参数target相等,则返回target对应的源点图。
⑦ targets方法
public Map<String, Integer> targets(String source)
参数:source。根据传入的source参数寻找以source为起点的边。返回一个键值对为(点,权重)的map。
实现:建立一个map,利用迭代器遍历edges,如果某个edge的edge.get_Source()和传入参数source相等,则返回source对应的目标点图。
⑧ toString方法
打印当前顶点图的顶点数量:
public String toString()
{
return String.format("This graph has %d vertices", this.vertices.size());
}
(3)ConcreteEdgesGraphTest测试
JUnit测试结果如下:
测试覆盖率:
3.1.4 Problem 3: Implement generic Graph<L>
3.1.4.1 Make the implementations generic
将具体类的声明更改为:
public class ConcreteEdgesGraph<L> implements Graph<L> { ... }
class Edge<L> { ... }
和
public class ConcreteVerticesGraph<L> implements Graph<L> { ... }
class Vertex<L> { ... }
更新两个实现以支持任何类型的顶点标签,使用占位符L代替String。
充分利用IDEA的智能改错功能快速修改成泛型实现。
3.1.4.2
Implement Graph.empty()选择ConcreteEdgesGraph来实现Graph.empty()
测试全部通过:
3.1.5 Problem 4: Poetic walks
3.1.5.1 Test GraphPoet
关于测试策略:
具体测试:
3.1.5.2
Implement GraphPoet首先声明:
private final Graph<String> graph = new ConcreteEdgesGraph<String>();
关于AF,RI和rep exposure:
① 检查表示不变量 checkRep
所有点的标识不能为空
private void checkRep()
{
assert graph != null;
}
② GraphPoet方法
参数:corpus文件路径。打开文件,读取文件输入,识别序列,构建图结构。
具体:利用BufferedReader.readLine方法读取全部输入后用string.split以空格划分,保存在数组中,随后每次取相邻元素,在图中新增边。
③ poem方法
参数:input。
利用相同方法分割输入字符串,声明一个StringBuilder保存返回结果。每次读取一个词,然后以当前词为source,下一个词为target,在graph中寻找符合此条件的边,记录权值,结束后选择权值最大的,利用StringBuilder. Append方法,将节点名字加入字符串。
④ toString方法
调用ConcreteEdgesGraph的toString方法,输出图结构
public String toString()
{
return graph.toString();
}
3.1.5.3
语料库为泰戈尔经典名句集锦
输入输出如下:
3.1.6
Before you’re done通过Git提交当前版本到GitHub上你的Lab2仓库。
git add .
git commit -m "P1 Finished"
git push -u origin master
项目的目录结构树状示意图。
3.2
Re-implement the Social Network in Lab1这个实验是基于在Poetic Walks中定义的Graph及其两种实现,重新实现Lab1中的 FriendshipGraph类。我们需要尽可能复用ConcreteEdgesGraph或 ConcreteVerticesGraph中已经实现的add()和set()方法,而不是从零开始。另外基于所选定的 ConcreteEdgesGraph 或 ConcreteVerticesGraph的rep来实现,而不能修改父类的rep。
3.2.1 FriendshipGraph类
(1)设计思路
继承ConcreteEdgesGraph<Person>,并在类中增加一些对非法情况的判断,例如顶点已经存在,边已经存在或找不到对应顶点等。
public class FriendshipGraph extends ConcreteEdgesGraph<Person>
(2)方法实现
构造一个ArrayList类型的变量person_list存储顶点列表
private final ArrayList<String> person_list = new ArrayList<String>();
① public boolean addVertex(Person people)
这个函数是为把参数添加到图中,作为图的一个顶点,直接调用父类的this.add()即可。调用过程中检查顶点列表中是否已出现参数对应的顶点,若重复则打印错误信息并返回false,成功添加顶点则返回true
② public boolean addEdge(Person people1, Person people2)
构建图的要素,在图中添加边。先调用 this.vertices().contains()方法来判断所添加边的顶点是否存在,再判断两顶点之间是否已有边连接,若条件满足,则调用this.set()方法设置边,权重初始化为1并返回true,其余情况返回false。
③ public int getDistance(Person People1, Person People2)
获取两个顶点之间距离的函数,题目要求返回最短距离,因此采用广度遍历的方式,此处需要用到Queue的数据结构,并且设置了一个List来存放已经访问过的person。
3.2.2
Person类Person类根据FriendshipGraph类的需求编写的。它用于描述每个成员的性质,主要是实例化姓名的构造方法,getName()方法,判断姓名是否重复的isSameName方法。
public class Person
{
private final String Name;
public Person (String Name)
{
this.Name = Name;
}
public String getName()
{
return this.Name;
}
public boolean isSameName(String Name)
{
return this.Name.equals(Name);
}
}
3.2.3
客户端main()main函数主体内容即为实验指导书给定的内容:先new一个FriendshipGraph类的对象,然后添加顶点,添加边。
输出错误类型在实现FriendshipGraph类时已输出,故此处不需要再次判断是否出现错误。
① 正常输出的测试结果
② 注释掉rachel -> ross后的测试结果
3.2.4 测试用例
测试策略:
根据划分的等价类设计测试用例
测试结果与覆盖度报告:
3.2.5 提交至Git仓库
通过Git提交当前版本到GitHub上的Lab2仓库。
git add .
git commit -m "P1 P2 first finished"
git push -u origin master
本项目的目录结构树状示意图:
4 实验进度记录
请使用表格方式记录你的进度情况,以超过半小时的连续编程时间为一行。
日期 |
时间段 |
计划任务 |
实际完成情况 |
2022-05-23 |
8:00-11:30 |
浏览报告,查看MIT相关内容要求 |
按计划完成 |
2022-05-23 |
15:00-19:00 |
完成P1的GraphStaticTest类测试 |
按计划完成 |
2022-05-24 |
9:00-11:00 |
完成P1的ConcreteEdgesGraph类编写 |
按计划完成 |
2022-05-24 |
15:00-20:00 |
完成P1的ConcreteVerticesGraph类 |
按计划完成 |
2022-05-25 |
8:15-11:00 |
实现泛型Graph<L>的转换 |
按计划完成 |
2022-05-25 |
13:00-14:00 |
浏览Poetic Walks的编写要求 |
按计划完成 |
2022-05-25 |
21:00-23:00 |
尝试实现Poetic Walks |
遇到困难,延期完成 |
2022-05-26 |
9:00-17:00 |
完成Poetic Walks的test和Implement |
按计划完成 |
2022-05-27 |
8:00-18:00 |
完成P2的Social Network的改写 |
按计划完成 |
5 实验过程中遇到的困难与解决途径
遇到的难点 |
解决途径 |
不了解IDEA如何实现代码覆盖率测试
|
查阅网上资料后发现相应解决方案因IDE版本迭代已发生改变。自己摸索解决了实现代码覆盖率测试插件切换的问题。 |
对面向test的编程思想理解不够深入
|
复习《软件构造》课程的PPT,上网查阅了相关资料并咨询了同学,完成了对测试代码的编写。 |
对规约的要求不够理解
|
通过学习模仿,尝试自己编写相应规约并实现之。 |
6 实验过程中收获的经验、教训、感想
6.1 实验过程中收获的经验和教训
经验:
加深了自己对于泛型的理解和认识,提高了代码编写、ADT设计的能力。编写test测试文件时,有些方法的测试也能覆盖到其他的方法,避免重复测试增加工作量。
教训:
在设计多个类并使之互相配合的方面做得不好,编写代码的逻辑性有待提高。
6.2 针对以下方面的感受
(1)面向ADT的编程和直接面向应用场景编程,你体会到二者有何差异?
我感觉,对于面向ADT的编程,类是其主要特点,程序执行过程中,先由主函数进入,定义一些类,根据需要,执行
,过程的概念被淡化了。而直接面向应用场景编程的抽象程度不高,虽然逻辑清晰但是代码思路混乱,不利于实现。
(2)使用泛型和不使用泛型的编程,对你来说有何差异?
泛型编程可以使代码被很多不同类型的对象所重用,并使代码具有更好的可读性。
(3)在给出ADT的规约后就开始编写测试用例,优势是什么?你是否能够适应这种测试方式?
优势是不考虑代码的内部实现,只需考虑是否完成了规约中指定的功能。作为java语言的初学者来说我很不适应这种测试方式。
(4)P1设计的ADT在多个应用场景下使用,这种复用带来什么好处?
提高了代码的利用率,减轻编程工作量。
(5)为ADT撰写specification, invariants, RI, AF,时刻注意ADT是否有rep exposure,这些工作的意义是什么?你是否愿意在以后编程中坚持这么做?
使编写的代码更加安全和可读性更强。愿意这么做。
(6)关于本实验的工作量、难度、deadline。
我认为,考虑到实验时间,与其他专业课的复习时间与考试时间有大量冲突,因此显得本实验工作量十分巨大,难度也很高,deadline十分紧张。
(7)《软件构造》课程进展到目前,你对该课程有何体会和建议?
希望减少工作量,增加课时安排,增加动手实验分数,减少笔试考试分数,合理安排课程开展时间,可以在刚开学时开课或者在小学期开课,更有利于学生能力的提升。