软件构造Lab2心得
2019年春季学期
计算机学院《软件构造》课程
Lab 2实验报告
目录
1 实验目标概述 1
2 实验环境配置 1
3 实验过程 1
3.1 Poetic Walks 1
3.1.1 Get the code and prepare Git repository 1
3.1.2 Problem 1: Test Graph 1
3.1.3 Problem 2: Implement Graph 1
3.1.3.1 Implement ConcreteEdgesGraph 2
3.1.3.2 Implement ConcreteVerticesGraph 2
3.1.4 Problem 3: Implement generic Graph 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.1.5.1 Test GraphPoet 2
3.1.5.2 Implement GraphPoet 2
3.1.5.3 Graph poetry slam 2
3.1.6 Before you’re done 2
3.2 Re-implement the Social Network in Lab1 2
3.2.1 FriendshipGraph类 2
3.2.2 Person类 3
3.2.3 客户端main() 3
3.2.4 测试用例 3
3.2.5 提交至Git仓库 3
3.3 Playing Chess 3
3.3.1 ADT设计/实现方案 3
3.3.2 主程序ChessGame设计/实现方案 3
3.3.3 ADT和主程序的测试方案 3
3.4 Multi-Startup Set (MIT) 4
4 实验进度记录 4
5 实验过程中遇到的困难与解决途径 4
6 实验过程中收获的经验、教训、感想 4
6.1 实验过程中收获的经验和教训 4
6.2 针对以下方面的感受 4
1 实验目标概述
本次实验训练抽象数据类型(ADT)的设计、规约、测试,并使用面向对象 编程(OOP)技术实现 ADT。具体来说:
针对给定的应用问题,从问题描述中识别所需的 ADT;
设计 ADT 规约(pre-condition、post-condition)并评估规约的质量;
根据 ADT 的规约设计测试用例;
ADT 的泛型化;
根据规约设计 ADT 的多种不同的实现;针对每种实现,设计其表示 (representation)、表示不变性(rep invariant)、抽象过程(abstraction function)
使用 OOP 实现 ADT,并判定表示不变性是否违反、各实现是否存在表 示泄露(rep exposure);
测试 ADT 的实现并评估测试的覆盖度;
使用 ADT 及其实现,为应用问题开发程序;
2 在测试代码中,能够写出 testing strategy 并据此设计测试用例。实验环境配置
实验环境:eclipse
在这里给出你的GitHub Lab2仓库的URL地址(Lab2-学号)。
https://github.com/ComputerScienceHIT/Lab2-1180300625
3 实验过程
请仔细对照实验手册,针对三个问题中的每一项任务,在下面各节中记录你的实验过程、阐述你的设计思路和问题求解思路,可辅之以示意图或关键源代码加以说明(但千万不要把你的源代码全部粘贴过来!)。
3.1 Poetic Walks
分别建立两个类ConcreteEdgesGraph,ConcreteVerticesGraph 实现Graph接口。Graph接口要求实现add(添加新节点),set(添加新边),remove(移除节点),vertices(获得所有的节点集合),sources(target)获得以target为目标节点的边的起始节点,targes(source)获得以source为起始节点的边的目标节点。还要求将ADT泛型化。并且编写测试文件的时候,需要从实现的基本功能出发,有良好的测试覆盖率。
Poet:给定一组单词(文件输入),对于两个相邻的单词a和b,认为存在一条由a到b的有向边,通过Graph接口构造有向图。再给定一由单词组成的句子,如果句子中两个相邻单词之间在Graph图中有一个中间单词则将中间单词插入到两单词之间(如果有多个则插入权重最大的那个)即由input和图中的映射关系,得出最后的poem。
3.1.1 Get the code and prepare Git repository
在同学群里要到的代码
3.1.2 Problem 1: Test Graph
默认ConcreteEdgesGraph implements Graph,修改empty为:
public static Graph empty() {
return new ConcreteEdgesGraph();
//throw new RuntimeException(“not implemented”);
}
此时可以运行GraphStaticTest进行测试。
// Testing strategy for add
// 测试先添加相同点,再添加相同点,检查返回值
// Testing strategy for set
// 测试对已有点添加新边,对不存在的点添加边
// 并改变已存在的边的权值和删除,检查返回值
// Testing strategy for remove
// 测试分别对已有点和不存在的点删除
// 并改变已存在的边的权值和删除,检查返回值、其前驱节点的后继节点、点集
// Testing strategy for Vertices
// 测试返回的点集的size和是否包含已添加的点
// Testing strategy for Sources
// 测试一个单独点的sources
// 有source的点的返回的map的键值对是否与设置的一致
// Testing strategy for Targets
// 测试一个单独点的targets
// 有target的点的返回的map的键值对是否与设置的一致
3.1.3 Problem 2: Implement Graph
以下各部分,请按照MIT页面上相应部分的要求,逐项列出你的设计和实现思路/过程/结果。
3.1.3.1 Implement ConcreteEdgesGraph
- Edge类
// Abstraction function:
// TODO start,end是一条边的两个顶点;weight为边的权值
// Representation invariant:
// TODO start和end的长度不为null且start != end两个字串不为同一个字串;weight>0;
// Safety from rep exposure:
// TODO 使用private使得其不会暴漏,用final让其值无法改变
private final L start, end;//起点,终点
private int edgeweight; //Edge里的边权值
//构造函数
public Edge(L source, L target, int weight) {
this.start = source;
this.end = target;
this.edgeweight = weight;
checkRep();
}
2.ConcreteEdgesGraph类
private final Set<L> vertices = new HashSet<>();
private final List<Edge<L>> edges = new ArrayList<>();
// Abstraction function:
// TODO vertices是图的顶点集,edges是图的边集
// Representation invariant:
// TODO 顶点集元素个数>=1,边集元素个数>=0
// Safety from rep exposure:
// TODO 使用private final数据类型来防止数据被修改和暴露
// 使用unmodifiableSet将返回值变为immutable类型
- add(L vertex):返回set中的add方法,即如果里面有相同点则返回false
- set(L source, L target, int weight):先判断vertices中是否有source和target,若没有,则加上;然后遍历edges,寻找source到target的边,若找到,就删除并记录此时的权值,遍历后判断若weight>0,则把set的参数制成的新边加入edges
- remove(L vertex):若vertices中没有该点,直接返回false;若有,遍历edges,把与之相关的边全部删除,最后把vertices中该点删除
- Set vertices():返回vertices
- Map<L, Integer> sources(L target):在edges中寻找所有目标点是target的初始节点(加权),通过stream的Map-filter-reduce的处理流程,返回对应键值对
- Map<L, Integer> targets(L source):在edges中寻找所有目标点是source的初始节点(加权),通过stream的Map-filter-reduce的处理流程,返回对应键值对
- 测试结果:
3.1.3.2 Implement ConcreteVerticesGraph
1.Vertex类
// TODO fields
private L p;
private Map<L,Integer> startto = new HashMap<>();
private Map<L,Integer> tostart = new HashMap<>();
// Abstraction function:
// TODO s表示顶点名称,startto储存p点指向的点和权值,tostart储存指向p点的点和权值
//
// TODO s不能指向空,startto和tostart中不能含有p;如果点p指向q,则s的startto含q,q的tostart含p
// Safety from rep exposure:
// TODO 使用private修饰变量;String为 immutable类型;使用防御式编程
public Vertex(L name) {
this.p = name;
startto = new HashMap<L, Integer>();
tostart = new HashMap<L, Integer>();
checkRep();
}
三个域的getter和setter,其中setter返回之前的值,方便set调用
public int setsource(L source, int weight) {
int x;
if (startto.containsKey(source))
x = startto.get(source);
else
x = 0;
if (weight > 0)
startto.put(source, weight);
else
startto.remove(source);
checkRep();
return x;
}
2.ConcreteVertices类
private final List<Vertex> vertices = new ArrayList<>();
// Abstraction function:
// TODO vertices为图的顶点集,点之间映射为边
// Representation invariant:
// TODO vertices的个数为顶点的个数 ;如果点s指向p,则s的gettostart含p,p的getstartto含s
// Safety from rep exposure:
// TODO 使用防御式编程
// TODO constructor
- add(L vertex):先寻找vertices中的name有没有与vertex中相同,若没有返回false,若有将以vertex为name的新边加到vertices中,返回true
- set(L source, L target, int weight):先判断vertices中是否有source和target,若没有,则加上;然后遍历vertices,寻找source和target点,若找到,就通过setTarget和setSource传入weight进行处理,返回两个函数的返回值
- remove(L vertex):遍历vertices,将其中map的键中的vertex删除,若有vertices中name是vertex,将其删除,并返回true;若没有,返回false
- Set vertices():遍历vertices,把每个name加到set中,最后返回set
- Map<L, Integer> sources(L target):遍历vertices,寻找name是target的点,通过 getSource返回他的map
- Map<L, Integer> targets(L source):遍历vertices,寻找name是source的点,通过 getTarget返回他的map
3.1.4 Problem 3: Implement generic Graph
3.1.4.1 Make the implementations generic
- 将具体类的声明更改为:
public class ConcreteEdgesGraph implements Graph { … }
class Edge { … }
public class ConcreteVerticesGraph implements Graph { … }
class Vertex { … } - 使用占位符L代替,更新两个实现以支持任何类型的顶点标签String。粗略地说,您应该能够使用!查找和替换String代码中的所有实例L。这种重构将Concrete¬EdgesGraph,Concrete¬VerticesGraph,Edge,和Vertex泛型类型。
3.1.4.2 Implement Graph.empty()
修改Graph.empty为:
public static Graph empty() {
return new ConcreteEdgesGraph();
//throw new RuntimeException(“not implemented”);
}
3.1.5 Problem 4: Poetic walks
3.1.5.1 Test GraphPoet
public void testRun() throws IOException {
GraphPoet poet;
poet =new GraphPoet(new File(“src/P1/poet/seven-words.txt”));
final String word =“This is a test.I am hemingxuan!!!”;
assertEquals(“This is a test.I am hemingxuan!!!”, poet.poem(word));
}
给定一个input。从文件中读取poet,再调用Graph.poem()后观察输出与预期是否相等
3.1.5.2 Implement GraphPoet
- public GraphPoet(File corpus) throws IOException:先按行读取文件,并每行要针对制表符或空格对该字符串进行分割,并且在将分割好的存放单词的list,一个个读取,并用前一个单词与后一个单词利用set函数将权重为1的边加到graph中:若set返回的是0,说明之前无边,可以结束;若不为0,说明之前有边,所以再次set权重为之前返回值加一
for (int i = 0; i <words.size() - 1; i++) {
int weight = graph.set(words.get(i), words.get(i + 1), 1);
if (weight != 0)
graph.set(words.get(i), words.get(i + 1), weight + 1);
} - public String poem(String input):将input用空格分隔成字符串数组,通过寻找前一个单词的target与后一个单词的source的值相加,寻找一个最大值,就是这两个单词之间需要穿插的单词,若两个之间没有相同的键,则不需要添加单词。
for (String key : tarMap.keySet()) {
if (souMap.containsKey(key))
tarMap.put(key, tarMap.get(key) + souMap.get(key));
else
tarMap.put(key, 0);
if (tarMap.get(key) > a)
str = key + " ";
} - // Abstraction function:
// 将输入的文本单词提取作为顶点
// 构建有向图并将其转化为poem
// Representation invariant:
// 输入的文本words不为空
// 有向图不为null
// Safety from rep exposure:
// 所有fields都是private final
3.1.5.3 Graph poetry slam
If I could save time in a bottle
the first thing that I’d like to do
is to save every day until eternity passes away
just to spend them with you
If I could make days last forever
if words could make wishes come true
I d save every day like a treasure and then
again I would spend them with you
3.1.6 Before you’re done
请按照http://web.mit.edu/6.031/www/sp17/psets/ps2/#before_youre_done的说明,检查你的程序。
使用git bath提交
在这里给出你的项目的目录结构树状示意图。
3.2 Re-implement the Social Network in Lab1
这个实验是基于在Poetic Walks中定义的Graph及其两种实现,重新实现Lab1中的 FriendshipGraph类。我们需要复用ConcreteEdgesGraph或 ConcreteVerticesGraph中已经实现的add()和set()方法,而不是从零开始。另外基于所选定的 ConcreteEdgesGraph 或 ConcreteVerticesGraph的rep来实现,而不能修改父类的rep。
3.2.1 FriendshipGraph类
private List<String>namelist = new ArrayList<>(); //建立名字的集合列表
private final ConcreteEdgesGraph<Person> people = new ConcreteEdgesGraph<Person>();
继承ConcreteEdgesGraph
- public void addVertex(Person newperson)直接用add方法,返回add的返回值即可;
- public void addEdge(Person a, Person b)直接用public int set(L source, L target, int weight),若set返回是0,说明之前无边,则返回true;若不为0,说明之前有边,则返回false;
- public int getDistance(Person a,Person b)在广度搜索需要该节点的后继节点时改为调用target方法
Map<Person,Integer> friendlist = people.targets(person1); //把person1的朋友列表赋给friendlist
for(Person i : friendlist.keySet()) { //从头开始循环,直到到达friendlist尾部
3.2.2 Person类
删除person类中除name之外的部分
3.2.3 客户端main()
3.2.4 测试用例
测试了添加相同点,添加已添加过的边,不连通的两点距离、自身距离、连通点的距离,均没有任何问题。
assertEquals(1, graph.getDistance(rachel, ross));
assertEquals(2, graph.getDistance(rachel, ben));
assertEquals(0, graph.getDistance(rachel, rachel));
assertEquals(-1, graph.getDistance(rachel, kramer));
3.2.5 提交至Git仓库
使用git bath提交。
在这里给出你的项目的目录结构树状示意图。
3.3 Playing Chess
3.3.1 ADT设计/实现方案
设计了哪些ADT(接口、类),各自的rep和实现,各自的mutability/ immutability说明、AF、RI、safety from rep exposure。
必要时请使用UML class diagram(请自学)描述你设计的各ADT间的关系。
- Action
private final Board board;
// Abstraction function:
// 刻画棋盘上基础的变化
// gameBoard为棋盘
// Representation invariant:
Method:
- putPiece(Piece piece):读取参数piece的position,将piece放到board中boardpiece数组的对应位置
- movePiece(Piece piece, Position position):将piece放到board中boardpiece数组与position对应位置
- removePiece(Piece piece):读取参数piece的position,将piece从board中boardpiece数组的对应位置删除
- Board
private final Piece[][] boardPiece;
// Abstraction function:
// 刻画棋盘
// boardPiece为棋盘上的位置信息
// Representation invariant:
// boardPiece中若元素非空,则该棋子在初始位置
// Safety from rep exposure:
Method:
-
setBoardPiece(Piece piece, Position position):将position处的piece替换为参数的piece
-
getPiece(Position position):返回指定position的piece
3.Game
private final Board gameBoard;
private final Action action;
private final Player player1, player2;
private int num1, num2;// Abstraction function:
// 刻画游戏中相比action更细节的动作
// boardPiece为棋盘,action为对棋盘的基础操作
// player1,player2为两个玩家,num1,num2分别为player1, player2的棋盘上的棋子总数
// Representation invariant:
// 棋盘,动作,玩家不为空
// num1,num2>=0,
// Safety from rep exposure:
// gameBoard,action,player1, player2为private final,不可改变
// Step采用防御性拷贝
public Game(boolean flag, Player player1x, Player player2x) {
参数flag为true时表示初始化国际象棋;为false时表示初始化围棋
/**- 如果第一个位置无棋子或棋子不属于该玩家,打印相关信息后返回
- 如果第二个位置无棋子或棋子属于该玩家,打印相关信息后返回
- 否则用该玩家在某一位置的棋子吃掉某一位置处的另一名玩家棋子,并将该操作信息以适当的形式加到该玩家的走棋历史中
- @param player3 由该player进行的操作
- @param position1 棋盘上的某一位置
- @param position2 棋盘上的某一位置
/
public void eat(Player player3, Position position1, Position position2)
/* - 在棋盘上的某一位置处放置属于该玩家的棋子
- 并将该操作信息以适当的形式加到该玩家的走棋步骤中
- 如果该位置已有棋子,打印相关信息后直接返回
- @param player3 由该player进行的操作
- @param position 棋盘上的某一位置
/
public void put(Player player3, Position position)
/* - 将棋盘上的某一位置处的棋子移动到另一位置
- 并将该操作信息以适当的形式加到该玩家的走棋历史中
- 但如果第一个位置无棋子或有棋子不属于该玩家或第二个位置有棋子,打印相关信息后直接返回
- @param player3 由该player进行的操作
- @param position1 棋盘上的某一位置
- @param position2 棋盘上的某一位置
/
public void move(Player player3, Position position1, Position position2)
/* - 将棋盘上的某一位置处的棋子提走
- 并将该操作信息以适当的形式加到该玩家的走棋步骤中
- 但如果此位置无棋子或有棋子属于该玩家,打印相关信息后直接返回
- @param player3 由该player进行的操作
- @param position 棋盘上的某一位置
*/
public void remove(Player player3, Position position)
4.Piece
// fields
private final String name;
private Position position;
private final Player player;
// Abstraction function:
// 描述棋子名字、位置和所属玩家
// name为棋子名字 , position为棋子位置,player为所属玩家
// Representation invariant:
// name,position,player均非空
// Safety from rep exposure:
// name,player为private final,不可改变
// position,name采用防御性拷贝
Method:
方法:pName,position,player的getter以及position的setter
5.Player
// fields
private final String name;
private String step;
// Abstraction function:
// 写玩家的名字及他下棋的步骤
// name为玩家名字 , step为下棋的步骤
// Representation invariant:
// 玩家姓名不能为空
// Safety from rep exposure:
// name为private final,不可修改
// step采用防御性拷贝
Method:
public void addStep(String str)//添加步骤
6.Position
// fields
private final int x, y;
// Abstraction function:
// 显示棋盘位置或棋子所在位置
// x为横坐标 , y为纵坐标
// Representation invariant:
// 横纵坐标不能为负数
// Safety from rep exposure:
// x,y均为private final
public String getPosition() {
return "(" + x + "," + y + ")";
}
3.3.2 主程序MyChessAndGoGame设计/实现方案
辅之以执行过程的截图,介绍主程序的设计和实现方案,特别是如何将用户在命令行输入的指令映射到各ADT的具体方法的执行。
首先调用gamestart让用户选择国际象棋或围棋。输入用户名建立两个new game。打印菜单之后程序根据玩家的输入显示相应的提示信息,告诉玩家该如何操作。在国际象棋中吃子就调用eat();移动调用move();在围棋中移动用move,放子用put();移除用remove();
3.3.3 ADT和主程序的测试方案
各个动作都是针对于Game类的,所以测试主要针对Game类
// Testing strategy for Game
// 分别测试新建国际象棋、围棋的game,并检查棋子数
@Test
public void testGame()
// Testing strategy for eat
// 分别测试参数的两个位置没有棋子、选择了敌手的棋子不做改动
// 再测试正常吃子时,检查此时的棋盘上对应位置的状态与现有棋子总数
@Test
public void testEat()
// Testing strategy for put
// 分别测试参数位置已有棋子时不做改动
// 再测试正常放子时,检查此时的棋盘上对应位置的状态与现有棋子总数
@Test
public void testPut()
// Testing strategy for move
// 分别测试参数第一个位置没有棋子、棋子的归属权不正确、第二个位置已有棋子时不做改动
// 再测试正常移子时,检查此时的棋盘上对应位置的状态与现有棋子总数
@Test
public void testMove()
// Testing strategy for remove
// 分别测试参数位置没有棋子、拿错棋子时不做改动
// 再测试正常提子时,检查此时的棋盘上对应位置的状态与现有棋子总数
@Test
public void testRemove()
5 实验过程中遇到的困难与解决途径
遇到的难点 解决途径
刚写P1的时候非常懵,不知道具体要干什么。
先将问题2给写了,把整个代码全补充完,突然发现第一个的问题
P3工作量太大,完全不知道从哪里下手
回看视频,又在CSDN上找前辈的心得,一点一点的写。终于完成了
P3测试出现了好多问题
静下心来慢慢找bug,还请教了同学帮我找bug。终于将bug全部修复了
6 实验过程中收获的经验、教训、感想
6.1 实验过程中收获的经验和教训
经验:熟练使用Java的库,并且懂得了规约。这对我们写代码的帮助十分大。
教训:写程序不要着急,而是要仔细斟酌问题是什么。然后将问题分为几步,一步一步的写。
6.2 针对以下方面的感受
(1) 面向ADT的编程和直接面向应用场景编程,你体会到二者有何差异?
答:ADT的编程就是强迫让自己将应用场景抽象化。
(2) 使用泛型和不使用泛型的编程,对你来说有何差异?
答:更加方便,这样不用修改原始代码。
(3) 在给出ADT的规约后就开始编写测试用例,优势是什么?你是否能够适应这种测试方式?
答:让我们有明确的目标,测试时头脑更清晰。
(4) P1设计的ADT在多个应用场景下使用,这种复用带来什么好处?
答:程序更轻便,通俗易懂。
(5) P3要求你从0开始设计ADT并使用它们完成一个具体应用,你是否已适应从具体应用场景到ADT的“抽象映射”?相比起P1给出了ADT非常明确的rep和方法、ADT之间的逻辑关系,P3要求你自主设计这些内容,你的感受如何?
答:p1的问题都给你了,按照规约一步步写就行了。而P3则是从头开始写难度大,但是相应的*度很高。
(6) 为ADT撰写specification, invariants, RI, AF,时刻注意ADT是否有rep exposure,这些工作的意义是什么?你是否愿意在以后编程中坚持这么做?
答:让别人更好理解,自己再次阅读时能更快的想到自己当初的目的。并且如果程序出现bug,能更快修复。
(7) 关于本实验的工作量、难度、deadline。
答:很难,难度相当大。
(8) 《软件构造》课程进展到目前,你对该课程有何体会和建议?
答:对我写代码影响很大,纠正了我以前写代码的不良习惯。