结对项目作业
1.在文章开头给出教学班级和可克隆的 Github 项目地址。
项目 | 内容 |
---|---|
这个作业属于哪个课程 | 2020春季计算机学院软件工程(罗杰 任健) |
这个作业的要求在哪里 | 作业要求 |
班级 | 006 |
项目地址 | https://github.com/syncline0605/IntersectPairProject |
2.在开始实现程序之前,在下述 PSP 表格记录下你估计将在程序的各个模块的开发上耗费的时间。
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
· Estimate | · 估计这个任务需要多少时间 | 10 | |
Development | 开发 | ||
· Analysis | · 需求分析 (包括学习新技术) | 30 | |
· Design Spec | · 生成设计文档 | 10 | |
· Design Review | · 设计复审 (和同事审核设计文档) | 10 | |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | |
· Design | · 具体设计 | 90 | |
· Coding | · 具体编码 | 300 | |
· Code Review | · 代码复审 | 100 | |
· Test | · 测试(自我测试,修改代码,提交修改) | 300 | |
Reporting | 报告 | ||
· Test Report | · 测试报告 | 30 | |
· Size Measurement | · 计算工作量 | 10 | |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 20 | |
合计 | 920 |
3.看教科书和其它资料中关于 Information Hiding,Interface Design,Loose Coupling 的章节,说明你们在结对编程中是如何利用这些方法对接口进行设计的。
Information Hiding
David Parnas在1972年最早提出信息隐藏的观点。他指出:代码模块应该采用定义良好的接口来封装,这些模块的内部结构应该是程序员的私有财产,外部是不可见的。
Interface Design
接口的设计原则:
- 依赖倒置原则:高层模块不应该依赖底层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。
- 接口隔离原则:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。
Loose Coupling
耦合的强度依赖于:一个模块对另一个模块的调用、一个模块向另一个模块传递的数据量、一个模块施加到另一个模块的控制的多少、模块之间接口的复杂程度等。
松耦合意味着模块与模块之间可分拆、少依赖。
我们在结对编程时,在图形模块中调用计算模块的接口来进行计算。对于图形界面的编写者而言,无需知道计算模块的细节。两个模块可以各自独立进行开发。
4.计算模块接口的设计与实现过程。
我们的独到之处在于针对线段与射线的新功能的实现基本都是对原有的直线相关函数的调用,计算交点时先将线段或射线转化为它们所在的直线,直线计算出交点后再判断该点是否在线段或射线上,极大地简化了代码逻辑。
因为考虑到封装的几何对象仅保存数据,不涉及方法,因此用struct封装几何对象
struct Point {//点
double x;
double y;
double length;
bool operator ==(const Point& b) const noexcept
{
if (compareDouble(x - b.x) == 0 && compareDouble(y - b.y) == 0) return true;
return false;
}
bool operator <(const Point& b) const noexcept
{
if (compareDouble(x - b.x) == 0 && compareDouble(y - b.y) < 0) return true;
if (compareDouble(x - b.x) < 0) return true;
return false;
}
};
typedef Point Vector; //向量
struct Line { //直线
Point p1, p2;
};
struct Segment { //线段
Point p1, p2;
};
struct Ray { //射线
Point start, direction;
};
struct Circle { //圆
Point center;
double r;
};
通过命令行处理函数,获得输入输出文件的名称。从输入文件中得到所有的几何对象,将同类的几何对象存入同一个vector中。将全体交点的Point
集合存入一个set
。
vector<Line> lineSet;
vector<Segment> segmentSet;
vector<Ray> raySet;
vector<Circle> circleSet;
set<Point> pointSet;
计算交点的calPoint
函数在同一个vector内部进行或是在两个不同类vector间进行。计算交点的getPoint
函数在两个几何对象间进行,返回计算出的交点个数:NOCROSS
,ONECROSS
,TWOCROSS
,MANYCROSS
(无数个交点)。
计算交点的函数定义如下
int getPoint(Line l1, Line l2, Point& crossPoint) noexcept;
int getPoint(Line l, Segment s, Point& crossPoint) noexcept;
int getPoint(Line l, Ray r, Point& crossPoint) noexcept;
int getPoint(Line l, Circle c, pair<Point, Point>& crossPair) noexcept;
int getPoint(Segment s1, Segment s2, Point& crossPoint) noexcept;
int getPoint(Segment s, Ray r, Point& crossPoint) noexcept;
int getPoint(Segment s, Circle c, pair<Point, Point>& crossPair) noexcept;
int getPoint(Ray r1, Ray r2, Point& crossPoint) noexcept;
int getPoint(Ray r, Circle c, pair<Point, Point>& crossPair) noexcept;
int getPoint(Circle c1, Circle c2, pair<Point, Point>& crossPair) noexcept;
int calPoint(vector<Line>& lineSet, set<Point>& pointSet);
int calPoint(vector<Line>& lineSet, vector<Segment>& segmentSet, set<Point>& pointSet);
int calPoint(vector<Line>& lineSet, vector<Ray>& raySet, set<Point>& pointSet);
int calPoint(vector<Line>& lineSet, vector<Circle>& circleSet, set<Point>& pointSet);
int calPoint(vector<Segment>& segmentSet, set<Point>& pointSet);
int calPoint(vector<Segment>& segmentSet, vector<Ray>& raySet, set<Point>& pointSet);
int calPoint(vector<Segment>& segmentSet, vector<Circle>& circleSet, set<Point>& pointSet);
int calPoint(vector<Ray>& raySet, set<Point>& pointSet);
int calPoint(vector<Ray>& raySet, vector<Circle>& circleSet, set<Point>& pointSet);
int calPoint(vector<Circle>& circleSet, set<Point>& pointSet);
int calPoint(vector<Line>& lineSet, vector<Segment>& segmentSet, vector<Ray>& raySet, vector<Circle>& circleSet, set<Point>& pointSet);
将线段或射线转换为直线、判断点是否在一线段或射线范围内的关键函数:
//将线段转化成对应的直线
//将横坐标较小的点作为p1,若两点横坐标相同则将纵坐标较小的点作为p1
Line segmentToLine(Segment s) {
Line l;
if ((s.p1.x < s.p2.x) || ((s.p1.x == s.p2.x) && (s.p1.y < s.p2.y))) {
l.p1 = s.p1;
l.p2 = s.p2;
} else {
l.p1 = s.p2;
l.p2 = s.p1;
}
return l;
}
//将射线转化成对应的直线
Line rayToLine(Ray r) {
Line l{ r.start, r.direction };
return l;
}
//判断一个点是否在一线段的坐标范围内
int pointIfOnSeg(Point p, Line l)
{
if (l.p1.x == l.p2.x) {
if ((p.y >= l.p1.y) && (p.y <= l.p2.y)) {
return ON;
} else {
return NOTON;
}
} else {
if ((p.x >= l.p1.x) && (p.x <= l.p2.x)) {
return ON;
} else {
return NOTON;
}
}
}
//判断一个点是否在一射线的坐标范围内
int pointIfOnRay(Point p, Line l)
{
if (l.p2.x < l.p1.x) {
//若射线指向负方向
if (p.x <= l.p1.x) {
return ON;
} else {
return NOTON;
}
} else if (l.p2.x == l.p1.x && l.p2.y < l.p1.y) {
//若射线指向正下方
if (p.y <= l.p1.y) {
return ON;
} else {
return NOTON;
}
} else if (l.p2.x == l.p1.x && l.p2.y > l.p1.y) {
//若射线指向正上方
if (p.y >= l.p1.y) {
return ON;
} else {
return NOTON;
}
} else {
//若射线指向正方向
if (p.x >= l.p1.x) {
return ON;
} else {
return NOTON;
}
}
}
5.画出 UML 图显示计算模块部分各个实体之间的关系。
6.计算模块接口部分的性能改进。记录在改进计算模块性能上所花费的时间,描述你改进的思路,并展示一张性能分析图(由VS 2015/2017的性能分析工具自动生成),并展示你程序中消耗最大的函数。
使用一个随机生成的包含一千多条数据的测试集。
很明显,程序将大多数时间用在了set的元素插入上。
除此之外,基于确定的精确值对equals意义的重写也占用了大量时间
7.看 Design by Contract,Code Contract 的内容,描述这些做法的优缺点,说明你是如何把它们融入结对作业中的。
优点:
- 规定了前置、后置和不变量约束条件,如果不满足这些约束,程序将抛出异常。事先规定好这些约束条件,能够对程序的正确性有所保证。
- 提高程序的规范性,使程序更加便于阅读和理解。
缺点:
- 如果想要关闭这些约束条件,需要专门编写一个模块来关闭它。
- 运行时性能会受到影响。
我们在结对作业中并没有专门用一个模块去规定约束条件,但对程序中需要规定的条件也进行了约束,如点的坐标需要在(-100000,100000)范围内,如果不满足,则抛出异常。
8.计算模块部分单元测试展示。展示出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路。并将单元测试得到的测试覆盖率截图,发表在博客中。要求总体覆盖率到 90% 以上,否则单元测试部分视作无效。
如上文的模块设计部分展示的函数定义,计算模块分为L-L,L-R,L-S,L-C,S-S,S-R,S-C,R-R,R-C,C-C几个函数,只要分别覆盖了这些这些计算交点的getPoint
函数,就能覆盖其调用的其他计算函数。为了覆盖所有calPoint
函数,再编写一个较大的测试集测试包含所有类别的完整calPoint
函数即可实现较好的覆盖率。
//测试圆与圆交点的多种情况,除保证计算交点个数正确外,还要保证计算的交点坐标正确
TEST_METHOD(Circle_Circle_TwoCross)
{
Circle c1;
Circle c2;
c1.center.x = 0; c1.center.y = 0; c1.r = 2;
c2.center.x = 2; c2.center.y = 0; c2.r = 2;
Point realPoint1, realPoint2;
pair<Point, Point> testPair;
realPoint1.x = 1; realPoint1.y = 1.73205081;
realPoint2.x = 1; realPoint2.y = -1.73205081;
Assert::IsTrue(getPoint(c1, c2, testPair) == 2);
Assert::IsTrue(((realPoint1 == testPair.first) && (realPoint2 == testPair.second)) || ((realPoint2 == testPair.first) && (realPoint1 == testPair.second)));
}
TEST_METHOD(Circle_Circle_OneCross)
{
Circle c1;
Circle c2;
c1.center.x = 0; c1.center.y = 0; c1.r = 2;
c2.center.x = 4; c2.center.y = 0; c2.r = 2;
Point realPoint1, realPoint2;
pair<Point, Point> testPair;
realPoint1.x = 2; realPoint1.y = 0;
Assert::IsTrue(getPoint(c1, c2, testPair) == 1);
Assert::IsTrue(realPoint1 == testPair.first);
}
TEST_METHOD(Circle_Circle_NoCross)
{
Circle c1;
Circle c2;
c1.center.x = 0; c1.center.y = 0; c1.r = 2;
c2.center.x = 5; c2.center.y = 0; c2.r = 2;
Point realPoint1, realPoint2;
pair<Point, Point> testPair;
Assert::IsTrue(getPoint(c1, c2, testPair) == 0);
}
TEST_METHOD(TestAll)
{
vector<Line> lineSet;
vector<Segment> segmentSet;
vector<Ray> raySet;
vector<Circle> circleSet;
set<Point> pointSet;
Line l1, l2; Segment s1, s2, s3; Ray r1, r2, r3; Circle c1, c2;
l1.p1.x = 1; l1.p1.y = 2; l1.p2.x = 3; l1.p2.y = 3;
l2.p1.x = 4; l2.p1.y = 2; l2.p2.x = 2; l2.p2.y = -4;
lineSet.push_back(l1); lineSet.push_back(l2);
s1.p1.x = -5; s1.p1.y = 1; s1.p2.x = 5; s1.p2.y = -3;
s2.p1.x = 5; s2.p1.y = 3; s2.p2.x = 2; s2.p2.y = 2;
s3.p1.x = 6; s3.p1.y = -5; s3.p2.x = 6; s3.p2.y = 6;
segmentSet.push_back(s1); segmentSet.push_back(s2);
segmentSet.push_back(s3);
r1.start.x = 5; r1.start.y = 2; r1.direction.x = 3; r1.direction.y = 4;
r2.start.x = 2; r2.start.y = -4; r2.direction.x = -1; r2.direction.y = -1;
r3.start.x = -10; r3.start.y = 1; r3.direction.x = 10; r3.direction.y = 1;
raySet.push_back(r1); raySet.push_back(r2); raySet.push_back(r3);
c1.center.x = 2; c1.center.y = 3; c1.r = 4;
c2.center.x = 5; c2.center.y = 2; c2.r = 2;
circleSet.push_back(c1); circleSet.push_back(c2);
Assert::AreEqual(calPoint(lineSet, segmentSet, raySet, circleSet, pointSet), 31);
}
覆盖率截图,计算模块GeoCalculate.pp
的覆盖率达到了90%:
9.计算模块部分异常处理说明。在博客中详细介绍每种异常的设计目标。每种异常都要选择一个单元测试样例发布在博客中,并指明错误对应的场景。
设计了以下几类异常:
- 非法输入格式
- 输入几何对象不足
- 文件尾部输入过多内容
- 坐标范围超出限制
- 命令行参数是其他字符
- 交点个数为无数个
非法输入格式
当遇到不符合要求的输入数据时,如文件开始未出现n、出现非法字符、几何对象的数据不正确时,抛出异常 illegalInputPattern
单元测试样例:
输入几何对象不足
从文件的首行获得n,但是当在文件之后输入数据不足n个时,抛出异常notEnoughInputElement
单元测试样例:
文件尾部输入过多内容
当读入n和n个几何对象的数据后,文件尾还有多余内容,抛出异常TooManyInputElements
单元测试样例:
坐标范围超出限制
对于每个输入数据,判断是否在(-100000,100000)范围内,如果不在,则抛出异常outRangeException
。当圆的输入半径小与或等于0时,也会抛出这一异常。
单元测试样例:
命令行参数是其他字符
若命令行参数不正确,则抛出异常commandException
。
单元测试样例:
交点个数为无穷个
若两个图形有无数个交点,则抛出异常infException
。
单元测试样例:
10.界面模块的详细设计过程。在博客中详细介绍界面模块是如何设计的,并写一些必要的代码说明解释实现过程。
界面模块使用Qt Creator进行开发,设计了两个窗口,一个是主界面dialog,另一个是画图界面new_window。
主界面如图所示:
接下来逐一介绍所实现的四个功能:
-
打开文件
void Dialog::readFile()
函数使用了
getOpenFileName
方法获取文件路径,并打开文件,用readLine
方法逐行读取文件内容,并用split
方法对字符串按空格进行分割,存储到相应的结构中。最后使用ui->label->setText
方法将读取的内容显示在界面上。点击“打开文件”按钮并选择文件后效果如图所示:
-
添加图形
void Dialog::addone()
函数使用
ui->text->toPlainText
方法获取文本框中的字符串,用split
方法分割并存储到相应的结构中。最后使用ui->label_2->setText
方法将所添加的数据显示在界面上。点击“添加”按钮后效果如图所示:
-
删除图形
void Dialog::deleteone()
函数使用
ui->text->toPlainText
方法获取文本框中的字符串,用split
方法分割,找到该数据对应的元素并删除。最后使用ui->label_2->setText
方法将所删除的数据显示在界面上。点击“删除”按钮后效果如图所示:
-
绘制图形和交点
void Dialog::open()
函数打开新窗口new_window,重写
paintEvent
方法,进行图像绘制。绘图方法:
- 线:drawLine
- 圆:drawEllipse
- 点:drawPoint
图形绘制效果如图所示:
11.界面模块与计算模块的对接。详细地描述 UI 模块的设计与两个模块的对接,并在博客中截图实现的功能。
计算交点需要用到calPoint
方法,它的接口是五个结构体的集合,分别代表直线、线段、射线、圆和点。我的想法是把界面模块中的结构体也包装成相同的形式,直接调用该方法进行计算即可。将Release版的动态链接库和相关头文件添加到项目中,即可直接调用该方法。
功能实现展示
从文件中添加3个图形,并手动添加1个图形(射线),如图所示:
点击“绘制图形和交点”按钮后效果如图所示:
从文件中添加4个图形,并手动删除1个图形(线段),如图所示:
点击“绘制图形和交点”按钮后效果如图所示:
12.描述结对的过程,提供两人在讨论的结对图像资料(比如 Live Share 的截图)。
我们使用了qq的共享屏幕功能,结合在微信上讨论,来完成结对编程。
13.看教科书和其它参考书,网站中关于结对编程的章节,说明结对编程的优点和缺点。同时描述结对的每一个人的优点和缺点在哪里(要列出至少三个优点和一个缺点)。
结对编程的优点
- 有随时的复审和交流,能够减少程序中的错误,提高程序的初始质量,节省以后修改、测试的时间
- 两人一起配合,是一个互相督促的过程,比起一个人单干更不容易开小差和拖延
- 能够互相学习到对方的优点和长处,提高编程能力
结对编程的缺点
- 两人可能会产生分歧和矛盾
- 如果团队的人员不能保证充足的结对编程时间,成员需要经常等待,浪费时间
- 对新手来说,需要一定的时间来适应这种开发方式,不适合时间紧迫的项目
队友的优点
- 代码风格很好,编程能力很强,注释清楚,一目了然
- 善于沟通,一起讨论解决问题,采纳彼此的意见,我们在结对编程的过程中没有发生任何争执或矛盾,合作十分愉快
- 态度非常积极,始终在积极地合作完成项目,没有拖延和鸽子的情况
队友的缺点
- 有时会出现一些粗心小bug,但都能够立刻发现并改正
我的优点
-
学习能力较强,面对新的问题,能够找到解决问题的方法并付诸实施
-
态度也很积极,不拖延、没有浪费彼此的时间
-
编程时比较细心、认真
我的缺点
- 代码风格较差,写注释较少
14.在你实现完程序之后,在附录提供的PSP表格记录下你在程序的各个模块上实际花费的时间。
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
· Estimate | · 估计这个任务需要多少时间 | 10 | 10 |
Development | 开发 | ||
· Analysis | · 需求分析 (包括学习新技术) | 30 | 60 |
· Design Spec | · 生成设计文档 | 10 | 10 |
· Design Review | · 设计复审 (和同事审核设计文档) | 10 | 10 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 10 |
· Design | · 具体设计 | 90 | 150 |
· Coding | · 具体编码 | 300 | 450 |
· Code Review | · 代码复审 | 100 | 200 |
· Test | · 测试(自我测试,修改代码,提交修改) | 300 | 450 |
Reporting | 报告 | ||
· Test Report | · 测试报告 | 30 | 30 |
· Size Measurement | · 计算工作量 | 10 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 20 | 10 |
合计 | 920 | 1400 |