项目 | 内容 |
---|---|
这个做业属于那个课程 | 班级博客 |
这个做业的要求在哪里 | 做业要求 |
我在这个课程的目标是 | 学习软件工程相关知识,加强本身的开发能力。 |
这个做业在哪一个具体方面帮我实现目标 | 学习结对编程的技巧和方法 |
sample | Sample |
教学班级:005java
项目地址:https://github.com/harrychen23235/MultiIntersectc++
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
· Estimate | · 估计这个任务须要多少时间 | 20 | 25 |
Development | 开发 | ||
· Analysis | · 需求分析 (包括学习新技术) | 120 | 300 |
· Design Spec | · 生成设计文档 | 30 | 30 |
· Design Review | · 设计复审 (和同事审核设计文档) | 20 | 20 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 40 | 30 |
· Design | · 具体设计 | 30 | 20 |
· Coding | · 具体编码 | 120 | 180 |
· Code Review | · 代码复审 | 30 | 60 |
· Test | · 测试(自我测试,修改代码,提交修改) | 120 | 120 |
Reporting | 报告 | ||
· Test Report | · 测试报告 | 30 | 30 |
· Size Measurement | · 计算工做量 | 20 | 20 |
· Postmortem & Process Improvement Plan | · 过后总结, 并提出过程改进计划 | 240 | 240 |
合计 | 820 | 1025 |
此次的做业主要难点是新知识的学习,包括dll库的生成以及qt的学习,其中qt对vs生成dll的不支持也让我DEBUG了很是长的时间。在架构上此次的程序沿用了以前的架构,所以具体设计时并无花费太多的时间,可是测试时仍然发现了很多的问题。在结对编程上,最大的难点是对双方的代码不熟悉同时远程合做极为不方便,所以将任务一分为二,我负责UI开发以及程序实现,另外一位同窗主要负责对于程序功能进行测试。git
信息隐藏、接口设计、松耦合都是软件工程以及面向对象实现的重要方法。目标是使得模块的调用者不须要关注模块的细节部分,只须要按照接口说明进行操做就能获得但愿的结果。同时调用者也没法修改模块内容,或者反推出模块源代码。github
信息隐藏:算法
接口设计:编程
松耦合:架构
核心模块的主要功能包括对于图像的添加,计算交点个数功能。app
一、首先是对于具体节点的处理。创建了自定义的point类,并采用unordered_set结构进行储存,内部采用相似java的hashset进行储存。因为采用的为自定义类,同时须要对于浮点数double进行hash,所以重写了equal和hash函数,具体以下所示:函数
unordered_set<Point*, Hash_Point, Equal_Point> g_allpoint; struct Hash_Point { size_t operator()(const class Point* input1)const { long temp1 = floor(input1->x); long temp2 = floor(input1->y); if (abs(input1->x - (temp1+1)) <= EPS) temp1++; if (abs(input1->y- (temp2+1)) <= EPS) temp2++; return (temp1 + temp2) * 13 + (temp1 * 1000 % 1000 + temp2 * 1000 % 1000); } }; struct Equal_Point { bool operator()(const class Point* input1, const class Point* input2)const { return abs(input1->x - input2->x) <= EPS && abs(input1->y - input2->y) <= EPS; } };
须要特别注意的是对于double的处理,在试运行时发现无论是floor仍是强制转换为int操做,均可能会出现BUG。例若有几率把(double)1转换成0或者是1,出现二义性。所以在转换完成以后必须新增一步if判断操做。牺牲部分效率确保正确性。工具
二、其次是对于图形类的具体实现以及图像的添加。对于新增的射线和线段类,让它们继承直线类,射线类新增方向属性以及起始节点,以下所示:
class Seg :public Line { public:double mx2, my2, mx1, my1; Seg(double input1, double input2, int ifspecial, double input3, double input4, double input5, double input6); };,而线段类则新增对于2个端点的记录。以下所示: class Ray :public Line { public:double mx1, my1, mx2, my2; int direction;//1,2,3,4表示延长到的象限位置,-1表示朝x轴正方向延伸,-2,表示朝x轴负方向延伸,-3表示向y轴正方向延伸,-4表示向y轴负方向延伸 Ray(double input1, double input2, int ifspecial, double input3, double input4, double input5, double input6); };
图形的产生依然使用工厂模式,在其中增长了对于线段和射线类的处理:
Shape* ShapeFactory::GetShape(string type, double temp1, double temp2, double temp3, double temp4) { if (type == "L") { if (temp1 == temp3) return new Line(temp1, 0, 1); else return new Line((temp4 - temp2) / (temp3 - temp1), temp2 - (temp4 - temp2) / (temp3 - temp1) * temp1, 0); } else if (type == "R") { if (temp1 == temp3) return new Ray(temp1, 0, 1, temp1, temp2, temp3, temp4); else return new Ray((temp4 - temp2) / (temp3 - temp1), temp2 - (temp4 - temp2) / (temp3 - temp1) * temp1, 0, temp1, temp2, temp3, temp4); } else if (type == "S") { if (temp1 == temp3) return new Seg(temp1, 0, 1, temp1, temp2, temp3, temp4); else return new Seg((temp4 - temp2) / (temp3 - temp1), temp2 - (temp4 - temp2) / (temp3 - temp1) * temp1, 0, temp1, temp2, temp3, temp4); } else if (type == "C") { return new Circle(temp1, temp2, temp3); } else { } return NULL;}
三、对于具体交点的计算,因为新增的线段和射线计算交点的方式和直线计算交点的方式基本相同,所以在架构上彻底沿用以前对直线求交点的函数。在交点计算完成后增长一个判断交点是否在射线或线段上的环节。须要注意的是可能会出现线段或射线首尾相连,出现1个交点的特殊状况,所以须要对这种状况进行特殊判断:
void L2L(Line* input1, Line* input2, unordered_set<Point*, Hash_Point, Equal_Point>& g_allpoint) { if (input1->mspecial == 0 && input2->mspecial == 0) { if (abs(input1->ma - input2->ma) <= EPS) { if (abs(input1->mb - input2->mb) <= EPS) L2L_Special(input1, input2, 0, g_allpoint);//对于特殊状况的判断处理 return; } double x = (input2->mb - input1->mb) / (input1->ma - input2->ma); double y = x * input1->ma + input1->mb; if (Line_Process(input1, x, y) && Line_Process(input2, x, y))//判断交点是否在线上 g_allpoint.insert(new Point(x, y)); return; } else if (input1->mspecial == 1 && input2->mspecial == 1) { if (input1->ma == input2->ma) L2L_Special(input1, input2, 1, g_allpoint); return; } else { if (input1->mspecial == 1) { double x = input1->ma; double y = input1->ma * input2->ma + input2->mb; if (Line_Process(input1, x, y) && Line_Process(input2, x, y)) g_allpoint.insert(new Point(x, y)); } else { double x = input2->ma; double y = input1->ma * input2->ma + input1->mb; if (Line_Process(input1, x, y) && Line_Process(input2, x, y)) g_allpoint.insert(new Point(input2->ma, input1->ma * input2->ma + input1->mb)); } } }
对于特殊状况处理,这部分代码逻辑较为复杂。主要思路是判断位于同一直线上的射线和线段,射线和射线,线段和线段之间究竟是存在一个交点,仍是存在覆盖区域,致使无限多个交点产生。代码以下所示:
void L2L_Special(Line* input1, Line* input2, int ifspecial, unordered_set<Point*, Hash_Point, Equal_Point>& g_allpoint) { if (input1->mtype == "L" || input2->mtype == "L") throw string("infinite point"); //若是有一个为直线一定存在无限多个交点 else if (input1->mtype == "R" && input2->mtype == "R") { if (abs(input1->ma - input2->ma) <= EPS && abs(input1->mb - input2->mb) <= EPS) { if (((Ray*)input1)->direction == ((Ray*)input2)->direction) { throw string("infinite point"); }//起始节点相同以及方向相同射线一定有无数多个节点 else { g_allpoint.insert(new Point(input1->ma, input1->mb)); //起始节点相同以及方向相反射线一定只有一个节点 return; } } else { //起始节点不一样节点要么有无数多个节点,要么没有节点 int direction = DirectionGet(((Ray*)input1)->mx1, ((Ray*)input1)->my1, ((Ray*)input2)->mx1, ((Ray*)input2)->my1); if (direction == ((Ray*)input1)->direction)throw string("infinite point"); else return; } } else if (input1->mtype == "R" || input2->mtype == "R") { //对于射线和线段的判断 Ray* r1; Seg* s1; if (input1->mtype == "R") { r1 = (Ray*)input1; s1 = (Seg*)input2; } else { r1 = (Ray*)input2; s1 = (Seg*)input1; } int direction1 = DirectionGet(r1->mx1, r1->my1, s1->mx1, s1->my1); int direction2 = DirectionGet(r1->mx1, r1->my1, s1->mx2, s1->my2); if (direction1 == r1->direction || direction2 == r1->direction) throw string("infinite point"); else if (direction1 == 0 || direction2 == 0) g_allpoint.insert(new Point(r1->mx1, r1->my1)); else return; } else { //对于线段和线段的判断 Seg* s1 = (Seg*)input1; Seg* s2 = (Seg*)input2; double largex1 = s1->mx1 > s1->mx2 ? s1->mx1 : s1->mx2; double smallx1 = s1->mx2 > s1->mx1 ? s1->mx1 : s1->mx2; double largex2 = s2->mx1 > s2->mx2 ? s2->mx1 : s2->mx2; double smallx2 = s2->mx2 > s2->mx1 ? s2->mx1 : s2->mx2; if (abs(smallx1 - largex2) <= EPS) { if (abs(smallx1 - s1->mx1) <= EPS) g_allpoint.insert(new Point(s1->mx1, s1->my1)); else g_allpoint.insert(new Point(s1->mx2, s1->my2)); } else if (abs(largex1 - smallx2) <= EPS) { if (abs(largex1 - s1->mx1) <= EPS) g_allpoint.insert(new Point(s1->mx1, s1->my1)); else g_allpoint.insert(new Point(s1->mx2, s1->my2)); } else if (smallx1 > largex2 || smallx2 > largex1) return; else throw string("infinite point"); } }
对于求交点的函数不太好将其归为具体的一类之中,所以不将其做为具体类的成员函数进行处理,而做为类c函数提供给接口。
下表为优化前的具体花费时间,可见当节点数大于1000后增加速度很是不乐观:
N | 时间(ms) |
---|---|
200 | 45 |
400 | 215 |
600 | 605 |
800 | 920 |
1000 | 1754 |
2000 | 8410 |
3000 | 30360 |
4000 | 83956 |
可见调用时间最多的是在unordered_set中的hash和equal函数的使用,所以打算着重对这两个函数进行优化。
对于hash函数将相同变量外提,减小具体代码量,同时改写对hash的生成,减小hash冲突:
size_t operator()(const class Point* input1)const { //return (int)(((int)input1.x) * 1e6 / 10 + ((int)input1.y) * 1e6 / 10); double x = input1->x; double y = input1->y; long temp1 = (long)floor(x); long temp2 =(long) floor(y); if (abs(x - ((long long)temp1+1)) <= EPS) temp1++; if (abs(y- ((long long)temp2+1)) <= EPS) temp2++; std::hash<long> long_hash; return long_hash.operator()(temp1)+ long_hash.operator()(temp2); }
因为equal函数已经为最简形态,没法进行进一步优化。
优化后的结果为:
可见hash冲突状况显著降低,同时hash和equal调用次数显著下降。对于4000个图形时的计算时间下降到14208毫秒,效率大幅提高。
code contract与design by contract经过前置条件,不变式以及后置条件,同时运用动态检查以及静态检查,使函数和接口的正确性获得充分的保证
优势:
相较于天然语言而言表述更加准确可靠,有无二义性。
明确接口的功能。帮助编程者以目标为导向进行编程,同时帮助调用者可以更全面地了解接口的功能
方便DEBUG调试。能够经过外置的插件快速生成大量测试数据完成DEBUG,比人工测试更加快捷和可靠。
便于程序复用。以后的编程者可以快速清楚代码的功能,更高效的着手对改写。
缺点:
对于JML早在面向对象的课程中就有所涉及。在此次的程序设计中我将UI所需的接口封装为5个函数,2个变量实体,并在底层确保以上接口的正确性。使得调用者并不须要了解程序运行的所有机制就可以经过调用获得所想要的效果。
我对本程序的单元测试共分为4个环节。包括对于程序计算函数的单元测试,对于exe文件的单元测试,对于UI接口的单元测试以及对于错误处理的单元测试。
对于计算函数测试部分主要目的是确保底层的准确性,进而确保接口准确性,如下为部分代码:
TEST_METHOD(TestMethod20) { bool b1 = RangeJudge(-100000); bool b2 = RangeJudge(100000); bool b3 = RangeJudge(-100001); bool b4 = RangeJudge(100001); bool b5 = RangeJudge(-99999); bool b6 = RangeJudge(99999); Assert::AreEqual(b1, true); Assert::AreEqual(b2, true); Assert::AreEqual(b3, true); Assert::AreEqual(b4, true); Assert::AreEqual(b5, false); Assert::AreEqual(b6, false);
对于exe文件部分测试代码以下所示:
TEST_METHOD(TestMethod22) { FILE* stream1; FILE* stream2; freopen_s(&stream1, "G:\\360MoveData\\Users\\HP\\Desktop\\nt\\input.txt", "w", stdout); printf("%d\n", 3); printf("L 0 0 1 1\n"); printf("L 0 0 1 2\n"); printf("L 0 0 1 3\n"); fclose(stdout); PROCESS_INFORMATION ProcessInfo; STARTUPINFO StartupInfo; //入口参数 ZeroMemory(&StartupInfo, sizeof(StartupInfo)); StartupInfo.cb = sizeof StartupInfo; //分配大小 if (CreateProcess("G:\\360MoveData\\Users\\HP\\Desktop\\nt\\IntersectProject.exe", "G:\\360MoveData\\Users\\HP\\Desktop\\nt\\IntersectProject.exe -i G:\\360MoveData\\Users\\HP\\Desktop\\nt\\input.txt -o G:\\360MoveData\\Users\\HP\\Desktop\\nt\\output.txt", NULL, NULL, FALSE, HIGH_PRIORITY_CLASS, NULL, NULL, &StartupInfo, &ProcessInfo)) { WaitForSingleObject(ProcessInfo.hProcess, INFINITE); CloseHandle(ProcessInfo.hThread); CloseHandle(ProcessInfo.hProcess); } //WinExec("G:\\360MoveData\\Users\\HP\\Desktop\\nt\\IntersectProject.exe -i G:\\360MoveData\\Users\\HP\\Desktop\\nt\\input.txt -o G:\\360MoveData\\Users\\HP\\Desktop\\nt\\output.txt", SW_HIDE); int result = 0; FILE* open = fopen("G:\\360MoveData\\Users\\HP\\Desktop\\nt\\output.txt", "r"); fscanf(open, "%d", &result); fclose(open); Assert::AreEqual(1, result); };
对于错误处理的部分代码以下所示:
TEST_METHOD(TestMethod46) { FILE* stream1; freopen_s(&stream1, "G:\\360MoveData\\Users\\HP\\Desktop\\nt\\input.txt", "wt", stdout); printf("%d\n", 1); printf("C 0 0 0\n"); fclose(stdout); PROCESS_INFORMATION ProcessInfo; STARTUPINFO StartupInfo; //入口参数 ZeroMemory(&StartupInfo, sizeof(StartupInfo)); StartupInfo.cb = sizeof StartupInfo; //分配大小 if (CreateProcess("G:\\360MoveData\\Users\\HP\\Desktop\\nt\\IntersectProject.exe", "G:\\360MoveData\\Users\\HP\\Desktop\\nt\\IntersectProject.exe -i G:\\360MoveData\\Users\\HP\\Desktop\\nt\\input.txt -o G:\\360MoveData\\Users\\HP\\Desktop\\nt\\output.txt", NULL, NULL, FALSE, HIGH_PRIORITY_CLASS, NULL, NULL, &StartupInfo, &ProcessInfo)) { WaitForSingleObject(ProcessInfo.hProcess, INFINITE); CloseHandle(ProcessInfo.hThread); CloseHandle(ProcessInfo.hProcess); } //WinExec("G:\\360MoveData\\Users\\HP\\Desktop\\nt\\IntersectProject.exe -i G:\\360MoveData\\Users\\HP\\Desktop\\nt\\input.txt -o G:\\360MoveData\\Users\\HP\\Desktop\\nt\\output.txt", SW_HIDE); char result[40]; FILE* open = fopen("G:\\360MoveData\\Users\\HP\\Desktop\\nt\\output.txt", "r"); fgets(result, 40, open); fclose(open); Assert::AreEqual("radius must be greater than 0\n", result); };
对于UI接口测试的代码以下所示:
TEST_METHOD(TestMethod53) { Add_Diagram("C 1 0 2 0", 1); Add_Diagram("C 2 2 1 0", 1); Add_Diagram("C 3 -2 6 0", 1); Add_Diagram("L -1 4 4 -1", 1); Calculate(); Assert::AreEqual(6, (int)myallpoint.size()); Clear(); Calculate(); Assert::AreEqual(0, (int)myallpoint.size()); Add_Diagram("C 1 0 2 0", 1); Add_Diagram("C 2 2 1 0", 1); Add_Diagram("C 3 -2 6 0", 1); Add_Diagram("L -1 4 4 -1", 1); Calculate(); Assert::AreEqual(6, (int)myallpoint.size()); Sub_Diagram("C 1 0 2 0", 1); Sub_Diagram("C 2 2 1 0", 1); Sub_Diagram("C 3 -2 6 0", 1); Sub_Diagram("L -1 4 4 -1", 1); myallpoint.clear(); Calculate(); Assert::AreEqual(0, (int)myallpoint.size()); }
总共进行了52组不一样的测试,且测试覆盖率达到了90%以上,整体上知足了课程要求。
错误类型 | 输入(其中一种) | 描述 | 输出(输出在文件中) |
---|---|---|---|
线重合 | 2 L 0 0 1 1 L 0 0 -1 -1 | 线性图形有重叠部分,致使无限多个交点 | infinite point |
圆的重合 | 2 C 1 1 1 C 1 1 1 | 两个圆圆心和半径相等 | same circle error |
输入点重合 | 1 L 1 1 1 1 | 线性图形2个输入点彻底相同 | same point in a line |
坐标超限 | 1 L 10000 1000001 1000002 1000003 | 输入点坐标超过限制 | coordinate out of range |
半径小于0 | 1 C 0 0 -1 | 输入圆的半径小于-1 | radius must be greater than 0 |
本程序使用QT完成界面模块的开发工做。结合以前计算模块给出的接口,同时QT模块中使用了Qcustomplot模组帮助进行了绘画功能的实现。整体上界面由一个主界面和子界面组成。主界面中给出了7种功能,包括从文件中导入图形,手动输入图形,手动删除图形,绘制图形,给出交点个数,清零,退出功能。
为了实现各分功能,在主界面程序中定义了所须要相应的函数,以及对应的信号槽,以下图所示:
private slots: void on_input_clicked(); void F_INPUT(); void FILE_INPUT(); void RESULT(); void CLEAR(); void SHOWDELETE(); void myPaint(); ui->setupUi(this); connect(ui->quit, SIGNAL(clicked()), this, SLOT(close())); connect(ui->input,SIGNAL(clicked()),this,SLOT(F_INPUT())); connect(ui->load,SIGNAL(clicked()),this,SLOT(FILE_INPUT())); connect(ui->show_result,SIGNAL(clicked()),this,SLOT(RESULT())); connect(ui->clear,SIGNAL(clicked()),this,SLOT(CLEAR())); connect(ui->delete_2,SIGNAL(clicked()),SLOT(SHOWDELETE())); connect(ui->draw,SIGNAL(clicked()),this,SLOT(myPaint()));
如下为自定义输入界面以及具体的函数,函数中使用了计算模块给出的接口Add_Diagram(),实现了图形的增长操做:
void Form::hand_out(){ string str=ui->first_edit->text().toStdString(); const char* ch=str.c_str(); if(temp==NULL){ ui->first_edit->setText("illegal input"); } else{ Add_Diagram((char*)ch,1); ui->first_edit->setText("successful insert");} //close();} }
图像绘制部分的函数较为复杂,使用了第三方库Qcustomplot完成。因为绘制部分偏向于UI部分单独完成,所以在UI中定义了一系列的函数帮助进行绘图工做:
void paint::paintEvent(QPaintEvent *){ QCustomPlot *customPlot = ui->qcustomPlot; customPlot->clearItems(); customPlot->clearGraphs(); ui->qcustomPlot->addGraph(); ui->qcustomPlot->graph(0)->setLineStyle(QCPGraph::LineStyle::lsNone); ui->qcustomPlot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectPlottables); ui->qcustomPlot->axisRect()->setupFullAxesBox(); ui->qcustomPlot->rescaleAxes(); ui->qcustomPlot->xAxis->setRange(-100000,100000); ui->qcustomPlot->yAxis->setRange(-100000,100000); for (int i = 0 ; i <(int) myallshape.size();i++){ double x1,y1,x2,y2; string type=myallshape.at(i)->mtype; if(type=="R"){ Ray* myray=(Ray*)myallshape.at(i); x1= myray->mx1; y1 = myray->my1; x2 = x1+myray->mx2; y2 = y1+ myray->my2; paintRay(customPlot,x1,y1,x2,y2); } else if(type=="S"){ Seg* myray=(Seg*)myallshape.at(i); x1= myray->mx1; y1 = myray->my1; x2 = x1+myray->mx2; y2 = y1+ myray->my2; paintSegment(customPlot,x1,y1,x2,y2); } else if(type=="L"){ Line* myline=(Line*)myallshape.at(i); if(myline->mspecial==0){ x1=1; y1=myline->ma+myline->mb; x2=2; y2=2*myline->ma+myline->mb; paintLine(customPlot,x1,y1,x2,y2); } if(myline->mspecial==1){ x1=myline->ma; y1=1; x2=myline->ma; y2=2; paintLine(customPlot,x1,y1,x2,y2); } } else if(type=="C"){ Circle* mycircle=(Circle*)myallshape.at(i); x1=mycircle->mx; y1=mycircle->my; x2=mycircle->mr; paintCircle(customPlot,x1-x2,y1+x2,x1+x2,y1-y2); } if (i%5 == 0) customPlot->replot(); } customPlot->replot(); }
其余部分因为篇幅有限,不给出具体UI图片,只给出具体函数代码:
删除操做:
void mydelete::delete_this(){ string str=ui->lineEdit->text().toStdString(); const char* ch=str.c_str(); Sub_Diagram((char*)ch,1); ui->lineEdit->setText("successful delete"); }
求解交点个数并显示:
result::result(QWidget *parent) : QMainWindow(parent), ui(new Ui::result) { ui->setupUi(this); myallpoint.clear(); Calculate(); QStandardItemModel* ItemModel = new QStandardItemModel(this); int result=myallpoint.size(); QString string = QString::number(result); QStandardItem *item = new QStandardItem(string); ItemModel->appendRow(item); ui->listView->setModel(ItemModel); //ui->listView->setFixedSize(600,600); }
经过计算模块事先给出的接口,UI模块只须要调用这些接口就可以实现求解交点、增长图形、删除图形、清空和处理文件输入的操做,给出的接口以下所示:
extern vector <Shape*> myallshape; extern unordered_set<Point*, Hash_Point, Equal_Point> myallpoint; void Add_Diagram(char* input1, int ifoutsource); void Sub_Diagram(char* input2, int ifoutsource); void File_InputProcess(); void Clear(); void Calculate();
UI程序经过引入dll程序以及调用.h文件就能快速实现所须要的一系列操做。
结对编程 | 我 | 结对伙伴 | |
---|---|---|---|
优势 | 一、持续代码复审,减小BUG产生。二、互相学习代码风格,可以纠正本身自己的错误。三、可以让双方更高效地编程,增长工做效率。 | 代码能力较为熟练。 | 比较仔细,可以快速找到程序中的BUG。 |
缺点 | 一、磨合和交流都须要花费必定的时间。二、结对伙伴的能力直接影响了编程的质量。三、容易出现二人之间地位不平衡的状况。 | 吧不太擅长测试,同时编写程序BUG较多,而且交流不太主动。 | 编码能力不强。 |
已完成对圆的处理(具体实现略),交换程序部分并无完成。