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 实验环境配置
所用IDE:Intellij IDEA
由于Idea上已经自带有统计Junit测试用例代码覆盖度的功能,所以并没有去安装EclEmma
3 实验过程
请仔细对照实验手册,针对三个问题中的每一项任务,在下面各节中记录你的实验过程、阐述你的设计思路和问题求解思路,可辅之以示意图或关键源代码加以说明(但千万不要把你的源代码全部粘贴过来!)。
3.1 Poetic Walks
在这个问题中,需要根据给定的Graph接口,分别实现ConcreteEdgesGraph,ConcreteVerticesGraph这两个类,并且分别写他们的测试代码。在此基础之上,将String拓展为泛型L类。
根据上面所完成的类,实现Poetic Walks。即若输入的两个词中间在图中有一个中间词,则将这个词加入到输入的诗句中。
3.1.1 Get the code and prepare Git repository
从QQ群中获取。
3.1.2 Problem 1: Test Graph
GraphStatictest测试结果:
GraphInstanceTest:
- 测试add:
往测试图中加入顶点后,测试图中是否包含该点,并且能够在二次加入的时候返回false - 测试set
根据接口的定义,测试以下几种情况,对于已经存在的顶点之间加边,对没有存在的顶点之间加边,修改原来的边,删除一条边 - 测试remove
先构造一个图
然后测试从这个图中删除b后是否正确 - 测试vertices
在图中加入几个点,然后判断返回的集合是否正确 - 测试sources
构建一个图,然后判断一个顶点的sources是否与预期的一致 - 测试target
构建一个图,然后判断一个顶点的targets是否与预期的一致 - 测试结果
覆盖度:
3.1.3 Problem 2: Implement Graph
以下各部分,请按照MIT页面上相应部分的要求,逐项列出你的设计和实现思路/过程/结果。
3.1.3.1 Implement ConcreteEdgesGraph
Edges类:
Edges是一个代表边的类,所以成员应该包括有边的出发顶点source和结束顶点target,同时,因为题目要求是有权值的边,所以说还需要一个整形变量来存储权值
Edges的构造器:
在edges当中,所需要的的方法就是返回该边的两个顶点和权值,同时,还定义了一个方法来判断是不是同一个边
Concreteedgesgraph类:
这个类是对于graph接口的具体实现,因此每个方法是要遵守规约并且实现功能就行了,下面具体来介绍每个方法干了什么
Add:
向图中加入顶点,如果图中已经有待加入的顶点,则返回false
Set:
首先判断输入的两个顶点在不在图中,若不在,则调用add讲该顶点加入到图中,如果两个都在,就循环寻找边,若有,则返回原来的权值,并且重置权值。如果没有,则新加一条边并且返回0
Remove:
选定一个顶点,先把该顶点有关的边都删除,然后将该顶点从顶点集中移除
Vertices/sources/targets
分别返回顶点集,入边集,出边集
ConcreteedgesgraphTest类:
在这个类中测试concreteedgesgraph中特有的方法,即两个tostring方法
测试结果:
3.1.3.2 Implement ConcreteVerticesGraph
Vertex类:
Vertex存储的是顶点的信息,因此需要的成员有,顶点的标识,入边集和出边集
相对于edges类,vertex的实现更加复杂一些,首先需要有能够获得入边,出边的方法,并且也要有更新入出边的方法
以上是获得成员变量的方法
增加入边
增加出边
移除入边
移除出边
Concretevertexgraph类
由于和上面的concreteedgesgraph是继承的同一个接口,因此需要实现的目的差不多,但区别是实现起来的方法和形式不同,但是感觉由于vertex类比edges难实现一些,相 对的,Concretevertexgraph类比concreteedgesgraph实现起来更加直观容易一些,因 此具体的不再多说。
ConcretevertexgraphTest类:
在这个类中测试concretevertexgraph中特有的方法,即两个tostring方法
测试结果:
3.1.4 Problem 3: Implement generic Graph
3.1.4.1 Make the implementations generic
讲程序中的具体类型String用泛型L代替
3.1.4.2 Implement Graph.empty()
对于静态函数empty,new一个空图返回,保护构造函数不暴露。
3.1.5 Problem 4: Poetic walks
3.1.5.1 Test GraphPoet
从文件中读入输入字符串,测试能否经过运行,在每两个有桥接单词的词中间插入正确的词
3.1.5.2 Implement GraphPoet
GraphPoet类:
首先读输入,然后根据顺序建图。建图的时候考虑,如果两个顶点是第一个成边,则设置权值为1,若之前就已经有边了,则权值在原来的基础上加1就行了
Poem类:
首先将输入变成字符串数组:
然后就寻找输入相邻两个单词之间的桥接词,即前一个单词的target和后一个单词的sources的重合部分,则能够成为桥接词,然后寻找权值最大的那条边插进去就行了
3.1.5.3 Graph poetry slam
3.1.6 Before you’re done
请按照http://web.mit.edu/6.031/www/sp17/psets/ps2/#before_youre_done的说明,检查你的程序。
如何通过Git提交当前版本到GitHub上你的Lab2仓库。
在这里给出你的项目的目录结构树状示意图。
3.2 Re-implement the Social Network in Lab1
这个问题就是要通过运用在上一问中所实现的ADT,将实验1中的FriendShip这个类重新写一遍,同时泛型L替换为Person。针 对 addVertex() 和addEdge() ,需要尽可能复用 ConcreteEdgesGraph或ConcreteVerticesGraph中已经实现的 add() 和 set()方法,而不是从 0 开始写代码实现或者把你的 Lab1 相关代码直接复制 过来;针对getDistance()方法,请基于你所选定的 ConcreteEdgesGraph 或 ConcreteVerticesGraph的 rep 来实现,而不能修改其 rep。
3.2.1 FriendshipGraph类
Addvertex:在图中加入顶点,并将名字加入namelist
Addedge:根据输入的两个人名,在他们之间加一条有向边
getDistance:返回图中两个人的最短距离
3.2.2 Person类
Person用于存储每个人的名字信息,并且能够通过getname这个方法来获得name信息
3.2.3 客户端main()
直接根据Lab1中问题三所给的main函数来进行设计
3.2.4 测试用例
Testaddvertex:往graph中加入顶点,看是否能正确添加,并且如果重复添加则返回false
Testaddedge:分别测试加入边行为正确性,重复加边行为
Testgetdistance里,首先创建如下图:
然后进行测试:
3.2.5 提交至Git仓库
如何通过Git提交当前版本到GitHub上你的Lab3仓库。
在这里给出你的项目的目录结构树状示意图。
3.3 Playing Chess
3.3.1 ADT设计/实现方案
设计了哪些ADT(接口、类),各自的rep和实现,各自的mutability/ immutability说明、AF、RI、safety from rep exposure。
- Action
Action的作用是描述棋盘上所发生的动作,比如落子(针对为期),吃子,移动棋子(针对象棋)。在构造的时候,将棋盘作为参数来进行构造
对于上面说的下棋的三种基本动作,分别写三个方法
- Board
Board的作用就是作为一张棋盘,因此在board上面定义一个二维数组,来表示棋盘上的坐标
这个二维数组用来存放棋子类型Piece。在数组中的棋子即意味着它被映射到棋盘上对应的坐标
在构造棋盘的时候,自然是需要知道期盼的大小,因此的传入的参数就是棋盘的大小,并根据此构建出棋盘
在棋盘上索要进行的操作有将棋子放到棋盘上,和判断棋盘上的特定位置放的是什么棋子
- Piece
Piece类代表棋子,棋子所具有的属性就是棋子的名字,比如在围棋中,就是黑子和白子,在象棋中的马,车,兵等等,同时我在这里还将棋子在棋盘上面所处的位置和棋子输入那个玩家作为其属性,这样能够更好地让需求实现
因此,在创建一枚新的棋子的时候,就应该明确棋子的这些属性
棋子上面没有什么特别的操作,基本就是获得棋子的各个属性的方法。根据围棋和象棋中的规则,棋子的所有权和类型是不会发生改变的,而在象棋中位置可能会发生改变,因此只额外写一个改变棋子位置信息的方法就行了
- Player
在player这个类中,记录了玩家的姓名和走棋历史
然后主要的方法也是获取类中的成员 - Position
Position用来刻画棋盘上的坐标,结合实际的棋盘和board类的实现形式,position类中庸想,y坐标来刻画位置 - Game:
首先需要确定下棋的种类,因为只有两种类型,因此选择使用布尔变量,true代表是象棋,false则代表是国际象棋
确定了下棋种类后就是对游戏进行初始化、:
象棋:
Numa,b分别代表两个玩家所持有的棋子数量,确保不会出现两个玩家拥有负数的棋子的情况。
围棋:
因为围棋一开始棋盘上是没有棋子的,所以初始化比较方便
Game的方法则是几个游戏的操作
比如围棋中的放子
默认围棋中先手的一方用黑棋,即玩家a使用黑棋
在得到哪个玩家想要在哪个位置放置棋子后,先判断该位置是否已经有棋子,根据围棋规则,如果已经有了,是没有办法落子的,若哪个位置是空的,那就在那里放置棋子就行了
然后是象棋的吃子操作,吃子在围棋和象棋中都有,但是规则不太一样。首先ear函数获得需要吃的棋子和去吃的棋子,判断棋子的归属是否正确还有棋子是否存在
如果情况都符合,则执行吃子操作,被吃掉的子被移除,相应的num要减一,然后去吃的棋子移动到被吃的棋子的位置。
之后是围棋的提子操作,围棋实际的提子操作比较麻烦,这里给出简化的处理方法。给定执行玩家和要移走的棋子的位置,首先进行检查,若没问题,则执行提子操作。
以上的操作之后都是需要记录到玩家的history属性中的
3.3.2 主程序MyChessAndGoGame设计/实现方案
主程序根据上面所实现的类,实现了以下功能:
(1) 让用户选择创建一盘国际象棋或一盘围棋,用户输入“chess”或“go”
分别代表国际象棋和围棋;让用户输入两个玩家的名字;
(2) 启动比赛,程序提示玩家双方交替采取行动,直到一方输入“end”而结
束。双方分别采取行动的时候,可以选择以下行为之一,也可以选择“跳
过”(即放弃本次采取行动的权利):
- 将尚未在棋盘上的一颗棋子放在棋盘上的指定位置;
- 移动棋盘上某个位置的棋子至新位置;
- 提子或吃子;
- 查询某个位置的占用情况(空闲,或者被哪一方的什么棋子所占用);
- 计算两个玩家分别在棋盘上的棋子总数。
请自行为用户设计针对上述行为所需输入的数据,用户输入越简洁越好。
(3) 当某一方输入 end 结束游戏之后,双方可以查看本次比赛的走棋历史,
即能够查询自己所走的所有步骤。
在主函数中,首先肯定是需要创建一个游戏,从用户的输入中获得玩家信息,下棋类型,创建对应的类然后开始游戏
然后进入到对应的游戏当中,用switch来进行循环,用户输入操作并执行直到用户输入end结束用户并询问用户是否需要下棋历史。因为这里主函数的实现只是对上面类的调用,因此就不放代码,直接来模拟一盘游戏介绍流程
下面是一盘A大战B的围棋游戏
首先是初始化游戏:
以步为单位,每一步结束后都会跳出选择菜单
首先轮到A,A选择在1,1处放棋子:
然后进行查询:
符合正确情况
查询占用操作:
测试提子操作,因为是本人的棋子,所以失败,但是可以提走对方的棋子
之后结束游戏,查看历史记录
3.3.3 ADT和主程序的测试方案
Testgame:
测试创建新的国际象棋和围棋游戏,判断返回的初始棋子数目是否正确
Testeat
首先测试吃自己的棋子的时候应该报错,然后棋盘上没有对应棋子的时候也要报错,然后正确吃子之后双方的棋子数量应该发生变化
Testremove
两种错误情况,分别是提子位置没有棋子和提子位置是自己得棋子。
Testmove
首先测试三种无法正确移动的情况:选择位置没有棋子或者不属于自己的棋子,目标位置不为空,然后正常移动后,测试棋子状态
Testput
若棋盘上目标位置已经有棋子,则放置失败。若放置成功,测试双方棋子的数目变化
测试结果: