BUAA2020软工作业(四)——结对项目

项目 内容
这个作业属于哪个课程 2020春季计算机学院软件工程(罗杰 任健)
这个作业的要求在哪里 结对项目作业
我在这个课程的目标是 进一步提高自己的编码能力,工程能力,团队协作能力
这个作业在哪个具体方面帮助我实现目标 学习了c++模块化方法,以及图形化界面的编写方式
其他参考文献
教学班级 006
项目地址 https://github.com/NSun-S/BUAA_SE_PairWork.git

目录

一、写在前面

本次作业功能实现比较简单,但是我第一次对一个项目进行封装,所以在后面封装和实现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<pair<double, double>> myIntersections;
            ioHandler("../testinput2.txt");
            solve(myIntersections);
            int answer = myIntersections.size();
            Assert::AreEqual(26, answer);
        }
    };

这是在我们写好ioHandler和solve接口后进行单元测试的样例,测试了通过接口进行交点计算。

TEST_CLASS(testinterface_ad)
    {
        TEST_METHOD(method1)
        {
            vector<pair<double, double>> 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<pair<double, double>> 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<pair<double, double>> 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<pair<double, double>> & 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成功的截图:

对接过程中出现的问题:

  • GUI.exe和core.dll的编译方式不一致,导致互换core.dll之后无法运行。
    • 解决方法:统一使用Release x64的模式进行编译。
  • 接口函数的关键词不一致,我方采用__cdecl,对方使用的默认。
    • 解决方法:对方将函数声明添加__cdecl关键词,即可正常运行。

十二、描述结对的过程

由于疫情原因,这次结对项目不能面对面进行讨论。所以在结对的过程中我们经历了大致几个阶段,使用live share+腾讯会议阶段,这一阶段一开始感觉很新奇,在计算模块的设计过程中我们都采用这一模式,后来在图形界面设计时由于live share编译时非常不稳定,且双方都没有接触过QT设计图形界面,共同探索效率较低,因此我们采用了腾讯会议连线加桌面共享的方式,这样有问题可以随时讨论,也可以方便展示最新的成果,下面是我们两个阶段的截图。

十三、对结对编程的评价

结对编程的优点:首先在面对困难的时候可以集思广益,更快地解决问题,比如一开始写图形界面的时候我们都不知道怎么写,然后我们都上网上去找资料,最后把我们找到的信息合并起来,就完成了图形界面的编写,如果一个人写的话可能会写一部分但是另一部分不会写,效率就很低了。其次可以减少细小错误的发生,因为大部分时间都是一个人写另一个人去检查他的代码,所以这在一定程度上保证了代码的质量。

结对编程的缺点:结对编程需要代码理解能力很强,我们不仅要知道自己在写什么,还要知道队友在写什么,他的逻辑是什么样的,以及是否正确,所以整个过程比较累。同时,写个人项目时我们自己有一个完整的思维流程,但这在结对项目中是不适用的,我们总是要根据队友的思维变化而不断调整我们的思维,难以形成对项目的整体把控。

我的优点:对工程文件的组织能力强,对代码结构的把控比较好,比较适合框架设计。

我的缺点:算法设计不如队友。

队友的优点:做事耐心,有责任心,对算法设计比较好

队友的缺点:有时会比较粗心,代码中会犯一些小错误。

猜你喜欢

转载自www.cnblogs.com/shanyanbo/p/12560349.html