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.实验环境配置
环境:IntelliJ IDEA,jdk-1.8.0,Junit4
3.实验过程
请仔细对照实验手册,针对三个问题中的每一项任务,在下面各节中记录你的实验过程、阐述你的设计思路和问题求解思路,可辅之以示意图或关键源代码加以说明(但千万不要把你的源代码全部粘贴过来!)。
3.1Poetic Walks
完成Graph接口的两实现个类ConcreteEdgesGraph,ConcreteVerticesGraph ,实现Graph数据类型,同时可实现泛型化Graph<L>。
Graph接口要求实现add(添加新节点),set(添加新边),remove(移除节点),vertices(获得所有的节点集合),sources(target)获得以target为目标节点的边的起始节点,targes(source)获得以source为起始节点的边的目标节点。
在两个具体的实现类中也有不同于接口中的toString(转换成字符串)方法。
GraphPoet类是给定一组单词(文件输入),对于两个相邻的单词a和b,认为存在一条由a到b的有向边,通过Graph接口构造有向图。再给定一段由单词组成的句子,如果句子中两个相邻单词之间在Graph图中有一个中间单词则将中间单词插入到两单词之间(如果有多个则插入权重最大的那个)。
3.1.1Get the code and prepare Git repository
打开实验要求的代码提取GitHub仓库。直接在本地GitHub仓库用git clone 获取文件,截取其中的P1。
如图:
3.1.2Problem 1: Test Graph <String>
以下各部分,请按照MIT页面上相应部分的要求,逐项列出你的设计和实现思路/过程/结果。
静态方法测试:直接使用了他提供的两个测试方法,未作修改。Graph有两种实现方式,在测试时Graph接口中的empty()方法可以返回ConcreteEdgesGraph或者ConcreteVerticesGraph中的任意一个新对象。这里选择ConcreteEdgesGraph。
静态测试结果:
Graph中各个接口以及给出了规约,应首先在GraphInstanceTest类中编写测试策略如下:
按照对应测试策略可以写出各种测试方法
-
-
- Problem 2: Implement Graph <String>
-
以下各部分,请按照MIT页面上相应部分的要求,逐项列出你的设计和实现思路/过程/结果。
该部分要求用两种实现方式实现图的类。ConcreteEdgesGraph中需要编写两部分代码:ConcreteEdgesGraph类和Edge类。首先需要在ConcreteEdgesGraph类中按照Graph中的规约实现Graph接口,并重写toString类。再者是编写Edge类,需要考虑安全性,所以类中的属性需设为private final,类中的不提供set方法,get方法采用防御性拷贝。ConcreteVerticesGraph同理,具体实现在下述问题中再具体描述。
此外,因为考虑到该问题中需要修改为泛型,所以防御性拷贝不能直接用具体类实现,应采用序列化方式进行拷贝。具体为在P1的包中增加deepclone类,内部只设置一个静态方法deepclone,代码如下:
只需要将想要拷贝的对象传入方法,返回值即为拷贝得到的新对象。此外,使用该方法时会产生编译时错误,故需要在使用处用try/catch捕获异常来使程序可以正常运行。
另外,P1中获取的原本代码不符合实验要求,应该对内部加以调整,调整后如下:
-
-
-
- Implement ConcreteEdgesGraph
-
-
Edge:在该问题中,需要先编写Edge类,内部属性如下:
AF、RI和Safety from rep exposure如下:
所以根据RI产生的checkRep函数如下:
任何可能改变值的位置都应用checkRep检查。
Edge类中,构造方法必须接收source、target和weight的初始值,getSource、getTarget、getWeight三个方法只需直接利用防御性拷贝返回对应的值即可。toString方法可以按照自己喜欢的方式输出边:
ConcreteEdgesGraph类:类中属性已经给出,AF、RI和Safety from rep exposure如下:
根据RI产生的checkRep如下:
add方法需要先检测输入点是否为空,再检测图中是否已经有待添加点,否则将点vertex直接加入点集vertices:
set方法需要检测几项标准:输入点不能为空、weight小于0,数据必然非法、source和target必须同时存在、起始点和终点之间是否有边、有边或无边情况下weight为0还是大于0,合法的标准组合为(起点和终点都在图中&&(起点和终点有边且weight>=0 || 起点和终点之间无边且weight>0)),不符合该条件的均为非法,返回-1,其余情况按spec规约编写即可,关键代码如下:
remove方法需要检测要删除的点是否在图中,如果在图中则删除该点,以及在边集edges中与该点相连的边。需要注意的是,如果用增强for的方法遍历删除边,会产生错误,应该采用迭代器的方法遍历edges删除边,利用Iterator中的remove方法删除,否则会产生空访问错误;而我这里采用了List的removelf方法,删除所有符合条件的边,而且不会产生上述问题。关键代码如下:
vertices方法需要检测是否为空图,如果为空,直接返回新建的空Set即可,若不为空,利用防御性拷贝vertices返回即可:
sources方法和targets方法是一对功能相似的方法,所以两者的实现是几乎相同的。具体只需寻找以输入点为终点或起点的边,将相应数据存入Map返回即可,以sources方法为例:
toString方法用自己喜欢的方式输出图即可,如下:
补充:Edge类中方法的测试类和ConcreteEdgesGraph类中toString的测试方法需要写在ConcreteEdgesGraphTest类中,Edge类中方法测试策略如下:
按照测试策略编写测试方法即可。
-
-
-
- Implement ConcreteVerticesGraph
-
-
Vertex类:编写此问题时需要先实现Vertex类,AF、RI和Safety from rep exposure如下:
由RI可得checkRep如下:
getName、getOutEdg、getInEdge方法都是直接进行防御性拷贝并返回。
而类中设置了setOutEdge、setInEdge方法,与graph接口中set方法的逻辑类似,就是检查合法性后,如果有边就返回原来的权值,无边就返回0,替换边的操作也如出一辙:
toString方法还是依照自己的想法来输出:
ConcreteVerticesGraph类:
类中的属性只有点集,所以需要借助Vetex中对边的操作来完成各个方法的编写。AF、RI和Safety from rep exposure如下:
由RI得checkRep:
add方法仍然是检测输入点不能为空、检测图中是否已经有此点,如果没有,即可加入。
set方法中检测起点和终点不能相等、起点和终点必须都在图中,若合法则直接调用setOutEdge、setInEdge方法加边,出入边都需要添加,所以应该搜索起始点和终点,关键代码如下:
remove需要先检测删除点是否在图中,如果在图中则直接删除点,然后再调用setOutEdge和setInEdge函数删除所有和vertex相连的边:
vertices方法直接将所有点加入集合中即可,然后再次防御性拷贝返回。
source与target只需要找到对应的点,然后调用getInEdge或getOutEdge返回即可。查找如下:
toString还是按照自己的格式输出即可:
补充:不在Graph接口中的方法和类需要在ConcreteVerticesGraphTest类中额外补充,Vertex的测试策略如下:
按照测试策略编写即可。
-
-
- Problem 3: Implement generic Graph<L>
-
在该问题中,需要更新两个实现以支持任何类型的顶点标签,使用占位符L而不是String。完成转换后,所有实例方法测试都应该通过。
-
-
-
- Make the implementations generic
-
-
为了使图适用于泛型而不仅仅是String,需要对上述完成的代码做出一定的修改,首先就是修改几个类的声明:
声明修改之后,要把这四个类中一大批以String声明改为以L声明。
之后还要修改接口和测试类:
此外还需要修改、添加<L>这样的标签。
修改后进行测试的结果如下:
因为防御性拷贝的问题,测试类中没有测试大量的try/catch,导致覆盖率下降,但总体覆盖率在预期内,测试到位。
-
-
-
- Implement Graph.empty()
-
-
如下:
两个实现任选一个即可。
-
-
- Problem 4: Poetic walks
-
该问题中给定一个语料库,根据语料库中的文本生成一个单词图,然后给定一条语句输入,在图中搜索词之间的关系,自动补全语句中可能可以完善的部分。在语料库中,对每一个单词看作一个顶点,相邻的单词之间,建立一条有向边,相邻单词对出现的次数,作为这条有向边的权值。在输入信息补全时,对相邻单词A和B做检查,如果存在一个单词C,在图中可以由前一个单词A通过这个单词C到达单词B,那么就在A和B之间补全C,补全的优先级按照权值越大者优先。
因为在GraphPoet的描述中以单词为内容的点是可以有指向自己的边的,所以应对Graph的两个实现进行适当的修改,解除起点和终点不能相同的限制,具体到代码就是修改一部分checkRep和set方法的判断条件,经测试修改后Graph依旧可以正常运行。
-
-
-
- Test GraphPoet
-
-
测试策略:
根据该策略编写结果即可
-
-
-
- Implement GraphPoet
-
-
GraphPoet的AF、RI和Safety from rep exposure如下:
GraphPoet方法比较简单,只需要逐行读取语料库中的句子,然后再用根据空格分割句子存入字符数组即可,之后根据字符数组,利用Graph接口的方法依次保存在图中。字符数组中的前一个单词应在图中添加指向后一个单词的边,如果边已经存在,就将权值加一。部分代码如下:
Poem是要利用上一个方法利用语料库形成的图为输入的字符串添加桥词,如果输入字符串中的两个词在图中只相隔了一个点(即桥词),就找到那个可以使两个词在只途径桥词的前提下距离最大的桥词,如果没有就不添加。部分代码如下:
-
-
-
- Graph poetry slam
-
-
除了Main函数中已经给出的用例,个人可以提出全新的语料库mycurpos.txt进行诗歌的生成,代码如下:
结果如下:
-
-
- 使用IDEA自带工具检查测试的代码覆盖度
-
完成上述部分,各个部分的代码覆盖度如下:
-
-
- Before you’re done
-
利用git add和git commit指令将项目添加到本地,最后再利用git push上传到指定仓库。
idea生成的项目的目录结构树状示意图如下:
-
- Re-implement the Social Network in Lab1
基于Graph<L>的两种实现ConcreteEdgeGraph<L>或者ConcreteVerticesGraph<L>,重新实现Lab1中的社交网络,并对其进行测试。
可以在Lab1的基础上稍加修改,将代码中相似的部分替换为适用Graph中的方法即可。
-
-
- FriendshipGraph类
-
这里继承了Lab1中的代码思想,但是结合了Graph接口。
AddVertex方法直接调用Graph中的add方法即可:
addEdge同理,调用set方法,加入少量判断条件即可:
直接继承了Lab1中的做法,先将图中点和边按照Lab1中的形式存入friends,再加入部分判断条件即可。部分代码如下:
值得注意的是,因为Graph的两个实现中都使用了防御性拷贝,所以friends中的点、边和原本Person点可识别性各不相同,判断是否包含便不能直接利用cointains,应该使用遍历结合equals方法来判断。
-
-
- Person类
-
给出你的设计和实现思路/过程/结果。
Person继承了Lab1中的Person类,只需要存储名字和获取名字即可。但是因为Graph的实现中判断相等用的是equals方法,所以Person类中应该重写equals方法,代码如下:
此外,P1中使用的序列化拷贝方法,被拷贝的类型需要实现Serializable接口,否则会报错,所以应将类的声明改成:
-
-
- 客户端main()
-
Main方法直接使用Lab1中的即可,在此处将main放入FriendshipGraph类中: