项目 | 内容 |
---|---|
课程:北航计算机学院2020春季软件工程 | 班级博客地址 |
作业:结对编程项目——求解几何对象交点 | 项目要求 |
我在这个课程的目标是 | 提高编程能力,积累项目经验,提高团队协作能力 |
这个作业在哪个具体方面帮助我实现目标 | 提高协作能力,学习新的软件开发知识 |
1.项目地址
- 教学班级:006
- 项目地址:https://github.com/gzhGit/Intersectpro
2.项目PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
· Estimate | · 估计这个任务需要多少时间 | 10 | 10 |
Development | 开发 | ||
· Analysis | · 需求分析 (包括学习新技术) | 600 | 720 |
· Design Spec | · 生成设计文档 | 60 | 30 |
· Design Review | · 设计复审 (和同事审核设计文档) | 30 | 20 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 15 | 25 |
· Design | · 具体设计 | 120 | 150 |
· Coding | · 具体编码 | 400 | 600 |
· Code Review | · 代码复审 | 60 | 80 |
· Test | · 测试(自我测试,修改代码,提交修改) | 240 | 180 |
Reporting | 报告 | ||
· Test Report | · 测试报告 | 30 | 20 |
· Size Measurement | · 计算工作量 | 10 | 20 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 10 | 20 |
3.接口设计
- Information Hiding: 只提供几何对象类、计算类等类的信息,包括成员变量、成员函数,类内部函数实现细节(包括构造函数、求解交点函数等)对外隐藏。这一过程是通过使用动态链接库实现的,我设计了intersectDll动态链接库,将负责求解交点的模块封装起来。调用者可以使用其内部的类,比如Line(表示直线、射线、线段的类)、Circle(表示圆)、Intersect(内置一系列函数,负责计算不同几何对象的交点)。
- Interface Design: intersectDll暴露所有类的成员函数:Line(int,int,int,int,char),Circle(int,int,int),lineCrossLine(Line,Line),lineCrossCircle(Line,Circle),circleCrossCircle(Circle,Circle)等。调用者可以随意创建、使用几何对象,并通过调用Intersect类的一系列函数,求解不同几何对象的交点坐标。
- Loose Coupling: 我的这种设计其实并不符合松耦合的要求,因为计算模块的类没有对外隐藏,而是可以被引用,所以计算模块对外暴露的信息较多。这样导致模块之间的相互依赖程度较高,也是我设计的缺点所在。
4.计算模块接口的设计与实现过程
- 我设计了一个Intersect类,其内部包含三个函数:lineCrossLine(Line,Line),lineCrossCircle(Line,Circle),circleCrossCircle(Circle,Circle)。三个函数分别用来求解线(L,R,S型线)与线的交点、线与圆的交点、圆与圆的交点。将构造好的Line与Circle对象传入Intersect类的方法中,返回一个包含交点对象Node的容器vector。几种几何对象类只与Intersect类具有联系,相互之间没有依赖关系。
- 本次作业的关键在于直线类型产生了扩展,新增了射线与线段类型。对此,并没有必要构造新的集合对象类,只需要为Line对象增添一个标识kind,来代表它是直线、射线、线段这三种类型的哪一种。对于射线与线段类型,没有必要对求解的过程进行更改,可以把它们都看作是直线,先求解交点。得到求解的结果vector 之后,判定容器中的Node对象是否合法:对于射线,交点的横坐标x和x2应该在x1的同侧,纵坐标y和y2应该在y1的同侧;对于线段,焦点的横、纵坐标x和y,应该分别在x1和x2、y1和y2之间。如果合法,则保留该交点;不合法,则删除。
- 这样设计的好处,在于改动较少。只需要在Line中添加一个标识kind,以及一个判定交点是否合法的函数judgeLegal(),就可以完成扩展的功能。
5.UML图
6.计算接口部分的性能改进
-
本次作业中,在计算模块性能改进方面没有做太多的工作。只是将需要重复使用的几何对象的参数,在一开始提取出来,保存在局部变量里,算是做了一点小优化。性能分析图如下:
-
由于测试样例规模不大,耗时主要集中在文件IO函数上。除此之外,计算类的三个函数lineCrossLine()、lineCrossCircle()、circleCrossCircle()耗时较多,其中耗时最多的是lineCrossCircle()函数,即求解线与圆的交点。这是由于数学上的运算复杂度造成的,相比来说求解线和线的交点、圆和圆的交点,在数学上运算的步骤更少。找到一种开销更小的方法,或是在细节上优化其求解过程,成为突破性能瓶颈的关键。
7.契约式设计和代码协定
- 之前在oo课程中,曾经学习过JML语言,接触了一些关于契约式设计的相关知识。JML的重要内容是方法规格,包括核心三个方面:前置条件、后置条件和副作用约定。这对应着契约式设计的三大部分:前置条件、后置条件、类不变项。
- 优点: 对于编程者来说,契约式设计方法可以让设计思路更加清晰、严谨。逻辑严格的规格,让代码实现过程更加规范,有利于减少编码上的漏洞。对于维护者来说,代码具有规格说明,有助于理解代码,提高可维护性。
- 缺点: 规格设计需要耗费大量时间,降低工作效率,除非对于正确性要求极高,在实际工程中很难真正做到,且没有必要进行严格地契约式设计。
- 在项目中的体现:我在具体的代码实现过程中,附加了很多注释来说明函数的功能、数据的约束等,我觉得这从某种程度上体现了契约式设计的准则。例子如下:
class _declspec(dllexport) Node {
public:
//judgeCross指示该交点是否有几何意义,其取值只能为0或1。
//如果judgeCross=0,说明该交点没有几何意义。适用于以下情况:两几何对象虽没有交点,
//但求解交点的函数仍须返回Node对象,以保证函数返回值的统一性;
//如果judgeCross=1,说明该交点是两几何对象的交点。
int judgeCross = 0;
double x = 0.0, y = 0.0;
//重载 < 运算符,目的是使用unordered_set
bool operator < (const Node node) const;
Node(int judgeCross);
Node(int judgeCross, double x, double y);
};
8.计算模块部分单元测试展示
- 部分代码如下:
Line* line1 = new Line(0, 0, 1, 1, 'L');
Line* line2 = new Line(1, 1, 3, 0, 'R');
Line* line3 = new Line(2, 0, 2, -1123,'S');
Circle* circle4 = new Circle(10, 0, 1);
Circle* circle5 = new Circle(0, 0, 4);
Circle* circle6 = new Circle(3, 0, 1);
Circle* circle7 = new Circle(0, 0, 1);
Intersect i;
vector v0 = i.lineCrossLine(*line1, *line2);
vector v1 = i.lineCrossLine(*line1, *line3);
vector v2 = i.lineCrossCircle(*line1, *circle4);
vector v3 = i.circleCrossCircle(*circle5, *circle6);
vector v4 = i.circleCrossCircle(*circle5, *circle7);
Assert::AreEqual((int)v0.size(), 1);
Assert::AreEqual((int)v1.at(0).judgeCross, 0);
Assert::AreEqual((int)v2.at(0).judgeCross, 0);
Assert::AreEqual((int)(v3.size()), 1);
Assert::AreEqual(v3.at(0).x, 4.0);
Assert::AreEqual((int)(v4.at(0).judgeCross), 0);
- 单元测试覆盖求解线(L,R,S型线)与线的交点、线与圆的交点、圆与圆的交点的函数。并且构造特殊的线类型(与x轴垂直),与线(L,R,S型)和圆分别求交点。此外,构造相互平行的线,判断交点个数。对于求线与圆交点的情况,分别考虑相离、相切、相交;求圆与圆交点的情况,分别考虑相离、外切、相交、内切、内含。
9.计算模块部分异常处理说明
- 我们考虑到的异常情况主要有以下几大类:
1、几何对象类别异常,即输入了除'L'、'R'、'S'、'C'以外的字符来标识类别。
2、输入点的范围异常,即输入了(-100000, 100000)范围之外的坐标点。
3、输入点重复,即输入线类型的两个坐标点重合。
4、圆的半径异常,即输入了小于或等于0的半径。
10.界面模块的详细设计过程
- 我是用win32来实现界面模块的。在初始界面的menu中,添加一些输入几何参数的选项。每个输入选项绑定一个dialog,上面带有文本框来接收用户的输入。整个界面没有添加别的组件,用来绘制坐标轴、几何图形、交点坐标等。
- 核心的函数是CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM),负责处理主窗口的消息。主窗口menu中设置了“确定”选项,当参数输入完毕、点击确定之后,弹出消息窗口,如果输入合法,则调用InvalidateRect()函数重新绘制界面中的所有对象。代码如下:
case ID_QUEREN:
{
if ((lines[numofobj][0] == lines[numofobj][2]) &&
(lines[numofobj][1] == lines[numofobj][3])) {
MessageBox(hWnd, _T("输入了重复的两个点,请重新输入!"), _T("Coordinate error!"), MB_OK);
break;
}
numofobj++;
MessageBox(hWnd, _T("输入成功!"), _T("输入成功!"), MB_OK | MB_ICONINFORMATION);
InvalidateRect(hWnd, NULL, TRUE);
}
break;
- 绘制图像的代码,在处理WM_PAINT消息的模块之中。每当WndProc()接收了WM_PAINT消息,会调用如下代码:
case WM_PAINT:
{
hdc = BeginPaint(hWnd, &ps);
initPaint(hWnd);
SetBkMode(hdc, TRANSPARENT);
for (int i = 0; i < numofobj; i++) {
paintLine(hWnd, i,0);
}
int a, b;
nodeset.clear();
for (a = 0; a < numofobj - 1; a++) {
for (b = a + 1; b < numofobj; b++) {
intersect(hWnd,a, b);
}
}
//输出交点个数
string s = "当前交点个数:" + to_string(nodeset.size());
TCHAR S[64];
SetBkMode(GetDC(hWnd), TRANSPARENT);
MultiByteToWideChar(CP_ACP, 0, const_cast(s.c_str()), 64, S, 64);
TextOut(GetDC(hWnd), 50, 50, S, lstrlen(S));
EndPaint(hWnd, &ps);
}
break;
11.界面模块与计算模块的对接
- 界面模块与计算模块对接的位置,是在intersect()函数中,计算两个几何对象的交点,并在交点的位置绘制出坐标。几何对象的所有参数在之前的WndProc()中被接收,并保存在lines二维数组中。由于计算模块的所有类(几何对象类、计算类)对界面模块公开,所以可以根据lines中的参数构造相应几何对象(Line、Circle),之后调用Intersect类的一些函数,求出交点坐标。
- 下面是当A、B两几何对象均为线时,求解交点的对应代码:
Line* line1 = new Line(lines[i][0], lines[i][1], lines[i][2], lines[i][3]);
Line* line2 = new Line(lines[j][0], lines[j][1], lines[j][2], lines[j][3]);
Intersect* inter = new Intersect();
result = inter->lineCrossLine(*line1,*line2);
//判断交点是否合法
result = judgeLegalll(A, B, *line1, *line2, result);
//绘制交点坐标
paintIntersect(hwnd, result);
- 值得注意的是judgeLegalll和paintIntersect函数,这两个函数是在界面模块内部实现的。前者用于判断当几何对象为线段或射线时,求出的交点是否在几何对象上(求解过程中把它们都当作直线来计算)。后者是在界面中的相应位置绘制出交点坐标,为了避免绘制的字符串的背景覆盖几何图形,需要设置背景色为透明,即SetBkMode(hdc, TRANSPARENT)。paintIntersect函数存在一个设计上的问题,就是当交点的位置比较集中时,绘制的坐标可能会相互重叠。目前还没有想出一个比较好的解决办法。
12.描述结对的过程
- 由于网络原因,Live Share等实时视频交流方式效果较差。所以我和搭档的结对方式是分工合作,将任务划分为几个模块,各自完成相应的部分。之后通过微信语音、聊天等线上交流方式,进行问题的研讨、模块的整合。截图如下:
13.结对编程的优缺点
- 结对编程的优点,在于采用领航员-驾驶员的模式,一人负责编码、一人负责审核与指导,相当于不断地进行代码复审,有助于提高代码质量。结对过程中,两人的角色每隔一段时间进行轮换,这使得编码人员在工作一段时间之后可以放松、活动身体,而指导人员则以较好的身体与精神状态进行编码,有助于保持编码与指导过程的高效率、高质量。但是结对编程也有一定的缺点,比如结对的两人有时会发生建议冲突,或是要花费时间理解对方的思路。特别是如果结对的两人水平差距较大,配合不好,可能会产生负效益,造成工作效率低下。
- 结对二人的优缺点:
人员 | 优点 | 缺点 |
---|---|---|
我 | 1.目标明晰,2.不断钻研摸索,3.有耐心 | 时间安排不合理,赶DDL |
许天立 | 1.写代码很快,2.有钻研精神,3.合作能力强 | 和我一样,赶DDL |