天天看点

结对项目博客

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

1. PSP表格

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

2. Information Hiding,Interface Design,Loose Coupling

  • Information Hiding

    是指信息隐藏原则,在1972年被 David Parnas 提出,他指出:代码模块应该采用定义良好的接口来封装,这些模块的内部结构应该是程序员的私有财产,外部是不可见的。在结对编程中,我们对于信息隐藏的应用是:
    • 在多层设计中的层与层之间加入接口层
    • 所有类与类之间都通过接口类访问
    • 类的所有数据成员都是private,所有访问都是通过访问函数实现的
  • Interface Design

    是指接口设计,接口被定义为职责(或角色、能力),接口决定了一种类能够做什么,赋予了他在某种情况下的权力和义务。在结对编程中,我们专注于定义行为,把某个具体功能提取出来,先把这个功能弄成策略,然后各自具体用类去继承这个接口。
  • Loose Coupling

    是指松耦合,是指减少一个代码单元与其他代码单元的配合关系。最理想、最松散的耦合,是一个单元无需其他代码单元的配合可以单独完成它的功能。松耦合的目标是最小化依赖。松耦合这个概念主要用来处理可伸缩性、灵活性和容错这些需求。在结对编程中,我们通过加入中间层,来减少各个代码单元之间的耦合度。

3. 计算模块接口的设计与实现

  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. 主要作用:
    • Circle

      Line

      类继承自

      Figure

      类,实现

      std::set<Point>intersect(Figure*)

      方法。该方法返回两个图形的交点集合。
    • 五个函数都是对外提供的接口函数。

      init_PlaneContainer、dispose_PlaneContainer

      采用单例模式用于维护全局静态变量

      PlaneContainer* pc

      add_Figure

      get_NumOfIntersectionPoints

      get_IntersectionPoints

      用于提供添加图形、获取交点序列、交点数目功能。
    • 异常由add_Figure函数捕获并处理,处理完后已错误代码形式返回,具体处理方式本节不做介绍。
  3. 算法关键及独到之处:
    • 首先考虑直线和圆的情况(先不考虑射线和线段):

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

      直线和直线:

      • 判断直线是否相交: \(A_1*B_2-A_2*B_1!=0\)则相交.
      • 若相交则求交点: \((\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})\)

        直线和圆:

      • 联立直线和圆方程(为了起见简便, 若\(B!=0\), 化为斜截式再联立), 求得系数\(tA\), \(tB\), \(tC\).
      • 根据\(Delta=tB^2-4*tA*tC\)判断交点个数
      • 若\(Delta\ge0\) , 根据求根公式求得交点横坐标, 进而求出交点.

        圆和圆:

      • 计算圆心距\(dis=\sqrt{(x_1-x_2)^2+(y_1-y_2)^2}\).
      • 比较圆心距\(dis\) 和半径和\(r_1+r_2\), 半径差\(|r_1-r_2|\) .
      • 若有交点则两圆相减求出相交弦方程, 进而转化为直线和圆得交点.
    • 注意到直线、射线、线段的不同点仅在于其坐标范围不同。因此为Line类引入范围属性(\(R \leq x \leq S\))即可同时表示三种线,其中射线、直线的无穷端以宏INF表示。求交点时,将

      Line

      Figure

      求交点,求得的交点再判断是否在

      Line

      所在范围内即可,若不在范围内则剔除。这就将直线、射线、线段统一了。

4. UML实体关系图

结对项目博客

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

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

可以看到,主要时间都花在了,

PlaneContainer.insert

方法的

set_union

函数上。

结对项目博客

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

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

此时

PlaneContainer.insert

方法已经不耗费太多时间了。

6. Design by Contract & Code Contract

Design by Contract

Code Contract

是指契约式设计和代码遵守的契约,是按照某种规定对一些数据等做出约定,如果超出约定,程序将不再运行,例如要求输入的参数必须满足某种条件,否则不会运行。我们在声明一个函数/方法的时候,对函数的输入和输出所具备的性质是有所期望和规定的。有时候这种性质会被我们明确的写出来,有时候会被我们忽略掉。这些期望和规定就是Contract。

契约编程在面向对象设计课程中有所涉及,第一次接触 JML,我的反应是:这也太傻了,不就是帮程序员把程序用伪代码实现了吗!其实这并不对,契约式编程的重要原则就是推迟对于过程的思考。JML是在代码中增加了一些符号,这些符号只表述一个方法要干什么,并不关心它的实现过程。

这样的契约编程在当时看来是非常耗时的,我们需要首先在设计好接口的条件下,先对所有的函数仔细思考其“输入” 及 “输出”,再进行具体的编码。

我们在结对编程中由于时间关系,并没有使用契约式编程的方法。经过个人项目的磨练,

core

中的核心代码涉及的函数在我们看来,其输入输出参数的条件都已经非常明了,特意地为了完成契约式编程的任务而改变相对更熟悉的工作方式,在我们看来是不明智的。

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

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

结对项目博客

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

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

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

// 射线 圆 内部相交
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);
           

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

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

  • 输入不满足标准格式

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

    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?");
               
  • 参数不在标准范围 (-100000, 100000) 内
  • 定义直线的两点重合
  • 圆的半径不大于零
  • 计算中出现无穷交点
    • 直线、线段或射线与直线、线段或射线出现部分或完全重合
    • 两圆完全重合

对于单元测试,我们对每一类异常场景及正常场景设计了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);
}
           

9. 界面模块的详细设计过程 & 10. 界面模与计算模块的对接

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

    用于定义

    Main

    函数外,整个模块一共只有类,下面结合图形、代码介绍该类的组成
结对项目博客
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<int> StraightLines;
        //射线容器,每四个数字代表一条射线
        private LinkedList<int> RayLines;
        //线段容器,每四个数字代表一条线段
        private LinkedList<int> LineSegments;
        //圆形容器,每三个数字代表一个圆形
        private LinkedList<int> 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)
    }
}
           
  1. 重点介绍以下几个函数的运行逻辑
    • private void inputButton_Click(object sender, EventArgs e);//文件输入

      • 若打开不成功则不进行处理并给出提示。
      结对项目博客
      • 否则,按行循环读取文件送

        string line

        ,调用

        add_Figure

        处理,并获取处理结果送

        int retCode

      • line

        retCode

        交给

        processRetVal

        处理。
      • 通过

        get_NumOfIntersectionPoints

        更新交点数目并显示
    • private void button2_Click(object sender, EventArgs e);//手工输入

      • 通过6个

        TextBox

        输入框获取输入数据。
      • 根据LRSC输入框输入的类型代码,将LRSC和后面的相应输入框中内容组成

        string line

        ,调用add_Figure处理,并获取处理结果送

        int retCode

      • line

        retCode

        processRetVal

      • get_NumOfIntersectionPoints

    • private bool processRetVal(int retCode, string line);

    • retCode==0

      ,未发生异常,调用

      store

      line

      存储。
    • 否则,不进行存储并提示相应的错误,提示用户,例如。
    结对项目博客
    • private void panel1_Paint(object sender, PaintEventArgs e);

      • 绘制坐标系、刻度等。
      • 调用

        drawLine

        drawCircle

        drawRay

        drawSegment

        绘制相应图形。
  2. 模块对接
    1. core模块不提供内存管理,由接口函数

      init_PlaneContainer

      dispose_PlaneContainer

      管理。
    2. 核心接口add_Figure供界面模块调用。
      1. 正则匹配,如果格式不符合要求,不进入下一步并返回相应的错误代码。否则予以解析进入下一步。
      2. 根据解析结果,构造相应的

        Figure

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

        Figure

        假如容器

        pc

        ,并捕获可能的异常,如果出现无穷交点等异常,不进入下一步,并返回相应错误代码。
      4. 正常返回。代码代码0;
  3. 使用图例
    结对项目博客

11. 结对过程描述

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

结对项目博客
结对项目博客

12. 结对编程的优缺点

优点:

  • 结对编程中,两个人都必须对代码熟悉,所以两个人都处于对代码不断地处于 “复审‘ 的过程,不断地审核、提高的过程。这样可以提高代码质量。
  • 结对编程可以让程序员更专注于工作,更注重代码质量、代码风格等。
结对编程的过程也是一个互相督促的过程,每个人的一举一动都在别人的视线之内,所有的想法都要受到对方的评价。由于这种督促的压力,使得程序员更认真地工作。
  • 结对编程使工作更容易、简单,因为一个人工作时遇到困难很容易陷入颓废无力的境况。而结对编程中,往往比较少产生两个人都无力解决的问题,即使遇到了,双方也会互相鼓励,而不是陷入恶性循环。

缺点:

  • 结对编程中如果双方的水平差不多,就能具有更高的效率。但如果双方的水平差距较大,则会出现强的一方拉着弱的一方走的情形。对于强的一方,可能承包了大部分工作,既累又没办法保障代码的质量;;而对于弱的一方,没有参与感,对项目一知半解,更产生了挫败感。在这种情况下,双方的交流也不在同一个层次上,结对编程不如独自编程。

结对伙伴的优点:

  • 有责任心
  • 自学能力强
  • 思路清晰

结对伙伴的缺点:

  • 有点贪玩,使项目完成时间略微滞后

我的优点:

  • 认真
  • 经常主动交流

我的缺点:

  • 没有脑子,做不了设计,只能跟着队友走