项目 | 内容 |
---|---|
这个作业属于哪个课程 | 2020春季计算机学院软件工程(罗杰 任健) |
这个作业的要求在哪里 | 结对项目作业 |
我在这个课程的目标是 | 进一步提高自己的编码能力,工程能力,团队协作能力 |
这个作业在哪个具体方面帮助我实现目标 | 学习了c++模块化方法,以及图形化界面的编写方式 |
教学班级 | 006 |
项目地址 | https://github.com/NSun-S/BUAA_SE_PairWork.git |
目录
- 一、写在前面
- 二、关于Information Hiding,Interface Design,Loose Coupling的实现
- 三、计算模块的实现
- 四、计算模块的UML图
- 五、计算模块的性能改进
- 六、关于Design by Contract,Code Contract
- 七、计算模块的单元测试
- 八、计算模块的错误处理
- 九、界面模块的设计
- 十、界面模块和计算模块的对接
- 十一、模块松耦合的实现
- 十二、描述结对的过程
- 十三、对结对编程的评价
一、写在前面
本次作业功能实现比较简单,但是我第一次对一个项目进行封装,所以在后面封装和实现GUI的时候感觉比较困难。还有在进行模块松耦合的时候,由于事先没跟对接的组做好商议,后面浪费了很长时间去修改。
不过这次作业带给我的收获是巨大的,首先,我学会了模块封装,其次我学会了用Qt怎么写UI界面。我还知道了模块要实现松耦合要注意哪些问题。这些收获为后面团队项目打下了重要的基础。
下述 PSP 表格记录了我在程序的各个模块的开发上耗费的时间:
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
· Estimate | · 估计这个任务需要多少时间 | 30 | 30 |
Development | 开发 | ||
· Analysis | · 需求分析 (包括学习新技术) | 200 | 300 |
· Design Spec | · 生成设计文档 | 30 | 50 |
· Design Review | · 设计复审 (和同事审核设计文档) | 20 | 20 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 60 | 120 |
· Design | · 具体设计 | 30 | 30 |
· Coding | · 具体编码 | 540 | 500 |
· Code Review | · 代码复审 | 120 | 200 |
· Test | · 测试(自我测试,修改代码,提交修改) | 240 | 300 |
Reporting | 报告 | ||
· Test Report | · 测试报告 | 50 | 120 |
· Size Measurement | · 计算工作量 | 20 | 20 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 20 | 10 |
合计 | 1360 | 1700 |
二、关于Information Hiding,Interface Design,Loose Coupling的实现
Information Hiding(信息隐藏原则): 这是David Parnas在1972年最早提出信息隐藏的观点,他在其论文中指出:代码模块应该采用定义良好的接口来封装,这些模块的内部结构应该是程序员的私有财产,外部是不可见的。所以我们在作业中设计接口的时候,接口的输入输出信息都是string、int、double这种类型,完全不会体现出我们设计的Line类和Circle类。
Interface Design(接口设计): 通过实现addLine(),addCircle(),deleteLine(),deleteCircle()函数,可以对内部的数据结构进行添加删除操作,这保证了虽然内部信息是隐藏的,但是却可以通过接口来进行更改。
Loose Coupling(松耦合): 我们和另一个小组通过接口的统一,实现了模块松耦合,替换核心计算模块以后,程序仍然可以正常运行。
三、计算模块的实现
1. 内部设计
在这一部分我们经过讨论,沿用了上次我的设计,在我个人作业基础上进行了拓展,仍然保持了上次作业的两个类——直线类和圆类,在直线类中,我们新加了一个属性类型,用来区分直线是直线型、射线型还是线段型,因为在求解交点的过程中,三种“直线”的求解方式是完全一样的,我们只需要根据其类型判断交点在不在其对应的区域上即可,这个类中除了上次的求解直线和直线交点的函数,增加了一个判断交点是不是在“直线”上的函数;在圆类中,和上次作业几乎完全一致,仍然是求垂足、求直线和圆的距离、求圆和圆的交点、求圆和直线的交点四个方法,区别在于在求垂足时需要暂时将直线类型统一设置为直线型,来避免垂足被判为不在直线上。直线类和圆类在功能上是一种协同关系。
2. 外部接口
我们将项目的主类作为对外的接口,在里面实现了solve,ioHandler等函数接口,用于求解交点,以及添加、删除几何对象,从文件中读取几何对象,在类内创建了保存现有几何对象的容器,在每个函数内调用直线类或圆类的方法来实现功能。这个类可以说是一个顶层类,是连接外部和内部的枢纽。
3. 流程图(以ioHandler和solve为例)
4. 算法关键及独到之处
算法的关键在于各类图形间交点的求解,这是我们的任务所在,也是整个模块功能的核心。我认为算法中的独到之处如下:
- 3类直线型对象的统一处理,在交点求解时以统一的函数来求解交点,在求解之后判断点在不在对应的几何对象上,短短数十行代码就实现了功能的拓展。
- 交点存储方式的选取,在交点的存储上,我们选择了用vector存储后排序去重的方式,其性能上的优势非常明显,对于一个10000个几何对象2900w+交点的数据,在未去重时,我和另一组同学的运行时间分别为4.97s和5.21s,在排序去重后运行时间分别为10s和31s,可见我们这种处理方式的优势是非常明显的。
四、计算模块的UML图
五、计算模块的性能改进
1. 性能的改进
这一部分与其说是性能的改进,不如说是bug的修复,在性能上我们已经取得了不错的效果,但有两个问题一开始处理的比较差,下面我们来分析一下这两方面的问题。
- 判断点在线段、射线上。我们一开始采用了计算距离的方式,理论上说这种方式没有任何问题,在计算精确度足够高的条件下,这种方法自然没有问题,但在我们完成作业后和同学进行比对时,发现在一个6000+条直线类几何对象上,我们的交点数目能相差40000多个,不愿意相信的是,就是这个判断点在不在直线上的函数造成的,距离的计算引入了1e-6级别的误差,让两个原本应该重合的点不再重合,将判断条件改成之间判断横纵坐标(都是整数),问题才得以解决。
- 判断圆相切。这个问题和上面的问题一样,同样是精度问题,但这个问题的解决过程要漫长的多,在600w+个交点上我们的结果多了两个,经过一晚上复杂的排查,锁定了两组几何对象,下面以其中一组进行说明。这是一个直线和圆相切的例子,(L, -272, 469, 673, 973)和(-401, 968, 501),我们的程序计算出两个交点,原因是直接使用==来判断相切,这带来了1e-5级别的误差,我们使用wolfram平台绘图验证了其相切,并计算出了交点(见下图)。最后改变了相切判断条件,问题才得以解决。
2. 性能分析
下图是我们使用VS性能分析工具分析的结果。
可以看出,对vector的排序花费了较多的时间。其中,消耗最大的函数是slove,因为全部的交点求解过程都是在这个函数里面完成的,下面是这个函数的代码。
六、关于Design by Contract,Code Contract
契约式设计就是按照某种规定对一些数据等做出约定,如果超出约定,程序将不再运行,例如要求输入的参数必须满足某种条件。在我们的作业中,接口的设计以及松耦合的实现均使用了契约式设计原则。这个原则的好处是可以预先定义好接口,方便把握软件的整体架构,也方便开发者和使用者进行对接,还有就是方便维护,在维护的同时原有功能可以继续使用,维护完成后替换核心功能部分代码即可。
七、计算模块的单元测试
在这次作业中我们切实感受到了单元测试的重要性,在每次增加新功能后进行回归测试,可以很容易的发现问题,设计上主要就是对新添功能的测试,以及一些边缘问题的测试。
1. 部分单元测试代码展示
TEST_CLASS(testinterface_solve)
{
TEST_METHOD(method1)
{
deleteAll();
vector> myIntersections;
ioHandler("../testinput2.txt");
solve(myIntersections);
int answer = myIntersections.size();
Assert::AreEqual(26, answer);
}
};
这是在我们写好ioHandler和solve接口后进行单元测试的样例,测试了通过接口进行交点计算。
TEST_CLASS(testinterface_ad)
{
TEST_METHOD(method1)
{
vector> myIntersections;
//ioHandler("../testinput2.txt");
addLine(-1, 4, 5, 2, LINE);
addLine(2, 4, 3, 2, SEGMENT);
addLine(2, 5, -1, 2, RAY);
addCircle(3, 3, 3);
solve(myIntersections);
int answer = myIntersections.size();
Assert::AreEqual(5, answer);
}
TEST_METHOD(method2)
{
vector> myIntersections;
//ioHandler("../testinput2.txt");
deleteCircle(3,3,3);
deleteLine(2, 5, -1, 2, RAY);
solve(myIntersections);
int answer = myIntersections.size();
Assert::AreEqual(1, answer);
}
};
这是我们写好addLine(Circle)和deleteLine(Circle)接口后进行单元测试的样例,测试了通过接口进行几何对象的增添和删除。同时消除代码中的所有Warning。
2. 单元测试覆盖率截图
从图中可以看出我们单元测试覆盖了93%的内容,剩下没有覆盖的部分大多为函数头和main函数中的内容。
八、计算模块的错误处理
直线型对象给定两点重复
TEST_METHOD(method1)
{
try
{
addLine(-1, 4, -1, 4, LINE);
}
catch (const char* msg)
{
Assert::AreEqual("Error: two points of a line should be different", msg);
}
}
这一错误是对于直线型几何对象,其输入的两点坐标重合。
坐标值越界
TEST_METHOD(method2)
{
try
{
addLine(-1000000, 4, -1, 4, LINE);
}
catch (const char* msg)
{
Assert::AreEqual("Warning: your coordinate value is out of bound", msg);
}
}
这一错误针对所有几何对象,正确的数据坐标值应限定在(-100000,100000)。
圆半径出现非正数
TEST_METHOD(method3)
{
try
{
addCircle(-10, 4, -1);
}
catch (const char* msg)
{
Assert::AreEqual("Error: circle's radius should be a positive integer", msg);
}
}
这一类错误针对圆类型几何对象,圆的半径应该为正值。
未定义类型标识
TEST_METHOD(method5)
{
deleteAll();
vector> myIntersections;
try
{
ioHandler("../undefined.txt");
}
catch (const char* msg)
{
Assert::AreEqual("Error: unexcepted type mark", msg);
}
}
这一类错误针对出现除'L','S','R','C'之外的类型标识符。
九、界面模块的设计
本次作业的界面模块,我们使用了Qt进行设计,并使用了Qt的开源库QCustomPlot进行图像绘制。主要包括6个函数。
QioHandler():
从文件中读取数据并进行计算。
void myGUI::QioHandler()
{
string input = ui.fileInput->toPlainText().toStdString();
ioHandler(input);
fstream inputfile(input);
int n;
inputfile >> n;
for (int i = 0; i < n; i++)
{
char type;
inputfile >> type;
if (type == 'L' || type == 'R' || type == 'S')
{
int tempType = -1;
if (type == 'L') tempType = LINE;
else if (type == 'R') tempType = RAY;
else if (type == 'S') tempType = SEGMENT;
double x1, x2, y1, y2;
inputfile >> x1 >> y1 >> x2 >> y2;
lines.push_back(UILine(x1, y1, x2, y2, tempType));
}
else if (type == 'C')
{
double c1, c2, r;
inputfile >> c1 >> c2 >> r;
circles.push_back(UICircle(c1, c2, r));
}
}
Qsolve();
}
QdeleteAll():
删除所有几何对象。
void myGUI::QdeleteAll()
{
deleteAll();
ui.widget->clearItems();
ui.widget->clearGraphs();
lines.clear();
circles.clear();
Qsolve();
}
QaddLine():
添加直线。
void myGUI::QaddLine()
{
string newType = ui.newType->toPlainText().toStdString();
//通过ui读入两个点
double x1 = ui.newX1->toPlainText().toDouble();
double y1 = ui.newY1->toPlainText().toDouble();
double x2 = ui.newX2->toPlainText().toDouble();
double y2 = ui.newY2->toPlainText().toDouble();
//判断类型
int type = -1;
if (newType == "L") {
type = LINE;
}
else if (newType == "R") {
type = RAY;
}
else if (newType == "S") {
type = SEGMENT;
}
//执行接口的addLine函数
addLine(x1, y1, x2, y2, type);
lines.push_back(UILine(x1, y1, x2, y2, type));
//重新计算交点
Qsolve();
}
QdeleteLine():
删除直线。
void myGUI::QdeleteLine()
{
string newType = ui.newType->toPlainText().toStdString();
double x1 = ui.newX1->toPlainText().toDouble();
double y1 = ui.newY1->toPlainText().toDouble();
double x2 = ui.newX2->toPlainText().toDouble();
double y2 = ui.newY2->toPlainText().toDouble();
int type = -1;
if (newType == "L") type = LINE;
else if (newType == "R") type = RAY;
else if (newType == "S") type = SEGMENT;
deleteLine(x1, y1, x2, y2, type);
for (auto iter = lines.begin(); iter != lines.end(); iter++)
{
if (iter->x1 == x1 && iter->y1 == y1 && iter->x2 == x2 && iter->y2 == y2 && iter->type == type)
{
lines.erase(iter);
break;
}
}
//在删除线后清屏
ui.widget->clearItems();
ui.widget->clearGraphs();
//重新绘制几何对象并求解交点
Qsolve();
}
圆的函数和直线类似,就不再赘述了。这里面的Qsolve()函数功能是使用QCustomPlot中的QCPItem模块绘制所有几何图形,并执行接口中的solve()函数,根据结果绘制交点。所以每次添加删除几何对象后都执行Qsolve()函数,可以实现图像的自动更新。
十、界面模块和计算模块的对接
本次作业我们采用动态链接库(dll)的方式进行模块对接。
首先在计算模块中实现这些函数:
void solve(vector> & realIntersections) throw(const char*);
void ioHandler(string input) throw(const char*);
void addLine(double x1, double y1, double x2, double y2, int type) throw(const char*);
void deleteLine(double x1, double y1, double x2, double y2, int type);
void addCircle(double c1, double c2, double r) throw(const char*);
void deleteCircle(double c1, double c2, double r);
void deleteAll();
然后再函数声明前加_declspec(dllexport)
,就可以在dll中实现这些函数的接口,然后界面模块导入计算模块的dll,即可使用这些函数。
如上一节所述,我们写好了界面模块的几个函数,然后在ui的按钮中添加对这些函数的链接,即可在ui中实现点击功能。如图:
在编译运行后,从input.txt中导入图形,即可实现交点求解和图像绘制功能。如图:
十一、模块松耦合的实现
合作小组两位同学:17373456,17373459
我方运行对方core.dll成功的截图:
对方运行我方core.dll成功的截图:
我方使用命令行运行对方core.dll的截图:
对接过程中出现的问题:
- GUI.exe和core.dll的编译方式不一致,导致互换core.dll之后无法运行。
- 解决方法:统一使用Release x64的模式进行编译。
- 接口函数的关键词不一致,我方采用__cdecl,对方使用的默认。
- 解决方法:对方将函数声明添加__cdecl关键词,即可正常运行。
十二、描述结对的过程
由于疫情原因,这次结对项目不能面对面进行讨论。所以在结对的过程中我们经历了大致几个阶段,使用live share+腾讯会议阶段,这一阶段一开始感觉很新奇,在计算模块的设计过程中我们都采用这一模式,后来在图形界面设计时由于live share编译时非常不稳定,且双方都没有接触过QT设计图形界面,共同探索效率较低,因此我们采用了腾讯会议连线加桌面共享的方式,这样有问题可以随时讨论,也可以方便展示最新的成果,下面是我们两个阶段的截图。
此外,我们通过热身作业中学到的pull request操作,共同维护了一个GitHub仓库,实现了代码的交互管理。
十三、对结对编程的评价
结对编程的优点:首先在面对困难的时候可以集思广益,更快地解决问题,比如一开始写图形界面的时候我们都不知道怎么写,然后我们都上网上去找资料,最后把我们找到的信息合并起来,就完成了图形界面的编写,如果一个人写的话可能会写一部分但是另一部分不会写,效率就很低了。其次可以减少细小错误的发生,因为大部分时间都是一个人写另一个人去检查他的代码,所以这在一定程度上保证了代码的质量。
结对编程的缺点:结对编程需要代码理解能力很强,我们不仅要知道自己在写什么,还要知道队友在写什么,他的逻辑是什么样的,以及是否正确,所以整个过程比较累。同时,写个人项目时我们自己有一个完整的思维流程,但这在结对项目中是不适用的,我们总是要根据队友的思维变化而不断调整我们的思维,难以形成对项目的整体把控。
我的优点:对工程文件的组织能力强,对代码结构的把控比较好,比较适合框架设计。
我的缺点:算法设计不如队友。
队友的优点:做事耐心,有责任心,对算法设计比较好
队友的缺点:有时会比较粗心,代码中会犯一些小错误。