目录
3.1.1 Get the code and prepare Git repository 1
3.1.2 Problem 1: Test Graph <String> 1
3.1.3 Problem 2: Implement Graph <String> 1
3.1.3.1 Implement ConcreteEdgesGraph 2
3.1.3.2 Implement ConcreteVerticesGraph 2
3.1.4 Problem 3: Implement generic Graph<L> 2
3.1.4.1 Make the implementations generic 2
3.1.4.2 Implement Graph.empty() 2
3.1.5 Problem 4: Poetic walks 2
3.2 Re-implement the Social Network in Lab1 2
3.3.2 主程序MyChessAndGoGame设计/实现方案 3
实验目标概述
本次实验训练抽象数据类型(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
实验环境配置
本次实验的总体配置和上次实验相同,这里不再赘述。唯一需要增添的插件就是代码覆盖工具,即EclEmma。
一开始我在网站上搜索EckEmma的配置过程,但是发现在官网上下载似乎需要翻墙,这令我而苦恼,而后我又在的eclipse商店查找相关信息,结果发现也无法下载。 稍微思考了一下我又去搜了一下,原来现在的eclipse直接装的就有这个工具,因此实验环境就不用再配置了。
在这里给出你的GitHub Lab2仓库的URL地址(Lab2-学号)。
https://github.com/ComputerScienceHIT/Lab2-1180500313.git
实验过程
请仔细对照实验手册,针对三个问题中的每一项任务,在下面各节中记录你的实验过程、阐述你的设计思路和问题求解思路,可辅之以示意图或关键源代码加以说明(但千万不要把你的源代码全部粘贴过来!)。
Poetic Walks
在这里简要概述你对该任务的理解。
Get the code and prepare Git repository
如何从GitHub获取该任务的代码、在本地创建git仓库、使用git管理本地开发。
git clone https://github.com/rainywang/Spring2020_HITCS_SC_Lab2.git
在本地获取该任务代码只需输入上述代码即可。
git init
git remote add origin git@github.com:ComputerScienceHIT/Lab2-1180500313.git
git pull origin master
git add .
git commit -m "init"
git push origin master
用上面的语句初始化本地仓库并做第一次提交的测试,之后用git管理本地开发时进行类似的操作即可。
Problem 1: Test Graph <String>
这里我们只需要测试Graph<String>中的empty()方法即可。补全Graph中的empty()。
public static Graph<String> empty() {
//throw new RuntimeException("not implemented");
return new ConcreteEdgesGraph();
}
此外由于测试中使用了Graph.empty().vertices()方法,我们将其补全,返回HashSet()。
Problem 2: Implement Graph <String>
以下各部分,请按照MIT页面上相应部分的要求,逐项列出你的设计和实现思路/过程/结果。
Implement ConcreteEdgesGraph
为了实现graph接口我们总体的想法就是测试优先,首先写出测试,然后根据规约进行设计。本实现方式是基于边的。
关于AF,RI和避免表示泄露,这里采用
// Abstraction function:
//用边的方式来表示图,映射到一个图结构上。
// Representation invariant:
//图的边是有向正权的,图中没有复边(两条方向和连接点均相同的边
//也没有重复的顶点
// Safety from rep exposure:
// vertices和edges是private final的,信息对外界是隐藏的
//final保证外界无法改变他的引用
//并且使用了防御式拷贝
//防止外界通过对内部引用进行操作导致表示泄露
而关于我们具体的设计,本部分涉及到的方法和变量如下:
其中右侧为用边实现一个有向正权图的所有方法和变量,左侧为实现一个完整的图所使用的方法和变量。其中比较重要的方法如:
public int set(L source, L target, int weight) {
int currentweight = 0;
if (weight != 0) {
for (Edge<L> e : edges) {
if (e.getSource().equals(source) && e.getTarget().equals(target)) {
currentweight = e.getWeight();
edges.remove(e);
break;
}
}
vertices.add(source);
vertices.add(target);
edges.add(new Edge<L>(source, target, weight));
} else {
for (Edge<L> e : edges) {
if (e.getSource().equals(source) && e.getTarget().equals(target)) {
edges.remove(e);
currentweight = 0;
break;
}
}
}
checkRep();
return currentweight;
}
这个地方实际上实现的是设置一条有起点、有终点、带权值的边的方法。我们通过输入的权值来更新目前储存的权值,便于返回操作得到的状态(成功更改,未找到目标顶点),实现方式是迭代器的查找。
实现结果:
测试覆盖率:
这里达到了94.6%,至于没能完全测试的原因是涉及到异常的测试,这一部分还没学习,因此就未进行测试
Implement ConcreteVerticesGraph
关于AF,RI和避免表示泄露,这里采用
// Abstraction function:
//用顶点的方式来表示图,映射到一个图结构上。
// Representation invariant:
//图的边是有向正权的,图中没有复边(两条方向和连接点均相同的边
//也没有重复的顶点
// Safety from rep exposure:
// vertices是private final的,信息对外界是隐藏的
//final保证外界无法改变他的引用
//并且使用了防御式拷贝
//防止外界通过对内部引用进行操作导致表示泄露
而关于我们具体的设计,本部分涉及到的方法和变量如下:
其中右侧为用顶点角度实现一个有向正权图的所有方法和变量,左侧为实现一个完整的图所使用的方法和变量。其中比较重要的方法如:
public int setSource(L source, int weight) {
Integer now_Weight = 0;
if(weight<0)
{
throw new RuntimeException("边权不为负");
}
else if (weight == 0) {
now_Weight = this.removeSource(source);
} else {
now_Weight = this.source_vertex.put(source, weight);
if(now_Weight==null) now_Weight=0;
else now_Weight=weight;
}
checkRep();
return now_Weight;
}
设计思路是 weight<0 抛出异常"边权为负"。weight=0,删去传入的target,返回其weight,若没找到则返回0; weight>0,加入新的target和weight,若已经有一个存在则返回weight,否则返回0。最后还要进行不变量的检查,调用checkRep()。
最后的运行结果为:
测试覆盖率:
这里达到了93.0%,至于没能完全测试的原因是涉及到异常的测试,这一部分还没学习,因此就未进行测试.
Problem 3: Implement generic Graph<L>
Make the implementations generic
将上面涉及到具体对象类型的地方改为泛型实现,可以采用eclipse中的重构功能。
Implement Graph.empty()
能返回一个空的实例即可。
public static <L> Graph<L> empty() {
Graph<L> graph = new ConcreteEdgesGraph<L>();
return graph;
}
Problem 4: Poetic walks
这里我们实现了对前面已经构建好的数据结构的复用。具体的复用方式是利用了前面的有向无权图的格式,在给定输入句子时,会参考已有的语料库,检测单词和单词之间是否存在bridge,即对语料库不一样的单词看作一个顶点,相邻的单词对应图中的一条有向边。相邻单词对出现的次数,作为这条有向边的权值。如果输入的句子在图中出现并且之间有通路的话,就会进行信息补全,加入间隔的单词。
Test GraphPoet
我们进行测试的等价类划分原则:
// 对GraphPoet的文件输入划分等价类:
// 文件只有一行
// 文件有多行
// 文件为空
// 对poem的选择划分等价类:
// 所有权都是1
//有不为1的权
// 对toString的输入划分等价类:
// 文件为空
//文件不为空
测试用的语料库:
Implement GraphPoet
关于AF,RI和避免表示泄露,这里采用
// Abstraction function:
// 将一个加权有向图映射到一个写诗的任务上
//诗歌之间的词语就是加权有向图通路上的点
// Representation invariant:
// 该图是加权有向图 weight>0
//图是非空的
// Safety from rep exposure:
//graph是private final的,信息对外界是隐藏的
//final保证外界无法改变他的引用
//并且使用了防御式拷贝
//防止外界通过对内部引用进行操作导致表示泄露
我们实现过程中涉及到的变量和方法如下
主要的一些方法设计思路:
poem方法
参数:输入的原始句子。
利用spilt分割输入字符串,用StringBuilder保存返回结果。每次读取一个词,然后以当前词为source,下一个词为target,在graph中寻找符合此条件的边,记录权值,结束后选择权值最大的,调用StringBuilder. Append方法,将节点名字加入字符串。
public String poem(String input) {
StringBuilder SB = new StringBuilder();
List<String> list = new ArrayList<String>(Arrays.asList(input.split(" ")));
Map<String, Integer> sourceMap = new HashMap<>();
Map<String, Integer> targetMap = new HashMap<>();
for (int i = 0; i < list.size() - 1; i++) {
SB.append(list.get(i)).append(" ");
String sourceString = list.get(i).toLowerCase();
String targetString = list.get(i + 1).toLowerCase();
targetMap = graph.targets(sourceString);
sourceMap = graph.sources(targetString);
int maxWeight = 0;
String bridgeWord = "";
for (String string : targetMap.keySet()) {
if (sourceMap.containsKey(string) && sourceMap.get(string) + targetMap.get(string) > maxWeight) {
maxWeight = sourceMap.get(string) + targetMap.get(string);
bridgeWord = string;
}
}
if (maxWeight > 0) {
SB.append(bridgeWord + " ");
}
}
SB.append(list.get(list.size() - 1));
return SB.toString();
}
GraphPoet方法
参数:本地的语料库。
打开文件,读取文件输入,识别单个的单词,构建图结构。利用BufferedReader.readLine方法读取全部输入后用string.split以空格划分,保存在数组中,随后每次取相邻元素,在图中新增边。
Graph poetry slam
我们用吉檀迦利诗歌节选作为语料库。
运行效果如上图,下面是补全之后的诗歌。
覆盖度:
我们的方法覆盖度为97%,这也进一步印证了我们代码的正确性。
Before you're done
请按照http://web.mit.edu/6.031/www/sp17/psets/ps2/#before_youre_done的说明,检查你的程序。
如何通过Git提交当前版本到GitHub上你的Lab2仓库。
首先进入我们要提交的文件夹中,然后依次输入以下指令
git add .
git commit -m "P1"
git push origin master
在这里给出你的项目的目录结构树状示意图。
Re-implement the Social Network in Lab1
在这里简要概述你对该任务的理解。
我们在lab1里面实际上已经构建过一个图,虽然我们实验1要求里面是采用无向图进行测试,但是实际上也是用有向图来实现的,那么我们进行了哪些扩充呢?实际上是进行了扩充到泛型领域的操作,我们的顶点可以是自己设计的对象people,也可以是其他对象。当然我们也加入了顶点之间的权值。
泛型的加入使我们程序的可扩展性大大增加了。
FriendshipGraph类
给出你的设计和实现思路/过程/结果。
我们将原来用邻接表表现社交关系换为了用有向图来表现,有向图是以边和顶点构成的。并且有向图的边是正权的,而不是局限于lab1的只有一种权。
这里涉及到的主要方法为:
addVertex:增加新的人
addEdge:增加新朋友
getDistance:获取二人距离。
public int getDistance(Person Person1, Person Person2) {
if (Person1 == Person2)
return 0;
Queue<Person> queue = new LinkedList<>();
Map<Person, Integer> distantMap = new HashMap<>();
queue.offer(Person1);
distantMap.put(Person1, 0);
while (!queue.isEmpty()) {
Person topPerson = queue.poll();
int nowDis = distantMap.get(topPerson);
Set<Person> friendList = people.targets(topPerson).keySet();
for (Person ps : friendList)
if (!distantMap.containsKey(ps)) {
distantMap.put(ps, nowDis + 1);
queue.offer(ps);
if (ps == Person2) {
return distantMap.get(Person2);
}
}
}
return -1;
}
这里使用了队列和深度优先搜索来实现计算两人之间的距离。具体思路就是:用diatantmap保存其他人到自己的距离,然后入队自己,将朋友加入队列,不是person2就加入队列,是就返回最终距离。
Person类
这里的变量和lab1一样,唯一的一个方法是:
public Person(String nameString) {
if (personlist.contains(nameString)) {
System.out.println("出现了重复的名字");
} else {
this.name = nameString;
personlist.add(nameString);
}
}
这个构造方法可以加入新的人,并判断是否重复。
客户端main()
public class FriendshipGraphTest {
@Test(expected = AssertionError.class)
public void testAssertionsEnabled() {
assert false;
}
@Test
public void testsamePerson() {
FriendshipGraph graph = new FriendshipGraph();
Person a = new Person("a");
graph.addVertex(a);
Person f = new Person("a");
assertFalse(graph.getPeople().vertices().contains(f));
}
@Test
public void testsaddEdge() {
FriendshipGraph graph = new FriendshipGraph();
Person a = new Person("a");
Person b = new Person("b");
graph.addVertex(a);
graph.addVertex(b);
graph.addEdge(a, b);
assertEquals("expected distance", 1, graph.getDistance(a, b));
}
@Test
public void testsaddEdgenotexist() {
FriendshipGraph graph = new FriendshipGraph();
Person a = new Person("a");
Person b = new Person("b");
graph.addEdge(a, b);
assertEquals("expected distance", 1, graph.getDistance(a, b));
}
@Test
public void testFriendshipGraph() {
FriendshipGraph graph = new FriendshipGraph();
Person a = new Person("a");
Person b = new Person("b");
Person c = new Person("c");
Person d = new Person("d");
graph.addEdge(a, b);
graph.addEdge(b, c);
graph.addEdge(c, d);
assertEquals("expected distance", 1, graph.getDistance(a, b));
assertEquals("expected distance", 1, graph.getDistance(b, c));
assertEquals("expected distance", 1, graph.getDistance(c, d));
assertEquals("expected distance", 2, graph.getDistance(a, c));
assertEquals("expected distance", 2, graph.getDistance(b, d));
assertEquals("expected distance", 3, graph.getDistance(a, d));
assertEquals("expected distance", -1, graph.getDistance(b, a));
}
}
测试用例
我们进行等价类划分的大致策略如下:
// Testing strategy
// addVertex输入划分等价类:
//只有一个人名
//有重复人名
// addEdge的输入划分等价类:
// 起始点存在
//起始点不存在
// getDistance输入划分等价类:
// 距离存在
//距离不存在
测试结果如下:
测试样例覆盖率如下:
可以看到,P2的测试覆盖率接近100%
提交至Git仓库
如何通过Git提交当前版本到GitHub上你的Lab2仓库。
首先进入我们要提交的文件夹中,然后依次输入以下指令
git add .
git commit -m "P1"
git push origin master
在这里给出你的项目的目录结构树状示意图。
Playing Chess
ADT设计/实现方案
我定义的主要类:
Action:模拟下棋的动作
public class Action {
private final ArrayList<String> historys = new ArrayList<>();
private int historycounter = 0;
// Abstraction function:
//映射为现实中下棋的场景
// Representation invariant:
// 每轮必须有棋手下棋
// 棋手必须轮流下棋
//棋手只能下在棋盘内
// Safety from rep exposure:
// historys是private final的,信息对外界是隐藏的
//final保证外界无法改变他的引用
//并且使用了防御式拷贝
//防止外界通过对内部引用进行操作导致表示泄露
//historycounter是基本数据类型,是不可变的
Board:模拟下棋的动作
public class Board {
private int boardSize;
private final Set<Piece> boardSet = new HashSet<>();
public String playerA;
public String playerB;
// Abstraction function:
// 从一个boardSet映射到真实的棋盘
// Representation invariant:
//进行操作的坐标必须在棋盘内,一个位置不能有两个棋子
// Safety from rep exposure:
// boardSet是private final的,信息对外界是隐藏的
//final保证外界无法改变他的引用
//并且使用了防御式拷贝
//防止外界通过对内部引用进行操作导致表示泄露
//String ,int都是不可变的数据类型,防止了表示的泄露
Game:进行对弈的初始化
// Abstraction function:
// 映射为真实世界的对弈游戏,进行初始化
// Representation invariant:
//进行操作的坐标必须在棋盘内,一个位置不能有两个棋子
//两人必须轮流下棋
// Safety from rep exposure:
// Board是不可变的
//gameAction,PlayerA,PlayerB都是private,final的
所有类的UML:
主程序MyChessAndGoGame设计/实现方案
辅之以执行过程的截图,介绍主程序的设计和实现方案,特别是如何将用户在命令行输入的指令映射到各ADT行。
方法:
goGameMenu | 输出围棋的菜单 |
cheseGameMenu | 输出国际象棋菜单 |
gameMain | 游戏尚未明确类型时的客户端函数 |
goGame | 执行围棋操作 |
cheseGame | 执行国际象棋的函数 |
main | 声明一个MyChessAndGoGame实例,调用gameMain方法,启动游戏。 |
游戏流程:
首先是main函数,新建实例,开始gameMain方法。
gameMain方法读取玩家输入选择游戏的种类,
初始化玩家名字,初始化棋盘,
之后按游戏种类调用两个游戏方法:
在游戏函数中,针对落子,提子,吃子一类操作,都由客户端读取输入,分解用户输入,检查输入是否合法:(这里以落子为例,其他类似)
之后传递参数让game类执行操作,game类再由声明的action实例调用board类执行一系列落子,提子,吃子操作,并返回成功与否的布尔值。如果执行不成功输出错误提示信息,如果成功,由action类记录历史,返回真值,game类返回真值给客户端,客户端收到真值后,令TURN = (TURN + 1) % 2;,达到令TURN在0,1之间反复的效果。以此判断下一个选手是谁。
如果输入是end,修改exitflag之后,退出。
之后选择是否输出游戏历史:
游戏流程截图:
ADT和主程序的测试方案
介绍针对各ADT的各方法的测试方案和testing strategy。
介绍你如何对该应用进行测试用例的设计,以及具体的测试过程。
主程序主要使用手动测试的方法,针对参数越界,控制权不符合要求,输入参数不足等情况手动测试。
测试board类:
主要测试putPiece,removePiece,move,eat几个重点操作。
public void testputPiece() {
Board B = new Board("b");
Piece piece1 = new Piece("AAA", 1);
Piece piece2 = new Piece("AAA", 1);
piece1.setPosition(1, 1);
piece2.setPosition(101, 101);
assertTrue(B.putPiece(piece1));
assertFalse(B.putPiece(piece1));
assertFalse(B.putPiece(piece2));
}
@Test
public void testremovePiece() {
Board B = new Board("b");
Piece piece1 = new Piece("AAA", 1);
Position pa = new Position(1, 1);
Position pb = new Position(2, 2);
piece1.setPosition(1, 1);
B.putPiece(piece1);
assertTrue(B.removePiece(pa));
assertFalse(B.removePiece(pb));
}
@Test
public void testmove() {
Game game = new Game("a");
Board B = game.getGameBoard();
Player paPlayer = new Player();
paPlayer.setPlayerName("a");
paPlayer.setPlayerTurn(0);
Position pa = new Position(0, 0);
Position pb = new Position(2, 2);
Position pc = new Position(3, 3);
Position pd = new Position(999, 999);
assertTrue(B.move(paPlayer, pa, pb));
assertTrue(B.move(paPlayer, pb, pc));
assertFalse(B.move(paPlayer, pa, pb));
assertFalse(B.move(paPlayer, pd, pb));
}
@Test
public void testeat() {
Game game = new Game("a");
Board B = game.getGameBoard();
Player paPlayer = new Player();
paPlayer.setPlayerName("a");
paPlayer.setPlayerTurn(0);
Position pa = new Position(0, 0);
Position pb = new Position(2, 2);
Position pc = new Position(3, 3);
Position pd = new Position(999, 999);
assertTrue(B.move(paPlayer, pa, pb));
assertTrue(B.move(paPlayer, pb, pc));
assertFalse(B.move(paPlayer, pa, pb));
assertFalse(B.move(paPlayer, pd, pb));
}
测试Game:按照操作流程测试
public void testAssertionsEnabled() {
assert false;
}
public Game gametest =new Game("b");
@Test
public void testgettersetter() {
gametest.setNames("pl1", "pl2");
assertEquals("pl1", gametest.getPlayerA().getPlayerName());
assertEquals("pl2", gametest.getPlayerB().getPlayerName());
}
@Test
public void testaddnew() {
Piece testPiece1=new Piece("black",0);
Piece testPiece2=new Piece("white",1);
Piece testPiece3=new Piece("white",1);
Position P1 =new Position(1,1);
Position P2 =new Position(2,1);
Position P3 =new Position(3,1);
gametest.addnewPiece(gametest.getPlayerA(), testPiece1, P1);
gametest.addnewPiece(gametest.getPlayerB(), testPiece2, P2);
gametest.addnewPiece(gametest.getPlayerB(), testPiece3, P3);
assertTrue(gametest.getGameBoard().getBoardSet().contains(testPiece1));
assertTrue(gametest.getGameBoard().getBoardSet().contains(testPiece2));
assertTrue(gametest.getGameBoard().getBoardSet().contains(testPiece3));
}
测试结果:
实验进度记录
请使用表格方式记录你的进度情况,以超过半小时的连续编程时间为一行。
每次结束编程时,请向该表格中增加一行。不要事后胡乱填写。
不要嫌烦,该表格可帮助你汇总你在每个任务上付出的时间和精力,发现自己不擅长的任务,后续有意识的弥补。
日期 | 时间段 | 计划任务 | 实际完成情况 |
2020-03-12 | 18:30-21:50 | 完成P1问题的边实现 | 未完成 |
2020-03-16 | 19:00-24:00 | 完成P1问题的边实现 | 完成 |
2020-03-18 | 18:30-22:00 | 完成P1问题的顶点实现 | 完成 |
2020-03-18 | 22:00-23:50 | 完成P1 | 未完成 |
2020-03-19 | 18:30-24:00 | 完成P1 | 完成 |
2020-03-24 | 17:30-21:00 | 完成P2 | 完成 |
2020-03-28 | 09:00-22:00 | 完成P3 | 未完成 |
2020-04-04 | 13:00-21:00 | 完成P3 | 完成 |
2020-04-05 | 13:00-24:00 | 更新代码,完成实验报告 | 完成 |
实验过程中遇到的困难与解决途径
遇到的难点 | 解决途径 |
忘记怎么书写规约,AF,RI等 | 查看PPT |
Eclipse不会安装UML生成工具
| 查看博客 |
|
实验过程中收获的经验、教训、感想
实验过程中收获的经验和教训
在类的总体设计能力上仍有欠缺。协调这些类的能力也有欠缺。我得到的经验是:首先要明白自己要设计什么,写出测试,然后根据目标为导向进行程序设计会快得多。
针对以下方面的感受
- 面向ADT的编程和直接面向应用场景编程,你体会到二者有何差异?
- 使用泛型和不使用泛型的编程,对你来说有何差异?
- 在给出ADT的规约后就开始编写测试用例,优势是什么?你是否能够适应这种测试方式?
- P1设计的ADT在多个应用场景下使用,这种复用带来什么好处?
- P3要求你从0开始设计ADT并使用它们完成一个具体应用,你是否已适应从具体应用场景到ADT的"抽象映射"?相比起P1给出了ADT非常明确的rep和方法、ADT之间的逻辑关系,P3要求你自主设计这些内容,你的感受如何?
- 为ADT撰写specification, invariants, RI, AF,时刻注意ADT是否有rep exposure,这些工作的意义是什么?你是否愿意在以后编程中坚持这么做?
- 关于本实验的工作量、难度、deadline。
- 《软件构造》课程进展到目前,你对该课程有何体会和建议?
- 后者针对的是具体对象,在代码量和可复用性上都比面向过程要好得多。
- 泛型增加了程序的可扩展性。
- 以目标为导向进行程序设计,更加具有针对性。
- 这种复用实际上是提炼出了实物的一些本质属性,这些属性可能是抽象的,我们就可以对这些抽象的属性进行复用,比如P3的国际象棋和围棋有一些共同的属性,我们对其加以复用就能减少很多工作量。
- 避免内部变量被修改,很有必要,不然会出现一些很难察觉的bug
- 难度适中
- 无