哈工大2021软件构造lab2 实验总结

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 并据此设计测试用例。

2. 实验环境配置

从实验指导提供的链接中创建Lab2个人仓库,接着在本地使用git clone命令克隆远程仓库。
进入克隆的远程仓库,将默认分支main改为master:
git checkout -b master //新建并进入本地分支master
git push origin master:master //向远程仓库推送新分支master
进入github官网,修改默认分支为master,返回git bash,输入命令行:
git push origin :main //删除远程main分支
git branch -d main //删除本地main分支

3.实验过程

3.1 Poetic Walks

本问题集的目的是练习设计、测试和实现抽象数据类型(ADT)。这个问题集侧重于实现可变类型,其中提供了规范。
 
Java集合框架中提供了许多有用的数据结构,它们是objects类型的集合,如:lists, maps, queues, sets等。其中没有提供图的数据结构。该任务将实现图类型,并实现该类型在若干场景下的应用。
 
Problems 1-3: 实现一个用于表示顶点标签化的可变有向带权图的抽象数据型Graph。该类型的图具有如下的属性:
1) 可变图:边与顶点可被添加与移除
2) 有向边:图中的每条边由源点指向目标点
3) 带权边:每条边带有一个正整数权值
4) 标签化顶点:顶点由某种可变类型标识区分,例如顶点可能具有String类型的命名(names)和Integer类型的编号(ID)
在该问题集中,我们将用两种不同的表示分别实现Graph来练习选择抽象函数,维护表示不变性和防止表示泄露。
 
Problems 4:实现图类型后,我们将实现GraphPoet类,该类利用一个词系图产生诗节。我们实现的“诗人”将能够接收一串文本,而后生成一段具有某种特殊形式的诗节。

3.1.1 Get the code and prepare Git repository

将从给定地址中下载的初始代码文件按照提交文件目录要求导入Eclipse新建的工程目录下。
Git仓库使用从实验指导书中链接克隆得到的仓库。
 

3.1.2 Problem 1: Test Graph <String>

这一部分主要是设计、记录和实现Graph<String>的测试。
首先,我们只需测试(而后实现)带有String类型顶点标签的图。稍后,我们将扩展到其他类型标签。

为了在多种Graph接口的实现上兼容运行我们的测试,以下给出测试步骤:

  1. 测试策略与对Graph.empty()静态方法的测试在GraphStaicTest.java中。由于该方法为静态方法,将只有一种实现方式,并且我们只需要运行一次测试。该部分测试已经给出,我们可以改变或添加内容,或不做改变。
  2. 在GarphInstanceTest.java中,对实例方法的测试使用emptyInstance()方法创建一个空图,并对Graph接口中的各个方法进行输入划分并测试。其中对起始空点集的测试testInitialVerticesEmpty()已经在GraphInstancesTest.java文件中给出。
    对Graph接口中方法的测试策略大致可以阐述如下:
    1) 将图划分为空图与非空图
    2) 将图中用于测试的顶点分为两组,一组已经存在于图中,另一组不存在于图中。
    3) 将图中用于测试的边根据端点在图中的存在性和边权的性质分组,根据端点存在性分为“两点存在”、“只有一点存在”和“两点不存在”三组;根据边权的性质将边分为“边权为0”和“边权为正整数”两组;
    4) 测试方法的实现调用Graph接口提供的add(), remove(), set, sources, targets, vertices()等方法充分覆盖对输入集的划分。
3.1.3 Problem 2: Implement Graph <String>

1.对于ConcreteEdgesGraph,实验规定使用已提供的表示完成实现:
private final Set vertices = new HashSet<>();
private final List edges = new ArrayList<>();

对于Abstraction function, ConcreteEdgesGraph代表一个带权有向图,其中每个顶点的标签为非空字符串且两两不等;每条有向边两个端点不等;每条有向边具有正整数权值。
在这里插入图片描述
对于Representation invariant表示不变性,具体如下:
在这里插入图片描述
对于rep exposure的防护保障,ConcreteEdgesGraph的每个field由private和final修饰,且在对返回值的处理上进行了防御性拷贝(defensive copying),因此用户无法从类外修改类内表示。
在这里插入图片描述
2. 对于Edge class,我们可以自行定义规约和表示,但必须保证Edge是immutable的类。
首先设计Edge类的fileds。

private final String source;
private final String target;
private final int weight;

三个属性表示边Edge从顶点source指向顶点target,且具有正整数权值weight。

在接下来的设计中,Edge类包括一个包含Edge属性字段中三个参数定义的构造方法,表示不变性检查方法checkRep(),对三个fileds属性值的观察器方法get,对两条边两端顶点是否相同的判定方法EdgeEquals,以及将Edge表示为字符串的toString()方法。

对于Abstraction Function,Edge类对应的是“一条由source指向target的、带有正整数权值的有向边“。具体如下:
在这里插入图片描述
对于Representation invariant表示不变性,保证每一个Edge对象的source和target都不是null或空字符串,且weight是一个正整数。

对于rep exposure的安全性防护在于,Edge的每一个filed都用private与final修饰,且Edge内没有修改filed值的方法,且Creator和producer方法返回前都进行了defensive copying,用户不能从外部修改内部实现,保证数据表示不会泄露。

基于以上几点,编写checkRep()函数如下:

/**
* Check the rep invariant of Edge class
*/
private void checkRep() {
	final String s = source;
	final String t = target;
	assert(t != null && s != null);
	assert(s.equals(t) == false);
	assert(weight >= 0); 
}

3.完成Edge设计后,编写ConcreteEdegsGraphTest.java中对Edges中操作的测试。Edge类中主要定义了三个observer方法和相等判断方法EdgeEquals,以及重写的toString方法。三个observer方法分别是getSource, getTarget和getWeight。
 
对于三个get方法来说,没有输入参数,只需测试方法是否正确返回结果即可。对于EdgesEquals方法来说,该方法判断两个给定Edge是否具有相同的起终点,需要按照顶点和边权性质对输入划分:
在这里插入图片描述
对于toString()方法来说,需要测试方法返回的字符串是否具有给定的格式:
在这里插入图片描述
4.完成上述步骤后,继续完成Edge与ConcreteEdgesGraph的实现。
Edges的实现主要在于对EdgeEquals方法的实现。我们认为两个Edge对应有向边相同当且仅当二者的source和target分别相同(此处不对weight判断相同,因为对weight的更改实际上只需要满足端点相同即可)。

对于ConcreteEdgesGraph的实现主要在于对Graph<L>接口中提供方法的实现。

  1. add方法
    该方法首先判断ConcreteEdgesGraph类的vertices集合中是否包含待添加节点,若不包含,则将其加入图中,并检查ConcreteEdgesGraph表示不变性,返回true;若包含,打印提示插入失败信息并返回false。
  2. set方法
    首先通过weight是否大于0判断对边的操作是添加还是修改或删除。
    若weight参数大于0,判断vertices集合中是否已经包含target与source参数,若不含其中任何一个,将不含的点加入图中,并检查表示不变性。而后遍历edges中每一条有向边,调用Edge.EdgeEquals方法查找edges集合中是否包含相同顶点边,若找到则移除后添加新权值边,返回之前边权;若没有找到则添加新权值边,返回0。若原本edges集合为空,添加参数边,检查表示不变性后返回0。
    若weight参数等于0,同样判断vertices集合中是否已经包含target与source参数,若不含其中任何一个,提示移除错误信息,返回0;否则遍历edges集合,查找对应边将其移除edges集合,并返回边权。若找不到对应边,打印错误信息并返回0.
  3. sources方法
    遍历边集edges返回以target为目的点的所有source点
  4. targets方法
    遍历边集edges返回以source为起始点的所有target点
  5. toString方法
    重写object类的toString方法,并加强了规约,生成字符串描述ConcreteEdgesGraph的顶点信息和边属性。
  6. vertices方法
    返回vertices点集的一个拷贝
  7. remove方法
    移除顶点vertex。需要注意的是,在点集中移除该点后,需要遍历edges边集移除所有与之有关的有向边。

5.运行ConcreteEdgesGraphTest测试
直接运行Junit测试,并利用EclEmma测试覆盖率,测试结果如下图所示:
在这里插入图片描述
在这里插入图片描述

3.1.3.2 Implement ConcreteVerticesGraph

1.对于ConcreteVerticesGraph,要求用以下表示实现且不能增加新的fields:
private final List vertices = new ArrayList<>();
Abstraction function,Representation invariant, Safety from rep exposure如下图:
在这里插入图片描述
2. 对于Vertex,需要保证自行设计的规约使得vertex class为mutable类型。Vertex类包含三个字段,分别表示顶点标签,有向边(源点,边权)集合,(目的点,边权)集合。

private final String label;
private final Map<String, Integer> sources = new HashMap<>();
private final Map<String, Integer> targets = new HashMap<>();

除构造方法之外,vertex还包含三个getter方法,用来访问vertex类型的三个字段,以及两个setter方法,用以改变vertex类的source集合与target集合,维护到达顶点与从顶点出发的边集与边权。Vertex还包含重写的toString(),用于将vertex转化为可读的字符串。

  1. 编写ConcreteVerticesGraphTest中对Vertex类的测试。首先,编写总体的测试策略,即分割输入参数的方法。对于顶点,分别使用不同字符串标签、尝试重复创建顶点用于测试;对于有向边,区分边权为0与正整数用于测试;测试其三个getter方法与两个setter方法,以及toString方法。
  2. 完成上述步骤后,分别完成Vertex类和ConcreteVertexGraph类。
    其中Vertex主要实现的是两个setter方法。

对于setSource方法,接收String类型参数source,int类型参数weight。当weight为0时,在sources集合中删除与source有关的条目,即删除以source为出发点,以当前Vertex为target的有向边。若weight>0,向sources中修改以source为起点,以当前Veretx为target,以weight为权值的有向边,并返回之前边的权值(如果存在)。

SetTarget方法可同理实现。

对于ConcreteVerticesGraph方法,主要实现其继承自Graph<L>接口的几个函数:

  1. add方法
    简单判断vertices中是否包含vertex点,若包含则打印信息后返回false;若不包含则添加该点入vertices顶点集合。
  2. set方法
    与ConcreteEdgesGraph不同,在该类中判断边存在后,直接对两个端点实施更改(注意vertex的mutable性质),并返回先前边的权值(如果存在,否则返回0);
  3. remove方法
    确认需要删除的点在点集vertices中存在后,除了在点集vertices中删除该点,还需要遍历vertices中其他点,调用set删除与被删点有关的有向边。若修改成功返回true。
  4. vertices方法
    返回图中顶点集合的拷贝
  5. sources方法
    首先简单查找vertices中目标点的存在性,若存在则返回以该点为target的有向边source顶点集合与边权。
  6. targets方法
  7. 首先简单查找vertices中目标点的存在性,若存在则返回以该点为source的有向边target顶点集合与边权。
  8. toString方法
    将ConcreteVericesGraph转化为可读的字符串,其中描述了顶点集合信息与各顶点关联有向边信息。

5.运行ConcreteVerticesGraphTest测试
直接运行Junit测试,并利用EclEmma测试覆盖率,测试结果如下图所示:
在这里插入图片描述
在这里插入图片描述

3.1.4 Problem 3: Implement generic Graph<L>

将已有两个Graph<String>的实现改为基于Graph<L>的实现,使用eclipse中文本替换工具即可。

3.1.4.1 Make the implementations generic

将所有的实现全部改为泛型实现即可,在更改结束后,重新运行ConcreteEdgesGraphTest和ConcreteVerticesGraphTest两个测试,若Graph<String>在两个泛型实现下仍然可以通过,表示更新成功。

3.1.4.2 Implement Graph.empty()

1.Graph.empty返回一个Graph接口的具体实现即可。例如,可以选择ConcreteEdgesGraph作为返回对象实现Graph.empty:

public  static <L> Graph<L> empty() {
	return new ConcreteEdgesGraph<L>();
}

2.GraphStaticTest使用不同的L泛型进行测试。由于基本数据类型都是Immutable类,可以采用对Long与Integer类型进行测试。具体测试结果如下:
在这里插入图片描述
在这里插入图片描述

3.1.5 Problem 4: Poetic walks

该部分任务主要要求实现图类型后,实现GraphPoet类,该类利用一个词系图产生诗节。我们实现的“诗人”将能够接收一串文本,而后图中的“bridge words”嵌入到输入文本中生成一段具有某种特殊形式的诗节。

3.1.5.1 Test GraphPoet

首先设计,记录,实现GraphPoetTest.java中对GarpPoet的测试。测试策略主要是对输入的文本进行分类。按照输入文件行数将文件分为空文件、单行文件、多行文件;按输入参考诗词结构构成将input字符串分为空串、单一词串、多词串;按GraphPoet构成分为空图和非空图。同时还需要对实验指导中的两个实例做测试。根据以上策略设计测试相应测试文件。

3.1.5.2 Implement GraphPoet

根据GraphPoet.java中给出的规约,Graphpoet由跟定的文本语料库生成,利用给定语料库生成一个词关联图。图中的顶点由单词标记,一个单词被定义为一个大小写不敏感的、不含空格的单行非空字符串。单词在语料库中由空格、换行符或文件结尾分割。图中的边计算连接次数:图中由w1指向w2节点的有向边边权即为文本中w2紧邻出现在w1之后的次数。

给定一个输入字符串,GraphPoet尝试在输入字符串的每一对连续词语中间插入一个桥接词来产生一个诗节。一个在单词w1与w2之间的桥接词是某个单词b,使得在图中所有w1到w2的、经过恰好两条边的路径中,w1->b->w2具有最大的权值。若图中没有这样的路径,该对单词之间将不会插入任何单词。在输出的诗节中,输入单词保持原来的大小写,桥接词全部小写。任何两个单词之间的空格都是单一空格。

GraphPoet调用Graph.empty()方法产生一个初始的空图:

private final Graph<String> graph = Graph.empty();

对于GraphPoet的Abstraction function, GraphPoet对应的是一个由语料库生成的词关联图,其中顶点大小写不敏感,且有向边权进行顺序邻接计数。

对于Representation invariant,GraphPoet为一个非空图;

对于Safety from rep exposure,GraphPoet中所有fields由provate与final修饰,用户无法从外部修改graph内部表示,不会造成表示泄露。

除构造方法外,GraphPoet还包含poem方法与重写的toString方法。Poem方法接收一个String类型字符串,提取顺序连接单词后在词关联图中查找桥接词并插入源字符串。需要注意的是,为避免重复内存复制,方法使用StringBuilder产生最终输出。重写的toString方法使用特定可阅读格式描述Graphpoet的节点信息与有向边信息。

运行GraphPoetTest测试,测试结果如下:
在这里插入图片描述
代码覆盖度测试结果如下:
在这里插入图片描述

3.1.5.3 Graph poetry slam

该部分任务主要是更新Main.java中main方法,给定一个输入语料库和测试诗词,将测试诗词进行扩展。

3.1.6 Before you’re done

进入本次保存的仓库,打开git bash,依次输入如下命令:
$git add .
$git commit -m “finish coding”
$git push
在这里插入图片描述

3.2 Re-implement the Social Network in Lab1

该任务主要基于3.1节Pietic Walks中定义的Graph<L>及其两种实现,重新实现Lab1中3.3节的FriendshipGraph类。

在本届FriendshipGraph中,图中的节点仍须为Person类型。故新的FriendshipGraph类要利用3.1节已经实现的ConcreteEdgesGraph<L>或ConcreteVerticesGraph<L>,L替换为Person。
根据Lab1的要求,FriendshipGraph类中应该提供addVertex()、addEdge()和getDistance()三个方法:针对addVertex()和addEdge(),需要尽可能复用ConcreteEdgesGraph<L>或ConcreteVerticesGraph<L>中已经实现的add()和set()方法;针对getDistance()方法,基于选定的ConcreteEdgesGraph<L>或ConcreteVerticesGraph<L>的rep来实现,而不能修改其rep。

根据实验要求,不变动Lab1的3.3节给出的客户端代码。重新执行在Lab1中缩写的Junit测试用,要求测试在本实验里重新实现的FriendshipGraph类仍然表现正常。

3.2.1 FriendshipGraph类

FriendshipGraph类继承ConcreteEdgesGraph,包含addEdge(), addVertex(), getDistance()三个方法。

1) addEdge方法
判断传入的两个Person非空且包含标签(label)不同的情况下,在vertices顶点集合中查找顶点标签,若图中不存在其中某点标签则将其添加入图中,而后调用ConcreteEdgesGraph中的set方法,加入所需有向边。

2) addVertex方法
首席在顶点集合vertices()中查找待加入Person节点对应标签是否已经存在,若是,则打印提示信息并返回false;若不存在,则将P加入图中,返回true。

3) getDistance方法
该方法采用广度有限搜索来获取给定两点之间最短路径长度。设置两个局部变量记录各点对应距离以及访问标记。需要注意的是,开始搜索前,应该先判断输入顶点的合法性,即二者是否在图中存在。

3.2.2 Person类

Person类包含一个String类型字段,用以标记顶点标签。

private final String name;

其中还包含两个方法,分别是构造方法与getName方法。两个方法均使用defensive copying,以防止表示泄露。

3.2.3 客户端main()

客户端代码沿用Lab1中样例代码。使用若干不同标签的Person节点插入图中,并添加若干条有向边,获取不同节点之间的最短距离。

3.2.4 测试用例

测试策略主要是根据FriendshipGraph中图的类型与结构进行测试,主要分为空图的测试、非空图的测试,对顶点划分为图中顶点与非图中顶点,划分有向边为图中有向边与非图中有向边。

主要对FriendshipGraph三个方法进行测试。
对addEdge方法的测试,根据有向边端点在图中的存在性划分输入;以及根据边的存在性划分输入。

对getDistance方法测试,根据所求距离两端顶点在图中的存在性、互异性以及路径的存在性划分输入。

运行FriendshipGraphTest.java测试结果:
在这里插入图片描述
在这里插入图片描述

3.2.5 提交至Git仓库

进入本次保存的仓库,整理文件目录,打开git bash,依次输入如下命令:
$git add .
$git commit -m “finish P2”
$git push
在这里给出你的项目的目录结构树状示意图。
在这里插入图片描述

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

(1) 对泛型类型的使用不够熟悉: 通过网络资料及博客学习
(2) JUnit测试报错无法解决: 复制报错信息,查阅相关文档
(3) 编程速度较慢: 先想后写,先测试后实现

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

6.2 针对以下方面的感受

(1) 面向ADT的编程和直接面向应用场景编程,你体会到二者有何差异?
(2) 使用泛型和不使用泛型的编程,对你来说有何差异?
(3) 在给出ADT的规约后就开始编写测试用例,优势是什么?你是否能够适应这种测试方式?
(4) P1设计的ADT在多个应用场景下使用,这种复用带来什么好处?
(5) 为ADT撰写specification, invariants, RI, AF,时刻注意ADT是否有rep exposure,这些工作的意义是什么?你是否愿意在以后编程中坚持这么做?
(6) 关于本实验的工作量、难度、deadline。
(7) 《软件构造》课程进展到目前,你对该课程有何体会和建议?

(1) 面向ADT的编程有一个非常明显的好处,就是ADT的可复用性对编程效率的极大提升。直接面向应用场景编程,虽然可能可以做到更为灵活的编程策略,但其中代码或函数的可复用性比起ADT而言没有显著优势,甚至在某些情况下由于可复用性不够好导致程序的后续开发和维护遇到困难。在ADT的复用中,不仅仅是像面向场景的编程的函数的复用,ADT能够复用数据结构和一系列方法,就像c语言,多个程序复用性更低;复用的好处就是能够利用接口、泛型或者是其他的方式,能够通过较少的代码实现多个应用与功能;同时,复用也会使代码更加清晰。

(2) 使用泛型的编程极大地提高了开发的效率。在很多情况下,编写一个功能相同的方法,如果不使用泛型编程,将由于数据类型的不同重复开发很多代码版本,虽然可能不同版本代码之间相差很小。而使用泛型编程将极大改善由于类型不同导致功能函数版本冗杂的问题。

(3) 优势在于测试用例可以不依赖于ADT的具体实现对ADT的各项功能进行测试,确保了ADT对spec的遵循,同时将测试与具体实现隔离,使得具体实现以测试为优先,更能在开发过程中准确实现spec中规定的功能。并且保证了具体实现的灵活性,能够在保证功能测试正确的大前提下改变具体实现,有利于版本的快速迭代和性能的提升。
这种方式对我来说,刚开始接触还是比较不能理解的。但是后来发现,如果在确定规约后先编写测试用例,可以不受具体实现的主观印象,只以功能为导向,具体实现时会更有目的性,也更能提升开发效率。

(4) 这种复用的好处在于将程序开发模块化,有利于程序版本的快速迭代,也更有利于程序的维护。

(5) 这么做的意义在于为开发者提供了一系列保证保障程序可用性、可靠性、安全性的准则,保障软件质量,提升开发效率。我十分乐意在以后的编程中坚持这种做法。

(6) 本实验的工作量、难度适中,有挑战但完成之后收获颇丰;deadline设置也较为合理。

(7) 整体来说很好,但是个别方面需要反复加深理解,希望老师以后可以讲清晰一些。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值