其他分享
首页 > 其他分享> > HIT-2022春软件构造-Lab2 实验总结

HIT-2022春软件构造-Lab2 实验总结

作者:互联网

这是对挺早之前结束的Lab2的实验内容和完成的总结说明,Lab2主要是针对ADT和OOP的一次练习。

写在前面,一点关于实验和这门课的感想:

之前写lab2的时候其实还是挺手生的(当时对java还是没什么掌握,对学习的内容可能并没有理解很深入,一些需要注意的点可能没有注意到),但是在作为一个新手写代码的时候感觉比较轻松,心里想的只是要怎样能够实现功能,填满代码空缺。同时,在写lab2的过程中,真的是很大程度上提高了我对java的熟悉程度,让我运用了一些课堂上学习的知识。对于这种偏工程的科目,实验真的很重要。

现在系统学习完课程之后,自己现在考虑如何写代码时也感到甚至比以前更难写了,主要是在学习完代码要追求可复用性,可维护性等之后,就难免开始想要追求更完善的代码,使用更好的设计模式,却因为学习的还不够深刻,不能灵活的运用这些知识,反而导致看代码和写代码时左思右想,畏首畏尾。总而言之,需要学习的东西还有很多,只有加深自己对知识的理解,不断进行代码实践,才能够提高自己的水平,更好的掌握这门技术。

我想,对于软件构造,这门课程也可能只是一个入门,但是在这门课程中,虽然我的学习还不够深入,但是我也深深感受到了软件构造这门课程的体系之完善,了解到了作为怎样的代码才是更加符合工程化要求的,以及java语言作为面向对象语言的魅力,点燃了我对软件工程的兴趣。

 

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 实验环境配置

配置开发环境:IntelliJ IDEA和测试环境Junit以及覆盖率测试插件。

1.IDEA:官网下载IntelliJ IDEA,并根据网上的教程官网下载JDK11并配置路径等。

2.Junit:根据老师给出的教程下载Junit的官方jar包,在File->Settings中选择Plugin,下载JunitGenerator,之后在other settings中选择junit进行配置。并且在IDEA中点击File->Project Structure->Project Libraries,将两个jar包加入。之后就完成了测试环境的配置。

3.覆盖率测试插件:由于我安装的是IDEA专业版,这个插件是自带的,故没有再进行配置。

3 实验过程

3.1 Poetic Walks

本实验的目的是训练设计,测试和实现ADT。Java并没有提供图的数据结构,因此我们的目的就是实现图,需要构造一个Graph<L>,(需要将ADT泛型化),实现添加或删除边功能,边为的加正权(正数,不能为0)有向边。在最后,我们将利用编写好的内容来实现GraphPoet,一个生成诗歌的类。编写的测试文件要有良好的测试覆盖率。

3.1.1 Get the code and prepare Git repository

在lab0创建仓库过程中,根据新建空仓库后github上的创建指令git init等初始化仓库并加入README.md文件。

之后将新建的仓库从github上clone到本地,首先在个人仓库获取url,之后打开git bash进入想要存放项目的目录,使用命令git clone [url]将项目克隆到本地目录下,接着就可以在idea中打开项目进行修改并提交了。之后在git bash中进入该目录会显示如下:

要求获取代码,这里由于无法打开外网,故直接获取课程提供的代码,代码不提供url,故选择直接下载,之后将文件复制到相对应目录下。

3.1.2 Problem 1: Test Graph <String>

 Problem1要求测试Graph<String>,这个测试在代码中已经写好了,即test/P1/graph中的GraphStaticTest,我们要做的是让它成功通过。观察直接运行时不通过的测试方法testEmptyVerticesEmpty,我们可以发现我们需要实现Graph接口中的empty方法。

补全empty()方法,这里考虑到后续需要使用泛型来实现所有ADT,故我们直接将String类型修改为泛型实现方法使用的<L>:

 

这里修改后会报错,需要将ConcreteEdgesGraph.java中的相关String也修改为<L>,比如类修改为如图:

ConcreteVerticesGraph.java中的可以先不用修改。

之后测试会发现还是会报错,分析报错信息,再次观察测试方法里的内容,发现我们还需要补充ConcreteEdgesGraph类中的方法vertices,让它返回一个存储vertices的HashSet(避免rep exposure),这样再进行测试,就可以成功通过了。

 

3.1.3 Problem 2: Implement Graph <String>

Problem2要求实现Graph<String>,这里我们可以直接实现Graph<L>。实验要求对于所有的类我们都需要写AF和RI文档,以及safety from rep explosure。我们还需要实现checkRep和重写toString。并且对于每个类,需要有清晰详细的规约,除了重写的方法每个类都需要有javadoc解释。

可以选择先写ConcreteEdgesGraph或ConcreteVerticesGraph。两个实现中不应该有依赖或共享代码。

3.1.3.1 Implement ConcreteEdgesGraph

1.实现Edge类:

在实现ConcreteEdgesGraph之前,首先需要实现Edge类,这一步还是比较轻松的。

考虑到后续泛型化的要求,这里直接使用泛型实现要求。按照代码中已经给好的大致结构进行函数和注释的填充。

(1)fields:三个field,分别为泛型变量source(起点),target(终点)和int类型的权重。

(2)撰写AF,RI和safe from exposure如下:

 

这里的AF可以直接用文字描述的,不需要严格按照上面这种类似公式化的形式来写

(3)按照要求编写constructor和checkRep。这里需要注意的是checkRep的编写,需要按照在(2)中所写的RI作为要求,实现对不为空和边权为正的检查。同时,在constructor的编写末尾也要加上checkRep。

(4)method:主要需要实现的method是三个变量的get方法,这里不设置set方法,在IDEA中可以直接右键generate中便捷生成get方法。

(5)重写toString方法:重写方法时需要注意加上@Override。实现时可以使用String.format方法来规范格式。

2.  实现ConcreteEdgesGraph类:

在编写具体类的时候,接口方法具体的实现内容,输入值与返回值都在官网文档中有给出(指导网站上有给出javadoc文档的网页,点进去就有了),要根据那个的标准来写,在两种实现中实现细节不同而已。

(1)这个类中需要实现的结构也已经提供好了。并且给出了规定的rep,之后需要使用的点集和边集,首先我们需要将它们都修改为泛型类型,如下:

 

      之后的String也都需要做这样的修改转为泛型实现,在后面不再赘述。

(2)接下来需要编写AF,RI和safety from rep exposure。需要注意的是,按照实验要求,本次实验要实现的图为有向加正权图,没有复边复点和空点。这些都需要在接下来的checkRep中得到实现,编写结果如下:

(3)在CheckRep中,需要按照要求实现RI中的要求,这里使用断言assert来判断每一条是否符合条件。因为实际编写的时候我是先写了一部分下面的方法,在其中就进行了对复边和复点的检验,所以后来编写checkRep的时候就没有验证这两个部分,而且在check中对整体进行这种验证比较麻烦,我认为直接在方法中实现也可以是一种选择。在checkRep编写之后,需要在所有对vertices和edges有所改变的操作之后调用checkRep来检查操作后的graph是否还符合条件。constructor内部没有什么需要实现的内容,不再叙述。

(4)method的编写:

       在编写method的过程中,为了之后看代码能较为清晰的了解这个函数的作用,大部分代码我都编写了注释规约。

1.add():

这个函数目的是向点集中加入点,首先需要使用contains检查是否vertices中是否含有这个点,已经有则返回false,如果还没有则直接调用vertices的add方法加入这个点,返回true。

2.set():

这个函数比较复杂,目的是在起点和终点之间加入边,如果输入的点不存在或边权为非正数,直接返回-1,之后遍历所有边,已经有这个边就删除这个边,之后添加新的边后返回删除的边的权重。如果原本没有边,直接添加边就返回0。这里的比较使用equals,主要是考虑到实际使用时泛型T可能是对象类型,使用equals可能更好。

理顺思路后函数的结构还是很清晰的:

 

3.remove():

这个函数的目的是删除一个结点以及其关联的所有边,返回删除结果,没有这个点则返回false。函数实现中唯一需要考虑的就是怎样删除看起来比较简洁美观,因为需要删除所有以这个点为起点和终点的边。一开始考虑的是使用循环来删除,这样做比较直观,但是后来在写到edges.remove方法的时候在IDEA自动生成的方法选项中看到了一个叫做removeIf的方法,(看起来就是能实现条件筛选的方法)经过上网的资料查询,发现这个方法能够筛选remove的对象,非常匹配当前的需求,因此可以删掉循环,直接这样实现:

 

         Java提供了很多很方便的方法,但很可惜的是如果没有提前了解可能就会因为不知道这些方法的存在而导致自己用比较笨拙的方法进行了麻烦的实现,就不能够很好地利用java的优点。但是这些方法太多了,全部记住的可能性显然不大,因此我认为IDE中提供的方法备选是很好的功能,写之前可以扫一下底下的各种方法,如果有看起来比较符合自己要求的说不定就可以省下一番力气。

4.vertices():

        这个方法就是返回边集,唯一需要注意的就是可以返回一个新的HashSet,使用防御性拷贝,可以减少表达泄露的风险。

5.Sources()和Target():

       这两个方法的思路和实现方法基本相同,写完一个后另一个的实现只需要小小的修改即可。Sources/targets要求找到所有以参数点target/source为终点/起点的边以及这些边的起点/终点,并以map<起点/终点,边权>的形式返回一个Map。实现并不复杂,只需要遍历边集找到这样的边进行操作即可。

6.toString():

        重写toString时我思考了怎样的实现比较好,因为想要返回一个有边集有点集的比较长和复杂的字符串,显然用一般的加号实现不太好。因此想到使用老师上课提到过的能够频繁添加内容的StringBuilder类型来实现。之后按照自己的设计进行对返回字符串的编辑即可,要注意对其中一些对象使用toString方法,如:

 

       需要注意的是最后返回的是这个StringBuilder对象调toString方法的结果。这个方法会返回一个新的String对象。

3.编写测试:

       在ConcreteEdgesGraphTest中编写测试,实现对ConcreteEdgesGraph类和Edge类中的各个方法的测试,自行设计测试即可(使用String类型测试):

测试结果如下:

 

4.测试覆盖率:

        进行ConcreteEdgesTest的测试率覆盖只能覆盖Graph中最多一半少一点的方法(因为Graph中有两种实现,这个只占一种,而且这种边实现编写的方法比点实现要少。结果如下):

  

       不过通过左侧目录的结果可以看到单独的对边实现的类的覆盖度。

3.1.3.2 Implement ConcreteVeriticesGraph

1.实现Vertex类:

Vertex类的实现思路与Edge类大体是相似的,但是要更加复杂一些。

同样,这里直接使用泛型实现要求。按照代码中已经给好的大致结构进行函数和注释的填充。

(1)fields:三个field,分别为泛型变量vertex(点),Map变量sources(入边点和权重)和targets(出边点和权重)。

(2)撰写AF,RI和safe from exposure如下:

(3)按照要求编写constructor和checkRep。checkRep主要通过检测点不为null实现。Constructor只接受参数vertex,其他边相关的操作调用后续方法进行。不在构造器中进行初始化。

(4)method:

1)三个变量的get方法:这里不设置set方法,在IDEA中可以直接右键generate中便捷生成get方法。getResources和getTargets使用防御性拷贝返回新的HashMap。

2)setSources和setTargets方法:这两个方法主要是用来为vertex添加以source为起点的入边和以target为终点的出边。这两个方法的编写其实可以直接参考在ConcreteEdgesGraph里的set方法,包括返回值的设计也是类似的。在之前方法的基础上这里没有太多难点,不再赘述其实现。

(5)重写toString方法:我在设计Vertex的ToString方法时重新设计格式,采取分别展示入边和出边的形式(如果没有边显示null)。重写方法参考EdgesGraph中的StringBuilder来实现(因为字符串格式较为复杂)。这里简单列举出边的String格式:

2.  实现ConcreteVerticesGraph类:

(1)这个类中需要实现的结构也已经提供好了。并且给出了规定的rep,之后同样将Vertex改为泛型类的格式。其他String也都需要做这样的修改转为泛型实现。

(2)接下来需要编写AF,RI和safety from rep exposure。这里的要求和边实现方法类似。

(3)在CheckRep中,需要按照要求实现RI中的要求。这里同样是没有验证复边和复点,理由同之前。只需要遍历List中的每个Vertex,查看它们的入点和出点是否都在List所包含的vertex中即可

(4)method的编写:

1.add():已经有这个点返回false,没有这个点的话则在vertices中加入一个new Vertex,用传入的vertex作为构造器的参数。

2.set():

Set函数的结构和思路和之前大体相同。不过在函数的编写过程中一开始由于我没有考虑全面,主体结构是遍历vertices,找到source代表的Vertex实例(if判断条件是这个Vertex的vertex是否是source),再调用Vertex类的方法查看target是否存在,存在则删除,之后再添加<target, weight>进入Vertex source的targets(Map)。但是后来编写toString测试之后发现存在问题,可以正常显示set的出边,但是没有入边,后来经过思考发现是set方法编写有问题,之前的写法并不全面,没有在为source添加target的同时为target代表的点添加source,导致set时边对于起点和终点的Vertex都有相应的Map需要改动,我写的方法却只改变起点内的target Map。找到问题之后解决方案即是仿照之前的实现主体再添加找到target后的sources的改变即可,最终主体实现如下(省略了部分开头结尾的代码):

 

4.remove():

这个函数的目的是删除一个结点以及其关联的所有边,返回删除结果,没有这个点则返回false。实现时先通过removeIf在vertices中删除这个点(If判断名字相同),之后遍历vertices,直接将所有sources和targets中含有这个点的入边/出边调用remove方法删除。(remove传入key直接删除键值对)。

5.vertices():

        这个方法返回所有的点,直接遍历vertices将所有Vertex的vertex加入一个新的HashSet,然后返回这个HashSet即可。

6.Sources()和Target():

       Sources/targets要求找到所有以参数点target/source为终点/起点的边以及这些边的起点/终点,并以map<起点/终点,边权>的形式返回一个Map。由于在Vertex类中已经实现了返回Map的方法,所以这两个方法在点实现中实现起来很简单,比如sources方法只需要找到target点之后调用方法返回它的sources即可,另一个方法类似。

7.toString():

        重写toString比较简单,因为Vertex类中的ToString已经编写的非常完善了,所以在这里对String再添加一些格式即可。

3.编写测试:

       在ConcreteVerticesGraphTest中编写测试,实现对ConcreteVerticesGraph类和Vertex类中的各个方法的测试,自行设计测试即可:

测试结果如下:

 

4.测试覆盖率:

       单独测试该方法覆盖率最多只占一半多一点左右(比边实现要多,因为点实现我编写的方法更多一些),结果如下:

左侧目录可以看到单独的顶点实现的覆盖度:

      下面是GraphInstanceTest的结果:

    下面是GraphStaticTest的覆盖率测试的结果(主要在这里有对接口的测试):

 

      下面是对整体四个Test文件进行覆盖率测试:

 

3.1.4 Problem 3: Implement generic Graph<L>

3.1.4.1 Make the implementations generic

这一步要求将之前实现的方法修改为泛型实现,由于之前两个问题我都是直接使用泛型实现的,之前已经提及修改方式,故这里不需要再做修改,也不再赘述。

3.1.4.2 Implement Graph.empty()

  这一步要求修改Graph.empty()为泛型实现,之前也已经进行过修改,结果如下:

 

3.1.5 Problem 4: Poetic walks

 在这个实现中,需要使用已有的语料库,复用之前的graph,用语料库构建一个有向加权图。这个图的点是语料库中的词(不区分大小写,一律按小写存储),任意相邻的两个词语即为两个点和一条有向边,这个相邻的组重复出现的次数为这条边的权重。这样构建出图之后,我们输入一个句子,之后检索这个句子中每一组相邻的词语,在图中查询这两个词语之间是否存在一个bridge word,如果存在,就将这个词加入到输入中的这两个词语之间,如果有多个bridge word,选择出发点到这个词语边上权重最大的那个词语插入。整个流程相当于是使用语料库构建的图对输入进行扩充,成为一首新的诗,然后进行输出。

3.1.5.1 Test GraphPoet

这里首先要求我们对GraphPoet类设计测试。根据代码中给好的待填充的方法,我们需要对以下内容设计测试:

(1)GraphPoet构造器:测试是否能正常打开文件,否则抛出FileNotFound的错误。

(2)Poem():测试是否能产生正确的新的诗歌,这里我使用两个测试文件,分别是官方实验文档中给出的Specification of GraphPoet中的例子hello,HELLO,hello,goodbye!和官方实验文档中对problem 4进行解释时给出的例子(这两个例子在官方文档中一个有图结构的参考答案一个有示意图,相当于给出正确答案,方便测试,避免自己想的答案不对)。在poem测试中检测输出结果和正确答案是否一致即可,如下图:

(3)ToString():根据设计的格式检测期待的输出格式和toString输出是否一致即可。

       下面是在代码编写完成后补充的对GraphPoetTest进行的覆盖率测试:

 

    

 

 

 

 

 

    这里少一个class和一些method是因为P1.poet中还有一个main.java。我没有对Main函数进行测试(Main可以直接运行,得到成功或失败的结果)。

3.1.5.2 Implement GraphPoet

这个类中主要需要实现三种方法:

1.构造器GraphPoet():

这个方法主要是使用传入的文件来构造语料图。读取文件采用官方文档推荐的方式BufferReader,在while循环中使用readLine函数按行读取。由于根据spec,我们可以得知要求这个语料图中的字符串均为小写(不区分词语的大小写),并且只考虑以空格间隔,所以在循环中首先对一行的字符串使用split(“\\s”)进行分割,得到一个返回的字符串数组,之后将得到的数组中的每个元素使用trim()和toLowerCase()处理(即全部转化为小写单词),循环加入graph(一开始由于忽略了这一步测试的时候结果一直不对,后来发现是在这里没有把点加进去)。

之后是比较关键的步骤,添加边。添加边的原则是循环添加,将相邻的两个点作为起点和终点,权重为1,设置变量weight为0。将返回值给weight,如果本来没有这条边,返回值是0,结束这一次添加,如果本来有这条边,那么返回值不为0,则重新set这条边,权重为返回的原边权+1。关键代码如下:

 

2.poem():

        这个方法是根据图来构造新的诗歌。存放诗歌采用StringBuilder。

       一开始对输入的字符串同样进行split处理得到字符串数组。之后进入循环,针对其中的每个元素,通过相邻两个元素的trim().toLowerCase()处理后的小写字母在图中使用sources和targets方法获得起点和终点分别相连的点。

       之后使用addAll方法和retainAll方法获取bridge words的集合,即获取起点指向的点和指向终点的点中重复的点。如下:

 

       之后使用方法来找到这些bridge words中和起点相连接的边权最大的点,将其插入输出中即可,在其中可以利用choose_bridge来存储最终添加的点,利用max_weight来存储最大权重。之后继续循环,这一步代码如下:

3.toString():

        这个方法比较简单,因为在graph的实现中已经重写了toString方法,所以在这里直接对格式略加修改即可。

        在所有方法编写完成之后,直接运行main函数,结果如下:

 

3.1.5.3 Graph poetry slam

这一步为可选做的,即可以添加一个自己选择的poet文件,并在main函数中修改输入,得到新的输出,我选择的诗歌为had I not seen the sun,内容如下(被遮住的是u):

 

修改main函数如下:

 

运行main函数,得到的结果如下:

  

3.1.6 Before you’re done

这步就是提交git啦,不再赘述。 

3.2 Re-implement the Social Network in Lab1

这个任务要求复用第一个任务中实现的graph来实现lab1中的人际关系网络。

3.2.1 FriendshipGraph类

在这个类中主要需要实现三个方法,分别是添加点,添加边和查询两个人之间的社交距离。编写AF,RI,constructor和checkRep等如下,两个方法都不是很复杂,并且添加了注释,故不再赘述:

 

(1)addVertex():

这个方法直接调用graph中的add来实现就可以。成功返回true。在此之前调用vertices获取所有顶点,再调用getName检查一下输入的人名是否是重复的即可,重复直接退出程序。

(2)addEdge():

这个方法直接调用set方法即可,weight设置为1.在之前检查一下输入的两个人名是否相同,相同报错即可。

(3)getDistance():

这个方法我选择直接在上次编写的方法上进行改动。这个方法中,两个人没有关系则距离为-1。采用BFS来实现。主要和上次的不同在于使用targets方法来获取正在遍历的人有联系的其他人,核心代码如下:

 

3.2.2 Person类

Person类我的设计比较简单,只需要一个人名作为参数,方法只有获取这个人名。

3.2.3 客户端main()

客户端main()的设计遵循lab1中实验指导给出的main函数的设计。

通过在图中加入四个人和他们的关系,调用getDistance方法打印出他们的社交距离。

3.2.4 测试用例

测试思路就是在图中加入点和边,测试每一个方法是否有期待的返回值。

对addVertex,测试是否能正常返回true。

对于addEdge,测试添加完边后调用getDistance方法是否能正常获取正确的边权。

对于getDistance方法,初始化并加入五个人,这五个人中t1,t2,t3相互连接构成一个三角形,t2和t4连接,t5独立,之后依次添加边并测试五个人两两之间的距离是否满足条件。

测试结果如下:

 

最终覆盖度如下:

 

 

Method的缺失是因为没有对main函数进行测试,覆盖Line比较少是因为main函数中的行(较多)没有覆盖(除此之外的代码行数并不很多),以及在类中我加入了比较多的异常报错以及退出程序的代码,如下:

这一部分报错内容没有进行测试(之前也进行了测试,出来的结果是灰色的横线不是绿色的勾,会中途退出。然后我又将这部分测试删除了)。

3.2.5 提交至Git仓库

还是提交~

 

一些感受

(1)  面向ADT的编程和直接面向应用场景编程,你体会到二者有何差异?

面向ADT的编程可以设计一种抽象数据类型供使用,相比于面向应用场景编写针对性的方法,其泛用性更强,但是功能也相对更弱一些。

(2)  使用泛型和不使用泛型的编程,对你来说有何差异?

使用泛型可以大大提高泛用性,可以在实际应用中使用各种数据类型,很方便。

(3)  在给出ADT的规约后就开始编写测试用例,优势是什么?你是否能够适应这种测试方式?

优势是可以提前设计好每种方法的返回值,以及需要应对的各种情况,帮助完善方法的设计思路。我现在还不太适应这种测试方法,拿到规约后下意识还是想直接写方法,后写测试,我会努力培养先编写测试用例的习惯的。

(4)  P1设计的ADT在多个应用场景下使用,这种复用带来什么好处?

这种复用减少了工程量,能帮助编写代码的人减轻重复的工作。

(5)  为ADT撰写specification, invariants, RI, AF,时刻注意ADT是否有rep exposure,这些工作的意义是什么?你是否愿意在以后编程中坚持这么做?

这些工作能让编写代码的人在忘记方法的相关信息后能通过这些注释内容迅速了解之前编写的方法,同样给阅读代码的人带来便捷的了解方式。使编写者明确方法的设计与实现思路,并且时刻牢记增强方法的健壮性,避免泄露。

(6)  关于本实验的工作量、难度、deadline。

本实验工作量适中,开始可能比较难上手,但熟悉之后工作量并不是很大。难度也比较适中,方法实现的逻辑都不是特别复杂。Deadline的时间我认为也比较合理。

(7)  《软件构造》课程进展到目前,你对该课程有何体会和建议?

软件构造这门课内容充实,实验的设置也很好,让人有充分实践的机会。但遗憾的是这门课的学时被减少了,学生做的实验可能存在跨度稍大的情况,导致比较难以上手。希望以后可以恢复学时,增加实验或者一些体量较小的作业/练手题目(阶段性的加深学生对学习到的内容的掌握)。

 

标签:ADT,需要,HIT,2022,实现,Lab2,测试,编写,方法
来源: https://www.cnblogs.com/redTide/p/16368834.html