• 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成功的截图:

    我方使用命令行运行对方core.dll的截图:

    对接过程中出现的问题:

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

    十二、描述结对的过程

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

    此外,我们通过热身作业中学到的pull request操作,共同维护了一个GitHub仓库,实现了代码的交互管理。

    十三、对结对编程的评价

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

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

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

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

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

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

  • 相关阅读:
    request内置对象(上)1
    康拓展开-----两个排列的位置之差
    判断一个数的质因子个数
    学生信息管理系统----(顺序表)
    学生信息管理系统----(链表)
    二分图的最大匹配--匈牙利算法
    hdu-1285拓扑排序
    文件的压缩与解压
    树莓派安装QT(全部库包括)
    Win10 + CLion + 树莓派 + QT 远程开发调用Python
  • 原文地址:https://www.cnblogs.com/shanyanbo/p/12560349.html
Copyright © 2020-2023  润新知