结对编程

PSP 表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划
· Estimate · 估计这个任务需要多少时间 880 1000
Development 开发
· Analysis · 需求分析 (包括学习新技术) 60 60
· Design Spec · 生成设计文档 60 120
· Design Review · 设计复审 (和同事审核设计文档) 60 60
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 40 40
· Design · 具体设计 60 60
· Coding · 具体编码 360 400
· Code Review · 代码复审 40 60
· Test · 测试(自我测试,修改代码,提交修改) 80 80
Reporting 报告
· Test Report · 测试报告 40 40
· Size Measurement · 计算工作量 20 20
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 60 60
合计 880 1000

Information Hiding,Interface Design,Loose Coupling

Information Hiding:对数据进行封装,让外部不能随便访问。由于本次作业的计算比较多,为了提高效率,并没有遵守这个原则,类的成员变量都被设为public。

Loose Coupling:松耦合。每个模块自身是一个整体,各个模块之间的依赖尽可能少,这样修改一个模块时,就不用修改其他的模块。我们的核心计算模块向UI模块提供了添加和删除图形的两个接口,UI模块不用考虑计算模块的具体实现,只需要调用接口函数,UI模块和计算模块之间的耦合比较低。

计算模块接口的设计与实现过程

​ 与上次作业类似,本次作业里只有三个类(点/向量、线、圆)及其公共常用函数以及三个求解函数,在类中只需对应属性以及提供向量计算的相关函数,并在三个求解函数中对类的交点进行求解。

求解函数的关键代码如下:

其中line.isOnLine()是判断点是否在线段/射线上的简单方法,其实现则是根据直线/线段/射线的向量方程 l = u + tv ,u是直线上一点,v是方向向量,t是系数,计算出t则易判断是否在所给线上。

void lineIntersectLine(const Line& l1, const Line& l2)
{
	if (dcmp(l1.v ^ l2.v) == 0)	return;
	Vector u = l1.p - l2.p;
	double t = (l2.v ^ u) / (l1.v ^ l2.v);

	Point temp = l1.p + l1.v * t;
	if (l1.type != "L" && !l1.isOnLine(temp))	return;
	if (l2.type != "L" && !l2.isOnLine(temp))	return;

	try {
		points.insert(temp);
	}
	catch(exception e){}
}

void lineIntersectCircle(const Line& L, const Circle& C)
{
	double t1, t2;
	double a = L.v.x, b = L.p.x - C.c.x, c = L.v.y, d = L.p.y - C.c.y;
	double e = a * a + c * c, f = 2 * (a * b + c * d), g = b * b + d * d - C.r * C.r;
	double delta = f * f - 4 * e * g;
	if (dcmp(delta) < 0)    return; //线圆相离
	if (dcmp(delta) == 0) {		    //线圆相切
		t1 = t2 = -f / (2 * e);
		if (L.type != "L" && !L.isOnLine(t1))	return;
		try {
			points.insert(L.point(t1));
		}
		catch (exception e) {
		}
		return;
	}
	//线圆相交
	t1 = (-f - sqrt(delta)) / (2 * e);
	t2 = (-f + sqrt(delta)) / (2 * e);
	Point p1 = L.point(t1);
	Point p2 = L.point(t2);
	if (L.type != "L" && !L.isOnLine(p1));
	else {
		try {
			points.insert(p1);
		}
		catch (exception e) {
		}
	}

	if (L.type != "L" && !L.isOnLine(p2));
	else {
		try {
			points.insert(p2);
		}
		catch (exception e) {
		}
	}
	return;
}

void circleIntersectCircle(const Circle& c1, const Circle& c2)
{
	double d = Length(c1.c - c2.c);
	if (dcmp(d) == 0) return; //两圆重合
	if (dcmp(c1.r + c2.r - d) < 0)    return;
	if (dcmp(fabs(c1.r - c2.r) - d) > 0)    return;
	double a = angle(c2.c - c1.c);
	double da = acos((c1.r * c1.r + d * d - c2.r * c2.r) / (2 * c1.r * d));
	Point p1 = c1.point(a - da), p2 = c1.point(a + da);
	try {
		points.insert(p1);
	}
	catch (exception e) {
	}
	if (p1 == p2)	return;
	try {
		points.insert(p2);
	}
	catch (exception e) {
	}
}

独到之处在于:用计算几何的方法,简洁高效的解决了核心代码的拓展部分。

计算模块接口部分的性能改进

性能改进方面依然没有想出更优复杂度的算法,只能够进行一些细节上的优化。由于C++的set是基于红黑树实现的,插入操作的复杂度为O(logn),所以考虑了换成基于哈希表实现的unordered_set, 在哈希函数理想的情况下,插入操作的复杂度为O(1)。换了unordered_set之后性能确实有了提升,在1000条直线,400000个交点的情况下,所用时间从9秒左右提升到了6秒左右。

Design by Contract

Design by Contract就是契约式编程,在大二的面向对象课程有过了解。这种方法规定了函数的前提条件,后继条件,不变量条件等,优点是严格区分了责任,让每个人只用关心自己的代码正确性。缺点是可能会在函数各种条件的考虑上花费很多时间。
项目中并没有严格地使用契约式编程,只是规定了一些接口的作用,参数,返回值。

计算模块部分单元测试展示

在构造测试数据时,两两组合,分为L_L,R_R, S_S, L_R, L_S, L_C, R_S, R_C, S_C, C_C十种情况。

首先把能想到的情况进行了测试,比如直线相交,直线平行,射线端点,直线与圆相交,相切,相离等情况,然后根据覆盖率工具显示的未覆盖的分支,再添加对应的测试数据 。

测试覆盖率:

计算模块部分异常处理说明

只设计了一个异常类,定义了五种异常类型。
WRONGTYPE: 输入了L, R, S, C之外的不支持的类型。
WRONGFORMAT: 输入了不符合规范的格式。
BADPOINT: 输入的点的坐标不在(-100000,100000)范围内。
BADLINE: 输入的直线的两个端点重合。
BADCIRCLE: 输入的圆的半径r小于等于0。

界面模块的详细设计过程

首先决定采用基于C++的QT编写UI,在资料查询的过程中,我发现了QT的第三方库QcustomPlot,看完其基本演示后,发现能够较为简单的实现类似GeoGebra的界面放缩和绘制功能,因此决定以QT+QcustomPlot完成UI任务。在阅读完《Qt5.9 c++开发指南》的前四章以及第七章IO处理之后,我认为已有的知识已经足够了,因此开始查询QcustomPlot官方文档以及答疑,同时进行UI代码编写。

UI界面如上,共有四个Button(分别负责绘制图像、交点、添加、删除,其中添加删除格式与输入格式一致,例如“L 0 0 1 1”),一条menubar(负责打开文本文件),一个TextEdit(负责显示文本文件以及输入),一个结果Lable(显示交点数目),以及QcustomPlot区域(绘图),由于QT是可以可视化拖动模块并自动对其产生代码的,所以该部分的设计实现并不是人工的。

接着为了实现上述的功能,需要利用QT中的信号与槽机制,按钮的信号在其库中已有定义,需要写的是按钮按下后的反应,即按钮对应的槽,如下:

private slots:
    void on_actOpen_triggered();
    void on_AddBtn_clicked();
    void on_DelBtn_clicked();
    void on_PaintBtn_clicked();
    void on_PointBtn_clicked();

具体代码过长就不做展示了。这些槽函数定义了按钮的反应,Add和Del对应的是核心代码接口,这里主要讲两个绘制按钮的槽。

根据QcustomPlot官方文档可知其对圆,直线,射线,点均有多种不同实现方法,但大体而言分两类,一类在Graph对象中添加点集,形成图像;另一类AbstractItem则是直接对图像进行绘制(这里的原理并未细究,但其绘制速度是远超于点绘制的)。因此我编写了不同的绘制函数,如下:

    void paintPoint(QCustomPlot *customPlot,double x,double y);
    void paintLine(QCustomPlot *customPlot,double x1, double y1, double x2, double y2);
    void paintRay(QCustomPlot *customPlot,double x1, double y1, double x2, double y2);
    void paintSegment(QCustomPlot *customPlot,double x1, double y1, double x2, double y2);
    void paintCircle(QCustomPlot *customPlot,double x1, double y1, double x2, double y2);

经测试,在500000数量级的绘制下均可以保持使用流畅(当然,绘制需要时间,该库还提供了Opengl加速,但我并未实验)

界面模块与计算模块的对接

由于提前设计了接口

void add_diagram(char T, int x1, int y1, int x2, int y2);
void sub_diagram(char T, int a, int b, int c, int d);
void calPoints();
set<pair<double, double>> uiPoints;

因此在对接时只需要在四个按钮对应槽中合理调用接口即可。

实现的功能:

  1. 支持从文件导入几何对象的描述。

    左上角File处可以添加文件,并显示在下方文本框中。

  2. 支持几何对象的添加、删除。

    按下Add,Del按钮会添加/删除文本框中的集合对象

  3. 支持绘制现有几何对象。

    PaintGraph按钮实现

  4. 支持求解现有几何对象交点并绘制。

    PaintPoint按钮实现

  5. 坐标系放缩,平移 (数量级10^-5 ~ 10^250)

结对过程

腾讯会议屏幕共享截图

image-20200321201533834

日常微信交流截图

image-20200321201423031

结对编程优缺点

我认为结对编程的好处在于:结对编程的两个人可以互相监督,不容易偷懒,可以互相学习编程技巧,以及可以同时检查代码,有效地减少bug。

坏处:可能会在某个问题上产生不同的想法,互相沟通会花费一些时间。

成员的优点和缺点:

我: 优点:思路清晰,比较认真

缺点:容易偷懒

对方:优点:很认真,耐心,学习能力很强

缺点:不是很细心

附加题

交换团队(17373259 、 17373250)

由于和对方团队提前商量好了接口,因此模块的替换较为容易,基本无需更改。

将DLL替换后编码如下(以添加Add按钮为例)

 QString text = ui->textEdit->toPlainText();
    QFile outFile("./temp.txt");
    outFile.open(QIODevice::WriteOnly);
    QTextStream out(&outFile);
    for(int i = 0 ; i < text.size();i++){
        out << text.at(i);
    }
    outFile.close();

    QFile inFile("./temp.txt");
    inFile.open(QIODevice::ReadOnly);
    QTextStream in(&inFile);
    QChar sub;
    int x1,y1,x2,y2,r;
    QCustomPlot *customPlot = ui->qcustomPlot;
    while(in.atEnd() == false){
        in >> sub ;
        if(sub == "L")  {
            in >> x1 >> y1 >> x2 >> y2;
            add_diagram('L',x1,y1,x2,y2);
        }
        else if(sub == "R"){
            in >> x1 >> y1 >> x2 >> y2;
            add_diagram('R',x1,y1,x2,y2);
        }

        else if(sub == "S"){
            in >> x1 >> y1 >> x2 >> y2;
            add_diagram('S',x1,y1,x2,y2);
        }

        else if(sub == "C"){
            in >> x1 >> y1 >> r;
            add_diagram('C',x1,y1,r,0);
        }
    }
    ui->textEdit->clear();
    cout << point_map.size() << endl;

基本不需要修改源代码,即可完成dll更改。

一开始由于随机生成的测试样例中存在平行直线,出现了一些问题(己方dll是在QT中重新改写生成的,并未将核心代码中的错误处理部分导入,造成了这个测试样例中的问题未被及时发现)。后续修改后,经过测试,对方dll能够完成任务需求。

img

原文地址:https://www.cnblogs.com/mjmj111/p/12522985.html