【软件构造】Lab2基本流程指导及重难点分析
本篇文章旨在引导HIT软件构造Lab2的基本流程并进行一些重难点的解释,总体内容偏向基础,出发点是在实验过程中我遇到了很多问题并花了大量时间才解决,希望可以帮助大家更好地完成实验。
实验目标:本次实验主要内容是训练设计ADT的能力,即自己设计实现合适的抽象数据类型,做到使用和实现分离,从而进一步训练面向对象编程思想。此外,还对程序的测试提出了规范化的要求,需要我们使用测试优先编程,即在编写程序代码前需要先编写测试,并且要有良好的测试策略,覆盖所有等价类。
实验环境:eclipse、git、eclemma。其中eclemma是检验测试用例覆盖度的工具,是eclipse自带的,无需额外下载。
问题1 Poetic Walks
此问题已经提供了整个代码的框架,需要完成的是内部补充和测试,相对容易,我认为难点仍然主要存在于理解英文题面。题目要求使用两种方式来实现一个Graph抽象类中的一些空白方法,一种是边图(即着眼点在图中的边上),一种是点图(即着眼点在图中的点上),并利用实现完成的ADT实现一个在诗歌上的功能。
1.1 Test Graph <String>
测试Graph类中已经实现的既适用于点图也适用于边图的功能,在GraphInstanceTest.java中编写JUnit测试用例,设计测试用例的方法是观察被测试方法,找到其中会被分类的地方,例如方法“加入一个点”在设计测试用例时要考虑到新添加的点是否在图中已经存在。将所有考虑到的会分类的地方都用测试用例覆盖到即可。设计完成后调用JUnit测试。
1.2 Implement Graph <String>
要求用两种方式(点图和边图)实现Graph的八个方法,分别是加入一个点、删除一个点、加入一条边、删除一条边、改变一条边的权值、求所有点的集合、求从某点出发的所有边的集合、求到达某点的所有边的集合。记得测试优先编程,写代码之前先写测试。
首先先说边图,在ConcreteEdgesGraph类中实现,给出一个例子:
注意,此时已经开始了ADT的设计,其中加入的rep需要给出AF、RI、表示泄露保护这些要求的信息。
rep中实现一个Set和List,分别用来记录点和边的信息,加点加边等信息可以直接进行。求从某点出发/到达某点所有边的信息需要遍历一遍边集,新建数据结构,筛选出符合要求的边返回。另外值得注意的是,在删除点的实现中,需要将这个点涉及的所有边全部删除,有可能存在同时删除多条边的情况,这时不能在List上边遍历边删除,因为remove方法执行后会影响接下来的遍历导致出错。我的解决方法是新建一个List拷贝原List,之后在新List上遍历,找到目标边后在原List上删除,这样可以保证遍历操作不出错。当然,这种实现方法略显粗糙,可以思考更优美的实现方法。
再说点图,在ConcreteVerticesGraph类中实现,给出一个例子:
在边图中Edge类不需要实现很多功能,主要功能在ConcreteEdgesGraph中实现。而在点图中我选择在Vertex类中承担一部分功能,内部rep是map数据结构,存储“点”对“点关联的所有边”的映射,类似邻接表的实现方式。实现了增加、修改、删除、传出的功能。而在ConcreteVerticesGraph类中,大部分等功能可调用Vertex轻松实现,值得注意的是删点方法,需要先在点集中将点删除,之后遍历所有点,检查每个点与删除点之间是否有边,如果有就删除。
1.3 Implement generic Graph<L>
实现泛型,有关泛型的知识如果不清楚可以去搜索学习,这里的具体做法就是将声明中的String改为L即可。empty函数里声明点图还是边图都可以,反正new一个返回就行了。
1.4 Poetic walks
要求利用之前完成的ADT实现一个小功能,简单来说就是首先给出一些单词和之间连接的有向边作为一个扩展依据集,之后给出一个句子,要使用扩展依据集来扩写这个句子,规则是如果在扩展依据集上存在一条A->B->C的路径而在句子中A单词的下一个单词是C单词,那么将句子中A单词和C单词之间加入一个B单词。
读懂题意后实现就非常简单了,读入扩展依据集使用之前实现好的Graph ADT来建图:依次取出前后两个元素,轻松实现前一个元素向后一个元素连边。再读入句子,枚举每个单词,在构造出的图上搜索到所有这个单词经过两条边能到达的点,如果到达的点与文本中下一个单次正好吻合,则为合理选项之一,将所有这样的路径找到后进行比较,取权值最大的路径,将中间点对应单词插入文本中。
问题2 Re-implement the Social Network in Lab1
该问题要求复写一遍Lab1中的问题3,使用Lab2问题1中实现的ADT来实现。由于ADT可以完全覆盖之前问题需要的功能,即实现一个有向图的维护,大体实现都和Lab1中的一致。我在实现过程中只遇到了一个需要更新的地方,由于ADT中涉及到了Set数据结构和许多比较操作,需要重写Person类的equals方法和hashCode方法,equals中直接比较两个字符串,若两个字符串相等(名字相等)则两个Person类相等。重写hashCode方法的方式很多,建议去学一下,然后选一种方式实现。
问题3 Playing Chess
这个问题是三个问题中最复杂的,但也是最自由的,之前的问题给出了框架,你需要读懂框架并补充相应代码,使得ADT可以正常完成需要的功能。但在这里,所有的一切都由自己完成,从零开始设计一套ADT并完成指定功能,开始时可能不好上手,但做了一段时间后会发现没有太多限制地自由发挥写起来会比较舒服。
那么如何上手呢?我的方法是首先通读一遍题目,理解问题,比如这个问题是让你实现一个模拟两人下棋的局面,其中包含了下棋、吃棋等等操作。之后开始考虑具体实现,一个强有力的方法是注重题目中的名词,一些高频名词往往可以对应一个ADT,比如在这个问题中,“棋子”,“棋局”,“位置”等名词可能意味着你可以设计相对应的ADT,然后通过ADT之间的相互调用来支撑起整个工程。由于内部包含哪些ADT,各个ADT之间包含那些功能,ADT之间的相互调用关系等等全都是自己来设定,因此自由度非常高。开始时如果做不到考虑得很细致也没关系,可以直接上手写,通常写到途中会经常发现某个功能放在某个地方可能看起来更好这样的想法,及时调整,写多了思路自然就清晰了。
举一个“部分”例子,我设计了一个名为Board的ADT,用来记录当前棋局状态,里面用二维数组实现了对当前棋局每个位置状态的记录(有子无子,黑方白方),还用一个集合类实现存储棋子的集合,类型是自行设计的Piece类ADT。Piece类ADT用来维护一个棋子的信息,包括它的归属(颜色)和位置信息。然后考虑到“位置”这一信息比较重要,单独提取出来比较合理,于是设计一个Position类,用来维护一个位置,内部存储横纵坐标,然后在Piece类中使用这个位置类即可。
类似上面这种情况,本质上就是很多ADT之间的相互调用,来实现需要的功能。而具体设计哪些类,类中方法怎么定,类与类的关系和调用这些问题就是需要自己去思考的事情,由于在本题中自由度非常高,并且对设计出来的软件质量要求也并不苛刻,所以可以比较随心所欲。但同时要知道这些问题正是影响软件各个维度上质量指标的关键因素,在要求比较高的时候就需要一个良好的ADT框架设计,所以建议在此问题中就多思考怎样设计ADT和ADT之间的关系可以让整个程序框架更好。
补充说明的是每个ADT都要设计良好的AF、RI和表示泄露保护,每个ADT都要设计覆盖全面的测试用例并写出测试策略,来保证软件的安全性和正确性。
最后,需要设计一个客户端类,实现用户和内部的交互,可以选择在控制台输入输出,例如提供选择菜单,输入规定信息执行棋手操作或查看指定信息。