• 软件工程结对项目博客作业


    项目 内容
    本作业属于北航 2020 年春软件工程 博客园班级连接
    本作业是本课程个人项目作业 结对项目作业
    我在这个课程的目标是 提高软件开发能力、团队协作能力
    这个作业在哪个具体方面帮助我实现目标 增加协作开发经验
    项目代码 https://github.com/btapple/Intersect

    一、需求分析与UML图

      与上一次的个人项目作业相类似,本次的任务关键在于交点的求解。至于新需求“交点绘制”,是简单扩展,实际上是将每个交点的坐标反馈到用户。

      本次任务增加了两种图形:线段与射线,它们都是特殊的直线。为什么是特殊的“直线”呢?因为本质上,这两种图形都是将无限长直线进行截断而形成的。

      由于这种性质,我们不难发现,整体的求解过程几乎没有改变。上一次的求解划分方法抄录如下

    • 直线与直线
      • 平行:交点个数 0
      • 同一条直线:交点个数无限
      • 相交:交点个数 1
    • 直线与圆
      • 相离:交点个数 0
      • 相切:交点个数 1
      • 相交:交点个数 2
    • 圆与圆
      • 相离:交点个数 0
      • 相切:交点个数 1
      • 相交:交点个数 2
      • 内含:交点个数 0

      但是,射线与线段毕竟不是直线,因此我们需要考虑因截断带来的影响,即所求得的交点是否在图形上。而求解交点的过程,依旧是参见 Paul Bourke 先生的文章

      除此以外,我们还需要考虑射线与线段之间的新的交点,即端点的重合。

    UML图如下:

    二、接口设计与实现

    Information Hiding,Interface Design,Loose Coupling

      我们本次的接口设计依照了Imformation Hiding(信息隐藏)原则与Loose Coupling(松耦合)原则,体现在通过一个特定的管理器指针与调用方互动,不会涉及类成员的修改,保证信息的隐藏;通信的参数都是基本类型参数,双方不必为各自的实现考虑,保证松耦合特性。

    Design by Contract,Code by Contract

      Design by Contract,契约式设计,其强调前置条件、后置条件与不变式,是一种形式约束。DbC的优点正如它的初衷,能够保证程序正确,它是一种形式逻辑上的正确,而不是测试之后得出的正确率,所以非常可靠,但其缺点是使得代码臃肿、工作量增大。OO课程中有一个单元练习的JML体现的便是这种思想,当程序复杂时带来的约束编写的工作量也会成倍增加。Code By Contract也是同样的道理,按照约束编写能够保证可靠,但是强行依照断言编写程序会造成代码的冗余和复杂,且如果约束复杂,也会增加理解交流的成本,降低效率。

    本次结对项目中,我们没有过多使用Contract这种理念,而是在需求分析时确定应有的约束,保证项目“敏捷”地进行下去。

    接口实现

      考虑到我们所要编写的库需要面对的环境是未知的,即不清楚会被什么语言以什么形式调用,因此我们选择了最广泛的 C 形式。我们希望,调用语言只要能够获取变量的地址,就能顺利地调用我们的库。具体的接口如下

    // 以下的 CORE_API 均是 __declspec(dllexport),声明将要在 dll 中导出
    extern "C" CORE_API GraphManager * create_graph_manager();
    
    extern "C" CORE_API int add_line(GraphManager*, char*, Type, INTTYPE, INTTYPE, INTTYPE, INTTYPE);
    
    extern "C" CORE_API int add_circle(GraphManager*, char*, Type, INTTYPE, INTTYPE, INTTYPE);
    
    extern "C" CORE_API void remove_graph(GraphManager*, int);
    
    extern "C" CORE_API int calculate_intersect(GraphManager*, char*, INTTYPE);
    
    extern "C" CORE_API int fetch_intersect(GraphManager*, char*, FLOATTYPE*, FLOATTYPE*);
    
    extern "C" CORE_API void clear_manager(GraphManager*);
    
    extern "C" CORE_API void dispose_graph_manager(GraphManager*);
    

    各个接口分别的作用是:

    • create_graph_manager:创建 GraphManager
    • add_line:向指定的 GraphManager 添加一条线(可能是直线、线段或射线,由 Type 指定)
    • add_circle:向指定的 GraphManager 添加一个圆
    • remove_graph:从指定的 GraphManager 中删除一个图形
    • calculate_intersect:计算指定的 GraphManager 中所管理的图形的交点
    • fetch_intersect:从指定的 GraphManager 中获取交点信息
    • clear_manager:清空指定的 GraphManager
    • dispose_graph_manager:销毁指定的 GraphManager

    性能分析与改进

    性能分析结果:

    其中消耗最大的函数是求交点函数calculate_intersect

      由于改进的切入点只有交点计算的算法改进,而计算的算法没有稳定改进办法,所以性能改进上处理不多。

    三、异常处理

      这一部分是接口设计的后续内容。

      异常处理是必要的,因为我们无法假设调用方的调用方式,否则调用方与被调用方将存在一定的非必要的耦合关系。

    以下面这个接口为例子

    extern "C" CORE_API int add_line(GraphManager* gm, char* msg, Type type, INTTYPE x1, INTTYPE y1, INTTYPE x2, INTTYPE y2);
    

      调用方在尝试调用这个接口之前,对内部实现没有了解,不清楚直线各个参数的限制(当然,库文档一般会提供,我们假设文档中没有注明),那么可能会错误地提供了两个相同的点,或者超过限制范围的坐标值等等,这些都是需要反馈的。

      那么,问题就在于如何反馈异常。首先,不可能抛异常(尽管我们的库是使用 C++编写的,具有抛异常能力),不同的语言与环境的异常模型未必相同,调用方不一定能够完成异常的处理。我们依旧需要一个通用的做法,选择返回错误信息的长度与错误信息的指针(对于本接口,返回值即错误信息长度,msg 用于存储错误信息的指针),如果错误信息的长度为 0,意味着没有异常发生,这里同样只有基本类型的参与。

    根据需求,我们设计了以下几种异常类型:

    • 直线两点重合
    • 输入参数的大小不在限定范围内
    • 圆半径为负
    • 在计算交点之前就尝试获取交点信息
    • 无交点却尝试获取交点信息
    • 输入数据中具有重合或重复的图形

      对于最后一种情况,即会产生“无限多交点”的情况,我们容许它的发生,会以警告的形式反馈给用户,但是会将发生重合的图形连接成为一个图形进行计算。

    四、单元测试

    先展示覆盖情况(使用 OpenCPPCoverage 生成)

    测试的框架如下

    static int main_ret = 0;
    static int test_count = 0;
    static int test_pass = 0;
    
    #define EXPECT_EQ_BASE(equality, expect, actual) 
        do {
            test_count++;
            if (equality)
                test_pass++;
            else {
                std::cerr << __FILE__ << ":" << __LINE__ << ": expect: " << expect << " actual: " << actual << "
    "; 
                main_ret = 1;
            }
        } while(0)
    #define EXPECT_EQ(expect, actual) EXPECT_EQ_BASE((expect) == (actual), expect, actual)
    #define EXPECT_TRUE(actual) EXPECT_EQ_BASE((actual) != 0, "true", "false")
    #define EXPECT_FALSE(actual) EXPECT_EQ_BASE((actual) == 0, "false", "true")
    
    void gm_test()
    {
        auto gm = create_graph_manager();
        ... // 测试项目
        dispose_graph_manager(gm);
    }
    
    int main()
    {
        gm_test();
        printf("%d/%d (%3.2f%%) passed
    ", test_pass, test_count, test_pass * 100.0 / test_count);
        return main_ret;
    }
    

    测试项目有功能性测试和异常测试。

    首先是功能性测试,以测试两圆外切情况为例

    clear_manager(gm);                              // 首先清空 GraghManager
    add_circle(gm, nullptr, Type::circle, 0, 0, 1); // 加入一个圆,用空指针接收错误信息可以及时发现异常的发生
    add_circle(gm, nullptr, Type::circle, 2, 0, 1); // 加入另一个圆
    EXPECT_EQ(1, calculate_intersect(gm));          // 计算交点数并进行比较
    

      测试数据的构造会考虑正负零以及在数据范围附近的情况(-100000, 100000),分类的依据可见需求分析。

    然后是异常测试,大致如下

    // 两点重合
    clear_manager(gm);
    EXPECT_TRUE(add_line(gm, msg, Type::line_segment, 0, 0, 0, 0) > 0);
    // 超出数据范围
    clear_manager(gm);
    EXPECT_TRUE(add_line(gm, msg, Type::line_segment, 10000000, 0, 0, 0) > 0);
    // 超出数据范围
    clear_manager(gm);
    EXPECT_TRUE(add_line(gm, msg, Type::line_segment, 0, 0, 0, -10000000) > 0);
    // 超出数据范围
    clear_manager(gm);
    EXPECT_TRUE(add_circle(gm, msg, Type::circle, 0, 0, -10000000) > 0);
    // 半径为负
    clear_manager(gm);
    EXPECT_TRUE(add_circle(gm, msg, Type::circle, 0, 0, -1000) > 0);
    // 计算交点前获取交点信息
    clear_manager(gm);
    EXPECT_TRUE(fetch_intersect(gm, msg, &x, &y) > 0);
    // 无交点获取交点信息
    calculate_intersect(gm, msg, &point_num);
    EXPECT_TRUE(fetch_intersect(gm, msg, &x, &y) > 0);
    // 输入数据中具有重合或重复的图形
    add_circle(gm, nullptr, Type::circle, 0, 0, 3);
    add_circle(gm, nullptr, Type::circle, 0, 0, 3);
    EXPECT_TRUE(calculate_intersect(gm, msg, &point_num) > 0);
    

    五、UI设计及与计算模块的对接

      界面模块我们选取的开发框架是WPF,开发语言为C#。由于项目给出的界面模块需求为几何对象的文件导入、增删、绘制与交点求解,这些需求都可以通过按钮点击事件完成,故界面模块的总体设计为:上方用画板展示绘制内容,下方排布按钮进行控制。完成后的初始界面如下:

    画布设计

      画布设计又分为坐标网格的绘制与几何图形的绘制,下面解释二者的实现细节。

      坐标轴的绘制比较简单,只需在画布中央画两条直线,其中y轴的两端为画布上下边界的中点,x轴的两端为画布左右边界的中点。而坐标网格的绘制其实也只是坐标轴绘制的一个加强版,横纵线交错即为网格;坐标的绘制通过TextBlock实现。下面是展示沿X轴方向的坐标网格和坐标的绘制,y轴同理翻转即可。

    几何对象绘制设计

      几何对象的绘制分为线绘制和圆形绘制,其中线绘制又分为无限长直线、射线和线段。

      线绘制最为麻烦,因为WPF框架中给出的现有方法只支持绘制线段,不支持无限长直线。我们对于无限长直线和射线的实现办法为:添加边界点。边界点的求法为:令横坐标x为INF_X(这里取INF_X为画布宽度的一半,也就是把画布放到二维坐标系的中心时的边界值),利用直线方程求出y。需要考虑平行于坐标轴的情况,下方代码不再赘述:

    // input: x1, y1, x2, y2
    double A = y2 - y1, B = x1 - x2, C = x2 * y1 - x1 * y2;
    double edge_x1, edge_x2, edge_y1, edge_y2;  // 1为边界起点,2为边界终点
    double INFX = frame_width / 2;
    
    if (x2 < x1)
    {
        edge_x1 = INFX;
        edge_x2 = -INFX;
    }
    else
    {
        edge_x1 = -INFX;
        edge_x2 = INFX;
    }
    edge_y1 = (-C - A * edge_x1) / B;
    edge_y2 = (-C - A * edge_x2) / B;
    

      之后即可根据线对象的类型进行绘制。需要注意的是,之前在二维坐标系中进行计算,最后绘制时应将点坐标转换到画布的坐标系(也就是二维坐标系沿x轴翻转后取第四象限)中去:

    Point start;
    Point end;
    switch (type)
    {
        case Type.infinite_line:    // 无限长直线
            start = convert_point(edge_x1, edge_y1);
            end = convert_point(edge_x2, edge_y2);
            break;
        case Type.line_segment:     // 射线
            start = convert_point(x1, y1);
            end = convert_point(edge_x2, edge_y2);
            break;
        case Type.segment: default:    // 线段
            start = convert_point(x1, y1);
            end = convert_point(x2, y2);
            break;
    }
    
    LineGeometry line = new LineGeometry();
    line.StartPoint = start;
    line.EndPoint = end;
    
    Path path = new Path();
    path.Stroke = Brushes.Black;
    path.StrokeThickness = 1;
    path.Data = line;
    
    mainPanel.Children.Add(path);
    

    convert_point的代码如下:

    private Point convert_point(double x, double y)
    {
        return new Point(x * SCALE + x_offset, -y * SCALE + y_offset);
    }
    

      圆形与坐标点的绘制可以使用WPF提供的EllipseGeometry(椭圆)类。二者区别只有空心实心和半径大小,下面给出坐标点的绘制方法:

    // input: x, y
    Point p = convert_point(x, y);
    EllipseGeometry el = new EllipseGeometry();
    int pointR = 2;
    
    el.RadiusX = pointR;
    el.RadiusY = pointR;
    el.Center = p;
    
    Path path = new Path();
    path.Stroke = Brushes.Black;
    path.StrokeThickness = 2;
    path.Fill = Brushes.Black;
    path.Data = el;
    
    mainPanel.Children.Add(path);
    intersections.Add(path);
    

    按钮设计

    按钮的功能设计如下:

    • Files:从文件导入几何对象的描述并绘制(不独立设置“绘制”按钮是为了更直观地展示输入的几何对象,避免用户进行重复冗余的点击操作,下面的增添/删除按钮也贯彻了这一理念)。
    • Intersect:求解现有几何对象的交点并绘制。
    • Delete:选择某一个几何对象,将其从画布上删除,并删除画布中的所有交点
    • Add:根据选择的类型及输入增添一个几何对象,并在画布上画出。
    • Clear:清空画布上所有的几何对象及交点
    • Scale+/-:放大/缩小画布的坐标系,将会删除图上所有的几何对象和交点。

      下面展示这些按钮触发事件的实现细节。

      按钮事件函数的写法为:

    private void Button_click(object sender, RoutedEventArgs e)
    {
        // xxx
    }
    
    1. Files

      var dialog = new Microsoft.Win32.OpenFileDialog
      {
          Filter = ".txt|*.txt"
      };
      if (dialog.ShowDialog(this) == false) return;
      string fileName = dialog.FileName;
      
      Console.WriteLine(fileName);
      
      drawer.clearAll();      // 清除画布
      drawer.DrawXY();        // 重新绘制坐标轴
      drawer.ReadGraphFromFile(fileName);
      

      其中read_graph_from_file()方法的实现流程为:

      1. 读文件第一行的数字
      2. 根据数字按行读取并parse
      3. 将parse后获得的几何对象信息输入到core中的计算模块进行添加
      4. 调用线绘制方法进行绘制

      其中还涉及若干错误判断,使用MessageBox.show()报告错误

    2. Intersect

        该事件主要调用core中的计算模块,并使用core中实现的取交点函数逐一获取所有交点。

      public void calc_and_draw_intersects()
      {
          // 计算交点
          int r = NativeMethods.calculate_intersect(core_graph_manager, msg, ref n);
          if (r > 0)
          {
              MessageBox.Show(msg.ToString());
          }
      
          for (long i = 0; i < n; i++)
          {
              // 从core中取一个交点坐标
              r = NativeMethods.fetch_intersect(core_graph_manager, msg, ref x, ref y);
              if (r > 0)  // 如果有错误信息
              {
                  MessageBox.Show(msg.ToString());
                  break;
              }
              else
              {
                  drawIntersectPoint(x, y);
              }
          }
      }
      
    3. Delete

        该事件弹出新窗口(项目中为GraphsWindow),获取当前所有几何对象的信息,并使用ListBox陈列,用户确认选择后分别删除其在ListBox中的描述、画布中的图形以及在core中的对象,并删除所有交点。

        画布中删除某一图形的逻辑如下:

      // input: graph id
      // remove graoh on Canvas
      mainPanel.Children.Remove(graphs[id]);
      
      // remove info
      graphs.Remove(graphs[id]);
      
      // remove points on graph
      Path[] points = pointsOnGraph[id];
      foreach (Path point in points)
      {
          mainPanel.Children.Remove(point);
      }
      pointsOnGraph.Remove(points);
      
      // remove intersections(all)
      foreach (Path intersect in intersections)
      {
          mainPanel.Children.Remove(intersect);
      }
      intersections.Clear();
      
      // remove data in core
      remove_graph(core_graph_manager, id);
      
    4. Add

        弹出新窗口进行几何对象的添加。实现方法是使用ComboBox实现下拉栏选择四种几何对象类型的其中一个,并根据类型的不同给出不同数量的参数输入框(TextBox)。提交输入时进行正确性检测。最后将正确的输入整合为一行,利用按行处理文件输入的方法进行添加处理。

      string wrongMsg = "";
      verify_TextInput(tbox1, tblock1, ref wrongMsg);
      verify_TextInput(tbox2, tblock2, ref wrongMsg);
      verify_TextInput(tbox3, tblock3, ref wrongMsg);
      if (combo.SelectedIndex != (int)ComboItem.C)    // 圆形没有第四个参数输入框,只有x, y, r
      {
          verify_TextInput(tbox4, tblock4, ref wrongMsg);
      }
      
      if (wrongMsg.Length != 0)
      {
          MessageBox.Show(wrongMsg);  // 输入栏的错误提醒
      }
      else
      {
          string info = combo.Text + " " + tbox1.Text + " " + tbox2.Text + " " + tbox3.Text;
          if (combo.SelectedIndex != (int)ComboItem.C)
          {
              info += " " + tbox4.Text;
          }
          try
          {
              drawer.AddGraphFromLine(info);  // 添加该行对应的几何对象信息
          }
          catch (FormatException)
          {
              MessageBox.Show("Wrong Format!");   // 在执行添加逻辑时的错误报告
          }
      }
      
    5. Clear

        该事件包括清除画布内容、几何对象所有信息,并重新绘画坐标网格。清除的细节如下:

          mainPanel.Children.Clear();     // 清楚画布内容
          graphs.Clear();                 // 清楚几何对象绘制信息
          intersections.Clear();          // 清除交点绘制信息
          pointsOnGraph.Clear();          // 清除几何对象上的点(起点、终点、圆心)的绘制信息
          graphsInfo.Clear();             // 清除用于生成ListBox的几何对象信息
          core_graph_manager = IntPtr.Zero;   // 清除core中的manager
      
    6. Scale+/-

        该事件由调整画布尺寸参数SCALE实现。坐标网格绘制中,在绘制垂直x轴的坐标网格的循环语句中,网格密度是根据SCALE来调整的,由此实现画布的尺寸增减:

      for (int i = SCALE; i < frame_width/2; i+=SCALE)
      

      在实现中,用户每点击一次按钮Scale+/-,首先清除画布,SCALE增加/减少5,并进行设定边界,避免无限放大,然后再重新绘制坐标轴。

      如下是实现的大致逻辑

      if (SCALE >= 100)
      {
          MessageBox.Show("Reach Max Scale", "Note");
          return;
      }
      clear_all();
      SCALE += 5;
      drawXY();
      

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

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

      计算模块生成动态链接库提供,由于界面模块使用的是WPF框架及C#进行开发,可以通过DLLImport语句可以导入dll中的接口进行使用。

      在界面模块中导入接口函数的语句如下:

    [DllImport("core.dll")]
    internal static extern IntPtr create_graph_manager();
    
    [DllImport("core.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true)]
    internal static extern int add_line(IntPtr gm, StringBuilder msg, int type, long x1, long y1, long x2, long y2);
    
    [DllImport("core.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true)]
    internal static extern int add_circle(IntPtr gm, StringBuilder msg, int type, long x, long y, long r);
    
    [DllImport("core.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true)]
    internal static extern int calculate_intersect(IntPtr gm, StringBuilder msg, ref long res);
    
    [DllImport("core.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true)]
    internal static extern int fetch_intersect(IntPtr gm, StringBuilder msg, ref double x, ref double y);
    
    [DllImport("core.dll", CallingConvention = CallingConvention.Cdecl)]
    internal static extern void remove_graph(IntPtr gm, long id);
    
    [DllImport("core.dll", CallingConvention = CallingConvention.Cdecl)]
    internal static extern void dispose_graph_manager(IntPtr gm);
    

      为了达到松耦合的条件,界面模块中不涉及计算模块的类操作。通过IntPtr型的计算模块管理器对接各个接口。如初始时创建新的计算模块管理器:

    IntPtr graph_manager = create_graph_manager();
    

      之后在添加一条直线,根据输入得到的类型及坐标,即可如下添加一条直线:

    add_Line(graph_manager, msg, type, x1, y1, x2, y2);
    

      其中msg用于获取计算模块的错误信息,通过MessageBox报告给用户。

    实现的功能如下:

    1. 从文件中读取几何图形信息并画出(Files按钮)

    2. 求交点并画出(Intersect按钮)
    3. 删除一个几何图形(Delete按钮)

      2020-03-21-22-12-26.png
    4. 增添一个几何图形(Add按钮)

    5. 清空画布(Clear按钮)

    6. 画布尺寸增减(将会删除图形)
      • Scale+按钮
      • Scale-按钮

    七、结对编程过程

      我们采用的结对编程方式是Live Share+语音,其中存在的问题,一方面是live share本身的不稳定,对方的修改可能经过一定延迟才能看到;另一方面是,互相不太了解可能产生一定的阻碍。但结对编程也带来一定好处,比如能够学习到队友比较良好的代码规范和新颖的编码技巧,并且有人一起review也减少了错误的产生,遇到比较纠结的问题时也能够通过沟通找到答案。队友在编码规范、设计能力以及项目管理等方面都很优秀,在交流中也能够精准地给出解答的方向,有时我可能会跟不上。我在此次项目中也用心、勤奋、认真地参与其中,但是对于代码规范、版本管理等细节问题还存在缺陷。

    讨论图形界面时:

    讨论模块对接时:

    八、PSP表格

    PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
    Planning 计划
    · Estimate 估计这个任务需要多少时间 30 30
    Development 开发
    · Analysis 需求分析 (包括学习新技术) 120 240
    · Design Spec 生成设计文档 30 30
    · Design Review 设计复审 (和同事审核设计文档) 20 20
    · Coding Standard 代码规范 (为目前的开发制定合适的规范) 10 5
    · Design 具体设计 60 60
    · Coding 具体编码 120 240
    · Code Review 代码复审 30 60
    · Test 测试(自我测试,修改代码,提交修改) 60 120
    Reporting 报告
    · Test Report 测试报告 10 10
    · Size Measurement 计算工作量 10 10
    · Postmortem & Process Improvement Plan 事后总结, 并提出过程改进计划 60 30
    合计 560 855
  • 相关阅读:
    利用API对OWLS描述的服务进行操作 转贴
    转arcgis server部署 自己安装的体会
    最后一次的温柔
    JUDDI安装完整版 (转帖W3CHINA)
    jUDDI安装总结
    Protege中安装owls editor、graphviz插件
    改完了开题报告 舒口气
    服务组合的QoS信息
    owls editor
    一、性能测试术语
  • 原文地址:https://www.cnblogs.com/kingkongk/p/12549257.html
Copyright © 2020-2023  润新知