• 结对编程


    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

  • 相关阅读:
    VS2010 C++环境下DLL和LIB文件目录及名称修改
    从点击Button到弹出一个MessageBox, 背后发生了什么
    Unicode化
    ANSI和UNICODE编程的注意事项
    SQL的主键和外键约束
    关于_WIN32_WINNT的含义
    清理Visual Studio中VC++工程里不需要的文件
    Windows应用程序的VC链接器设置
    #define WIN32_LEAN_AND_MEAN 的作用
    c++中char*wchar_t*stringwstring之间的相互转换
  • 原文地址:https://www.cnblogs.com/mjmj111/p/12522985.html
Copyright © 2020-2023  润新知