软件工程结对项目作业
项目 | 内容 |
---|---|
本作业属于北航软件工程课程 | 2020春季计算机学院软件工程(罗杰 任建) |
本作业的要求请点击链接查看 | 2020BUAA软件工程结对项目作业 |
教学班级 | 005 |
Github项目地址 | https://github.com/syncline0605/IntersectPairProject |
我在这个课程的目标 | 提高自身的代码能力、学习团队协作开发的过程 |
本作业帮助我实现目标的具体方面 | 实践结对编程过程、学习使用Qt进行GUI开发、学习异常处理、学习封装dll、熟悉用VS进行C++开发的流程、熟悉VS带有的各种工具 |
1.在文章开头给出教学班级和可克隆的Github项目地址
- 教学班级:005
- 项目地址:https://github.com/syncline0605/IntersectPairProject
2.在开始程序之前,在下述PSP表格记录下你估计将在程序的各个模块的开发上耗费的时间。在实现完程序之后,记录下在各个模块上实际花费的时间。
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 5 | |
- Estimate | - 估计这个任务需要多少时间 | 5 | |
Development | 开发 | 840 | 1720 |
- Analysis | - 需求分析(包括学习新技术) | 180 | 900 |
- Design Spec | - 生成设计文档 | 30 | 30 |
- Design Review | - 设计复审(和同事审核设计文档) | 30 | 10 |
- Coding Standard | - 代码规范(为目前的开发制定合适的规范) | 60 | 30 |
- Design | - 具体设计 | 60 | 60 |
- Coding | - 具体编码 | 300 | 240 |
- Code Review | - 代码复审 | 60 | 0 |
- Test | - 测试(自我测试,修改代码,提交修改) | 120 | 450 |
Reporting | 报告 | 120 | 110 |
- Test Report | - 测试报告 | 30 | 30 |
- Size Measurement | - 计算工作量 | 30 | 20 |
- Postmortem & Process Improvement Plan | - 事后总结,并提出过程改进计划 | 60 | 60 |
合计 | 960 | 1830 | |
本次作业基本直接沿用上次的算法、调用上次的函数。在想清楚编程方法后非常简单,编码工作比预想的简单得多。测试工作也相对来说非常简单,特别是在完成GUI模块后,可以直接在GUI中直观地看到每个交点是否都被计算出,不用再借助其他工具。
本次作业最耗费时间的工作是学会各种新工具的使用。
- 发现一种错误的结构体初始化方式导致单元测试无法正常运行
- 下载查看单元测试覆盖率的插件并将其用于单元测试
- 下载配置Qt
以上几点都耗费了比编码和测试多得多的时间。
3.看教科书和其他资料中关于 Information Hiding,Interface Design,Loose Coupling 的章节,说明你们在结对编程中是如何利用这些方法对接口进行设计的
过去我在编程时常常是大片代码和函数放在同一个文件中,“一main到底”。在这次作业时,因为有充分的时间来解决这个并不困难的问题,我得以能够认真地设计代码架构。
我设计了定义了几何对象的基础模块,基于这个基础模块完成了命令行处理的输入输出模块和计算交点的计算模块。主函数要先引用这个基础模块,然后调用输入输出模块,得到基于基础模块保存的输入数据,然后将输入数据传入计算模块,将计算的结果再传入输入输入模块。这样一来,“得到输入文件”的过程、“将数据输出到文件”的过程和“计算交点”的过程就完全分离。两个模块的交流就仅仅是以基础模块定义为基础的数据结构。
因为不便进行完全面对面的结对编程,我和我的队友采用了近似分工的方式。基于我设计的计算模块的接口,我的队友非常轻松地理解了我的思路,并完成了GUI的编码。
至于松耦合的部分,因为时间原因我们没有去实现,但是我认为我们的代码在松耦合上面的实现并不太好。如上所述,所有的行为都是基于定义了几何对象的基础模块来说的,如果与别人交换模块,我还没有想好如何处理这个基础模块。另一方面,在后面的异常处理部分,有些异常发生在输入输入模块,有的发生在计算模块,有的异常在主函数中捕获,有的异常在计算模块中捕获,如果与别人交换模块,我不知道应该如何保证异常处理能够正常、完备地发挥作用。
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.计算模块接口部分的性能改进
使用一个随机生成的包含一千多条数据的测试集。
很明显,程序将大多数时间用在了set的元素插入上。
除此之外,基于确定的精确值对equals意义的重写也占用了大量时间
7.看 Design by Contract,Code Contract的内容,描述这些做法的优缺点,说明你是如何把它们融入结对作业中的
- http://en.wikipedia.org/wiki/Design_by_contract
- http://msdn.microsoft.com/en-us/devlabs/dd491992.aspx
The contracts take the form of pre-conditions, post-conditions, and object invariants. Contracts act as checked documentation of your external and internal APIs. The contracts are used to improve testing via runtime checking, enable static contract verification, and documentation generation. Code Contracts bring the advantages of design-by-contract programming to all .NET programming languages.
Code Contracts工具使用pre-conditions, post-conditions 和 object invariants来检查内部和外部的API。这一工具体现了design-by-contract编程的优点。
Design by contracts是一种设计软件的方式。一个简单的DBC(契约式设计),就是约束了某个方法调用的要求以及返回的承诺。与DBC区别的是“防范式设计”,它的设计前提是假设调用这个方法是恶意的,因此要对所有可能出错的输入进行检测,在这种设计模式下,程序会包含很多与业务逻辑无关的代码。
Design by contracts的优点:
- 调用者必须提供正确的参数,被调用者必须保证正确的结果和调用者要求的不变性,双方都有必须履行的义务,也有使用的权利,这样就保证了双方代码的质量,提高了软件工程的效率和质量
Design by contracts的缺点:
- 对于语言有一定的要求,DBC一般用断言来实现但并不是所有的程序语言都有断言机制
- DBC并未被标准化,代码造成很大混乱
我目前对这种编程方式的体会还不是很深刻。我在代码中尽量保证了单个函数实现明确的单一功能,我认为大多数时候是运用了DBC思想的。但是我并不是很明确这种DBC的方法如何与异常处理结合,我平时的编程思维与DBC距离还有多远。
至于在结对式编程上的应用,我的体会并不是很深,二人在结对编程时,函数的作用往往已是公认商量好的。我的理解中,DBC是一种组织代码的思想,与结对这种编程方法关系不是很大。
8.计算模块部分单元测试显示
如上文的模块设计部分展示的函数定义,计算模块分为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
。
单元测试样例:(命令行参数为"-d")
交点个数为无穷个
若两个图形有无数个交点,则抛出异常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.描述结对的过程
13.说明结对编程的优点和缺点。同时描述结对的每一个人的优点和缺点在哪里。
结对编程
-
优点
- 边编码边复审,思维更严密,减少低级bug
- 两人可能掌握不同的技能、有各自的编程特点,能够融合优点,通过互相学习,在对方的指导下更快地掌握新技能。
- 集思广益,通过讨论得到更好的结构设计
-
缺点
- 如果两人技术能力差距过大,会拖慢进度,结对编程的效率甚至不如较强的一方单独编程
- 如果有一方或两方的责任心都不强,会互相甩锅,或者将两人的代码分得很清、不关心对方的代码效果如何
我
- 优点:代码架构思考较深入;编程习惯好、代码风格优;对解决问题比较执着
- 缺点:学习新东西速度慢,有时会要求队友直接告诉我而不是自主解决;有很多无谓的较真、工作效率较低
队友
- 优点:学习能力强、学习新工具的速度快;交流主动积极、热心;执行力强、效率高
- 缺点:有时候不太注意代码风格