结对项目作业

结对项目作业

写在前面:

项目 内容
这个作业属于哪个课程 2020计算机学院软件工程(罗杰 任健)
这个作业的要求在哪里 结对项目作业
教学班级 006
项目地址 https://github.com/CrapbagMo/PairProgramIntersect

在开始实现程序之前,在下述 PSP 表格记录下你估计将在程序的各个模块的开发上耗费的时间。

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

看教科书和其它资料中关于 Information Hiding,Interface Design,Loose Coupling 的章节,说明你们在结对编程中是如何利用这些方法对接口进行设计的。

  1. 在设计的所有类中,都将属性设置为了 private,通过 setXgetX 等方法访问。

  2. 全局静态变量 PlaneContainer* pc 通过 init_PlaneContainerdispose_PlaneContainer 方法访问,对外隐藏了指针。

  3. 在接口向外传递异常时,考虑到 C# 的异常类和 C++ 的异常类并不兼容。我们也没有什么好办法解决这个问题,于是决定采用传统 C 用返回值传递错误状态的思想。使用返回值来传递异常。

  4. 在core.dll中,我们并不依赖任何外部项,完全依靠自身即可完成对外接口提供的功能。


计算模块接口的设计与实现过程。 设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图?说明你的算法的关键(不必列出源代码),以及独到之处。

  1. 类及函数

    • 五个主要类:

      点类:class Point
      图形类:class Figure
      线条类:class Line: Figure
      圆形类:class Circle: Figure
      平面容器类:class PlaneContainer

    • 五个主要函数:

      添加图形:int add_Figure(std::string buf)
      初始化容器:void initial_PlaneContainer()
      释放容器:void dispose_PlaneContainer()
      获取交点序列:double* get_IntersectionPoints()
      获取交点数目:int get_NumOfIntersectionPoints()

  2. 主要作用:

    • CircleLine类继承自Figure类,实现std::setintersect(Figure*)方法。该方法返回两个图形的交点集合。

    • 五个函数都是对外提供的接口函数。init_PlaneContainer、dispose_PlaneContainer采用单例模式用于维护全局静态变量PlaneContainer* pcadd_Figureget_NumOfIntersectionPointsget_IntersectionPoints用于提供添加图形、获取交点序列、交点数目功能。

    • 异常由add_Figure函数捕获并处理,处理完后已错误代码形式返回,具体处理方式本节不做介绍。

  3. 算法关键及独到之处:

    • 首先考虑直线和圆的情况(先不考虑射线和线段):

      按照直线和直线, 直线和圆, 圆和圆在平面上的关系分为下面三种情况考虑:

      直线和直线:

      1. 判断直线是否相交: \(A_1*B_2-A_2*B_1!=0\)则相交.

      2. 若相交则求交点: \((\frac{B_1*C_2-B_2*C_1}{A_1*B_2-A_2*B_1},\frac{A_2*C_1-A_1*C_2}{A_1*B_2-A_2*B_1})\)

      直线和圆:

      1. 联立直线和圆方程(为了起见简便, 若\(B!=0\), 化为斜截式再联立), 求得系数\(tA\), \(tB\), \(tC\).

      2. 根据\(Delta=tB^2-4*tA*tC\)判断交点个数

      3. \(Delta\ge0\) , 根据求根公式求得交点横坐标, 进而求出交点.

      圆和圆:

      1. 计算圆心距\(dis=\sqrt{(x_1-x_2)^2+(y_1-y_2)^2}\).

      2. 比较圆心距\(dis\) 和半径和\(r_1+r_2\), 半径差$ | r_1-r_2 | $ .

      3. 若有交点则两圆相减求出相交弦方程, 进而转化为直线和圆得交点.

    • 注意到直线、射线、线段的不同点仅在于其坐标范围不同。因此为Line类引入范围属性(\(R \leq x \leq S\))即可同时表示三种线,其中射线、直线的无穷端以宏INF表示。求交点时,将LineFigure求交点,求得的交点再判断是否在Line所在范围内即可,若不在范围内则剔除。这就将直线、射线、线段统一了。


阅读有关 UML 的内容:https://en.wikipedia.org/wiki/Unified_Modeling_Language。画出 UML 图显示计算模块部分各个实体之间的关系(画一个图即可)。

  • UML实体图:


计算模块接口部分的性能改进。记录在改进计算模块性能上所花费的时间,描述你改进的思路,并展示一张性能分析图(由VS 2015/2017的性能分析工具自动生成),并展示你程序中消耗最大的函数。

  • 改进之前,一分钟之内只能运算两百张图不到,运行一分钟(处理了大约150个图形)后性能探测截图:

    结对项目作业_第1张图片

    可以看到,主要时间都花在了,PlaneContainer.insert方法的set_union函数上。

    经查阅资料发现,在set_union函数中,存在了多次set复制,效率极低。于是修改了此处逻辑如下:

    结对项目作业_第2张图片

    修改之后,可以在20秒左右完成2000个图形的计算,性能大大提高,与改前可谓“天壤之别”再次执行了性能探查:

    结对项目作业_第3张图片

    结对项目作业_第4张图片

    此时PlaneContainer.insert方法已经不耗费太多时间了。


看 Design by Contract,Code Contract 的内容:

  • http://en.wikipedia.org/wiki/Design_by_contract

  • http://msdn.microsoft.com/en-us/devlabs/dd491992.aspx

描述这些做法的优缺点,说明你是如何把它们融入结对作业中的。

  • 关于 Design by Contract,Code Contract

    • 优点

      1. 获得更优秀的设计

        可以启发程序员思考程序的先验条件,有助于了理清思路

        契约双方的权力和义务得到了共享,同时获得了清晰的描述

      2. 提高可靠性

        普遍采用断言机制,可靠性高

        帮助开发者更好地理解代码

        方便测试,随调随用

      3. 是优秀的文档

        好的契约设计,代码本身即是注释,即是文档

        契约设计是精确的规范

      4. 复用性高

    • 缺点

      1. 需要花费时间学习契约思想和技术

      2. 契约代码的开发会更加耗时(但可以节省测试,编写文档时间)

      3. 需要大量时间

      4. 只适用于顺序程序,不适用于并行程序。

  • 在本项目中的应用

    • add_Figure函数调用其他函数的设计中,我们使用了契约方式,约定,add_Figure只负责传递格式正确的格式给其他的函数,其他的函数不再做格式检查。

    • PlaneContainer.insert方法中也采用了契约方式,约定,PlaneContainer.insert方法只负责插入一个合法的Figure,但不保证是否会和已有的Figure冲突,这需要由他所调用的方法检查。


计算模块部分单元测试展示。展示出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路。并将单元测试得到的测试覆盖率截图,发表在博客中。要求总体覆盖率到 90% 以上,否则单元测试部分视作无效。

  • 代码覆盖率情况:(使用VS2017 Enterprise代码覆盖率工具生成)

    结对项目作业_第5张图片

    对项目中的类的单元测试覆盖率为 \(93.07\%\) ,对类 Circle 的覆盖率为 \(100\%\), 对类 Line 的覆盖率为 \(98.5\%\)

  • 我们设置了针对功能的测试和针对异常的测试。

    1. 针对功能性测试,我们测试了圆、直线、线段和射线的一些函数在正常情况下的功能性,同时也构造了一些特殊情况下的测试用例,比如:

      // 射线 圆 内部相交  
      PlaneContainer pc;  
      pc.insert(new Circle(0, 0, 2));  
      pc.insert(new Line(1, 0, 2, 2, RL));  
      int count = pc.countIntersectionPoints();  
      Assert::AreEqual(count, 1);  
      
      // 射线 射线 一个交点  
      PlaneContainer pc;  
      pc.insert(new Line(0, 0, 1, 1, RL));  
      pc.insert(new Line(0, 0, -1, -1, RL));  
      int count = pc.countIntersectionPoints();  
      Assert::AreEqual(count, 1);  
      
      // 精度测试  
      PlaneContainer pc;  
      pc.insert(new Line(0, -100000, 1, 100000, SL));  
      pc.insert(new Line(0, 0, 0, 1, SL));  
      pc.insert(new Line(0, -99999, 1, -99999, SL));  
      int count = pc.countIntersectionPoints();  
      Assert::AreEqual(count, 3);  
      
    2. 针对异常的测试用例见下一部分。


计算模块部分异常处理说明。在博客中详细介绍每种异常的设计目标。每种异常都要选择一个单元测试样例发布在博客中,并指明错误对应的场景。

  • 本次结对项目中,我们共设计了七类异常

    1. 输入不满足标准格式

      我们使用正则表达式来识别输入是否满足标准格式,正则表达式如下:

      std::regex segREGEX("S\\s+-?\\d+\\s+-?\\d+\\s+-?\\d+\\s+-?\\d+\\s*\\n?");
      std::regex lineREGEX("L\\s+-?\\d+\\s+-?\\d+\\s+-?\\d+\\s+-?\\d+\\s*\\n?");
      std::regex rayREGEX("R\\s+-?\\d+\\s+-?\\d+\\s+-?\\d+\\s+-?\\d+\\s*\\n?");
      std::regex circleREGEX("C\\s+-?\\d+\\s+-?\\d+\\s+-?\\d+\\s*\\n?");
      
    2. 参数不在标准范围 (-100000, 100000) 内

    3. 定义直线的两点重合

    4. 圆的半径不大于零

    5. 计算中出现无穷交点

    6. 直线、线段或射线与直线、线段或射线出现部分或完全重合

    7. 两圆完全重合

  • 对于单元测试,我们对每一类异常场景及正常场景设计了3-4个测试样例,测试代码如下,

    TEST_METHOD(TestMethod1)
    {    // 格式错误
        int res = add_Figure("acsd");
        Assert::AreEqual(res, -1);
        delete(pc);
    }
    
    TEST_METHOD(TestMethod2)
    {    // 格式错误
        int res = add_Figure("C 5 3 -2 1");
        Assert::AreEqual(res, -1);
        delete(pc);
    }
    
    TEST_METHOD(TestMethod3)
    {    // 格式错误
        int res = add_Figure("L -5 3 -2 0 4");
        Assert::AreEqual(res, -1);
        delete(pc);
    }
    
    TEST_METHOD(TestMethod4)
    {    // 格式错误
        int res = add_Figure("R 5 -3 2");
        Assert::AreEqual(res, -1);
        delete(pc);
    }
    
    TEST_METHOD(TestMethod5)
    {    // 格式错误
        int res = add_Figure("c 5 -3 2");
        Assert::AreEqual(res, -1);
        delete(pc);
    }
    
    TEST_METHOD(TestMethod6)
    {    // 格式错误
        int res = add_Figure("S 5 -3 2 0-1\n");
        Assert::AreEqual(res, -1);
        delete(pc);
    }
    
    TEST_METHOD(TestMethod7)
    {    // 正常
        int res = add_Figure("R 5 -3 2 3");
        Assert::AreEqual(res, 0);
        delete(pc);
    }
    
    TEST_METHOD(TestMethod8)
    {    // 正常
        int res = add_Figure("C 5 -3 3\n");
        Assert::AreEqual(res, 0);
        delete(pc);
    }
    
    TEST_METHOD(TestMethod10)
    {    // 半径小于0
        int res = add_Figure("C 5 -3 -2 \n");
        Assert::AreEqual(res, 3);
    }
    
    TEST_METHOD(TestMethod11)
    {    // 半径等于0
        int res = add_Figure("C -5 -3 0\n");
        Assert::AreEqual(res, 3);
    }
    
    TEST_METHOD(TestMethod12)
    {    // 点超出坐标轴范围
        int res = add_Figure("L 963214 -3 2 3\n");
        Assert::AreEqual(res, 1);
    }
    
    TEST_METHOD(TestMethod13)
    {    // 点超出坐标轴范围
        int res = add_Figure("R 5 -3 526151 3\n");
        Assert::AreEqual(res, 1);
    }
    
    TEST_METHOD(TestMethod14)
    {    // 点超出坐标轴范围
        int res = add_Figure("C -3 526151 3\n");
        Assert::AreEqual(res, 1);
    }
    
    TEST_METHOD(TestMethod15)
    {    // 两点重合
        int res = add_Figure("R 2 -3 2 -3 ");
        Assert::AreEqual(res, 2);
    }
    
    TEST_METHOD(TestMethod16)
    {    // 两点重合
        int res = add_Figure("L 99999 88888 99999 88888 \n");
        Assert::AreEqual(res, 2);
    }
    
    TEST_METHOD(TestMethod17)
    {    // 两点重合
        int res = add_Figure("S 0 2 0 2 \n");
        Assert::AreEqual(res, 2);
    }
    
    TEST_METHOD(TestMethod18)
    {    // 无穷交点
        add_Figure("L 2 2 3 3 ");
        int res = add_Figure("L 0 0 -1 -1 \n");
        Assert::AreEqual(res, 4);
    }
    
    TEST_METHOD(TestMethod19)
    {    // 无穷交点
        add_Figure("S 2 2 4 4 ");
        int res = add_Figure("S 3 3 0 0 \n");
        Assert::AreEqual(res, 4);
    }
    
    TEST_METHOD(TestMethod20)
    {    // 无穷交点
        add_Figure("R 1 1 4 4 ");
        int res = add_Figure("S 2 2 0 0 \n");
        Assert::AreEqual(res, 4);
    }
    
    TEST_METHOD(TestMethod21)
    {    // 无穷交点
        add_Figure("R 1 1 1 0 ");
        int res = add_Figure("S 1 0 1 5 \n");
        Assert::AreEqual(res, 4);
    }
    
    TEST_METHOD(TestMethod22)
    {    // 无穷交点
        add_Figure("C 1 1 1  ");
        int res = add_Figure("C 1 1 1 \n");
        Assert::AreEqual(res, 4);
    }
    

界面模块的详细设计过程。在博客中详细介绍界面模块是如何设计的,并写一些必要的代码说明解释实现过程。

界面模块与计算模块的对接。详细地描述 UI 模块的设计与两个模块的对接,并在博客中截图实现的功能。

  1. 这两个问题内容有相关联的地方故,合起来说。

  2. 界面模块是基于.NET 4.7.2 的窗体应用,采用C#语言编写。

  3. Program用于定义Main函数外,整个模块一共只有类,下面结合图形、代码介绍该类的组成:

    • 程序界面:

      结对项目作业_第6张图片

    • 声明部分代码:

      namespace GUI  
      {  
          unsafe public partial class MainForm : Form  
          {  
              //下面为控件类实例,由VS自动生成  
              //Panel是主要的控件,用于绘图  
              private System.Windows.Forms.Panel panel1;  
              //打开输入文件对话框  
              private System.Windows.Forms.OpenFileDialog openFileDialog1;  
              //文件输入按钮和手工输入按钮  
              private System.Windows.Forms.Button inputButton;  
              private System.Windows.Forms.Label pathInput;//保存并显示输入文件路径  
              private System.Windows.Forms.Button button2;  
              //1-6用于获取手工输入图形的数据,7用于显示多行提示信息  
              private System.Windows.Forms.TextBox textBox1;  
              private System.Windows.Forms.TextBox textBox2;  
              private System.Windows.Forms.TextBox textBox3;  
              private System.Windows.Forms.TextBox textBox4;  
              private System.Windows.Forms.TextBox textBox5;  
              private System.Windows.Forms.TextBox textBox6;  
              private System.Windows.Forms.TextBox textBox7;  
              //以下label用于显示单行信息  
              private System.Windows.Forms.Label num;  
              private System.Windows.Forms.Label label2;  
              private System.Windows.Forms.Label label3;  
              private System.Windows.Forms.Label label4;  
              private System.Windows.Forms.Label label5;  
              private System.Windows.Forms.Label label6;  
              private System.Windows.Forms.Label label7;  
              private System.Windows.Forms.Label label8;  
              private System.Windows.Forms.Label label9;  
      
              //下面为用户变量  
              //绘图画笔  
              private Pen pen;  
              //绘图类  
              private Graphics g;  
              //直线容器,每四个数字代表一条直线  
              private LinkedList StraightLines;  
              //射线容器,每四个数字代表一条射线  
              private LinkedList RayLines;  
              //线段容器,每四个数字代表一条线段  
              private LinkedList LineSegments;  
              //圆形容器,每三个数字代表一个圆形  
              private LinkedList Circle;  
      
              //下面的五个函数皆为从core.dll中导入的接口函数  
              //添加图形并返回添加结果  
              [DllImport("core.dll")]  
              private static extern int add_Figure(StringBuilder buf);  
              //初始化容器  
              [DllImport("core.dll")]  
              private static extern void initial_PlaneContainer();  
              //摧毁容器  
              [DllImport("core.dll")]  
              private static extern void dispose_PlaneContainer();  
              //获取交点序列  
              [DllImport("core.dll")]  
              private static extern double* get_IntersectionPoints();  
              //获取交点数目  
              [DllImport("core.dll")]  
              private static extern int get_NumOfIntersectionPoints();  
      
              //下面为成员方法(包括回调函数)  
              //构造方法,主要为变量分配。  
              public MainForm();  
              //窗体打开和关闭的回调,用于显示欢迎和再见提示  
              private void MainForm_Load(object sender, EventArgs e);  
              private void MainForm_FormClosing(object sender, FormClosingEventArgs e)  
              //输入按钮的回调函数,用于处理文件输入  
              private void inputButton_Click(object sender, EventArgs e);//文件输入  
              private void button2_Click(object sender, EventArgs e)//手工输入  
              //画板的重绘回调函数,用于绘制图形  
              private void panel1_Paint(object sender, PaintEventArgs e);  
              //处理add_Figure函数返回来的错误代码  
              private bool processRetVal(int retCode, string line);  
              //存储图形,以供绘图函数使用  
              private void store(string buf);  
              //四个小的绘图函数  
              private void drawLine();  
              private void drawCircle();  
              private void drawRay();  
              private void drawLineSegment();  
              //坐标系变换函数  
              private void change(ref Point p);  
              private void change(ref PointF p)  
          }  
      }  
      
  4. 重点介绍以下几个函数的运行逻辑

    1. private void inputButton_Click(object sender, EventArgs e);//文件输入

      • 若打开不成功则不进行处理并给出提示。

        结对项目作业_第7张图片

      • 否则,按行循环读取文件送string line,调用add_Figure处理,并获取处理结果送int retCode

      • lineretCode交给processRetVal处理。

      • 通过get_NumOfIntersectionPoints更新交点数目并显示

    2. private void button2_Click(object sender, EventArgs e);//手工输入

      • 通过6个TextBox输入框获取输入数据。
      • 根据LRSC输入框输入的类型代码,将LRSC和后面的相应输入框中内容组成string line,调用add_Figure处理,并获取处理结果送int retCode
      • lineretCode交给processRetVal处理。
      • 通过get_NumOfIntersectionPoints更新交点数目并显示
    3. private bool processRetVal(int retCode, string line);

      • retCode==0,未发生异常,调用storeline存储。

      • 否则,不进行存储并提示相应的错误,提示用户,例如。

        结对项目作业_第8张图片

    4. private void panel1_Paint(object sender, PaintEventArgs e);

      • 绘制坐标系、刻度等。
      • 调用drawLinedrawCircledrawRaydrawSegment绘制相应图形。
  5. 模块对接

    1. core模块不提供内存管理,由接口函数init_PlaneContainerdispose_PlaneContainer管理。

    2. 核心接口add_Figure供界面模块调用。

    3. 正则匹配,如果格式不符合要求,不进入下一步并返回相应的错误代码。否则予以解析进入下一步。

    4. 根据解析结果,构造相应的Figure并捕获可能的异常,如果构造过程中出现两点重合、半径非正等异常,不进入下一步,并反回相应的错误代码。

    5. 将构造的Figure假如容器pc,并捕获可能的异常,如果出现无穷交点等异常,不进入下一步,并返回相应错误代码。

    6. 正常返回。代码代码0;

  6. 使用图例

    结对项目作业_第9张图片


描述结对的过程,提供两人在讨论的结对图像资料(比如 Live Share 的截图)。关于如何远程进行结对参见作业最后的注意事项。

  • 在项目一开始,我们采用了“领航与-驾驶员”模式,但是在实验一下午之后,由于网络等原因,我们发现“领航员-驾驶员”模式工作效率不太高,于是我们采用了独立写然后互审,互测的方式,工作效率有所提高,并一直沿用到结束。

  • 我们在结对过程中使用了 Visual Studio 中的 Live Share 和微信进行交流,截图如下:

    结对项目作业_第10张图片

    结对项目作业_第11张图片

看教科书和其它参考书,网站中关于结对编程的章节,例如:http://www.cnblogs.com/xinz/archive/2011/08/07/2130332.html ,说明结对编程的优点和缺点。同时描述结对的每一个人的优点和缺点在哪里(要列出至少三个优点和一个缺点)。

  • 关于结对编程

    • 优点

      1. 互相鼓励,不容易沮丧。

      2. 互相监督,不容易偷懒。

      3. 互相学习,双方都能在编程中学到东西。

      4. 两个人随时复审,降低 bug产出率。

    • 缺点

      1. 与合不来的人容易发生冲突,导致团队矛盾。

      2. 双方技术实力差距过大,可能会使得另一方产生不满情绪。

      3. 观点出现分歧容易发生争吵。

      4. 开发时可能会闲谈、走神,降低效率。

  • 关于结对双方

    优点 缺点
    有耐心
    代码风格良好
    善于交流
    粗心
    结对伙伴 细心
    时间观念好
    逻辑清晰
    不善交流

在你实现完程序之后,在附录提供的PSP表格记录下你在程序的各个模块上实际花费的时间。

见上文PSP表格。


你可能感兴趣的:(结对项目作业)