BUAA软工-结对项目

BUAA2020 软件工程-结对项目

Author:17373015 乔玺华

 

学号cnblog profile
17373260(本文作者) Prime21
17373015(结对队友)  
   
   
17373331(合作测试edge case伙伴) MisTariano
17231145(合作测试strong case伙伴) FuturexGO

一. 教学班级和github地址

项目内容
班级:北航2020春软件工程 006班(罗杰、任健 周五) 博客园班级博客
作业:设计程序求几何对象的交点集合,支持命令行和GUI 结对项目作业
个人课程目标 系统学习软件工程,训练软件开发能力
这个作业在哪个具体方面帮助我实现目标 实践结对编程等敏捷开发方法
项目地址 https://github.com/prime21/IntersectProject

二. PSP表格

PSP2.1预估耗时(分钟)实际耗时(分钟)
Planning 30 30
· Estimate 30 30
Development 1020 1050
· Analysis 90 120
· Design Spec 60 60
· Design Review 30 20
· 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
*In Total 1050 1350

三. 接口设计的方法

Information Hiding(信息隐藏)

信息隐藏甚是指让模块仅仅对外公开必须要让外部知道的内容,而将其他部份进行隐藏,在模块设计的接口设计部份中,就需要严格遵守信息隐藏这样的原则,即接口是模块的外部特征,如函数名,应当对外公开,而参数,结构体的定义、具体算法等,应当是模块的内部特征,应当进行隐藏,一个模块仅提供有限的接口,且外界仅仅可以获得该模块的接口,其他的私有属性一概隐藏。比如在本次作业中,要求将计算模块进行封装为动态链接库,提供给UI模块使用,因此在计算模块的头文件中,我们只包含了必要的函数名以及类的声明,具体实现放在dll文件中,并不需要让UI模块知道计算模块的实现细节,而只需要知道函数名并进行调用,即可获得计算结果。

Interface Design (接口设计)

接口设计,即实现信息隐藏以及程序结构化的重要方法,需要严格遵循信息隐藏原则,进行接口设计,避免将内部信息透露给外部,而在本次作业中,我们设计的接口为几个重要的函数以及类,提供给外部使用。并且在接口设计过程中,也需要与对方团队进行沟通,为之后实现模块互换的工作,打好基础。本次作业设计的接口包括:

  • vector< pair<double, double> > count(vector<string> s)返回一个vector,提供所有交点,方便GUI画图使用

  • int count_int返回int类型字符,即交点个数

Loose Coupling(松耦合)

松耦合,也就是模块之间的依赖程度或是提供的接口数量,越少越好。如在本次项目中,UI模块仅仅需要接触到计算模块的count以及count_int函数即可完成全部的功能,而具体如何找到所有的交点,内部判断去重复点等功能只需要在计算模块中实现即可,UI模块不需要也不能接触到这些接口,以此实现松耦合,实现与他人核心模块的互换。

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

松耦合组件设计

由于考虑到有和其他队伍交换dll的可能性,我们和他们详细的讨论了一次组件之间的设计模式.

总结如下:

  • 向量式的直线

  • 利用c++标准类型做数据间的传递通道

  • 组件函数的交叉验证

向量式的直线

我们把一条线​AB描述成p(t) = A + t * AB

对于点在线段有,点在射线,点在直线有,很好的解决了点在线上的判断问题。这样的好处是,最大可能的复用上次的代码,便于回归测试

 1 struct Line {
 2     int id;
 3     char tp;
 4     Point u;
 5     Point v;
 6     Line() { id = 0;  tp = 'L'; }
 7     Line(Point u, Point v) :u(u), v(v) { id = 0; tp = 'L'; }
 8     Line(Point u, Point v, char tp) :u(u), v(v), tp(tp) { id = 0; }
 9     bool operator == (const Line& A) const { //判断是否是同一条直线
10         if (tp != A.tp) {
11             return 0;
12         }
13         if (tp == 'S') {
14             if (u == A.u)
15                 return v == A.v;
16             Point uv = u + v;
17             Point auv = A.u + A.v;
18             if (uv == A.u)
19                 return u == auv;
20             return false;
21         }
22         if (tp == 'R') {
23             return u == A.u && !dcmp(v.y * A.v.x - A.v.y * v.x);
24         }
25         return !dcmp(v.y * A.v.x - A.v.y * v.x) && !dcmp((u - A.u) ^ (u - (A.u + A.v)));
26     }
27     Point point(double t) const {
28         return u + v * t;
29     }
30     bool online(Point x) {
31     
32     }
33     bool online(double len) { // 利用向量形式的特性
34         if (tp == 'R') {
35             return dcmp(len) >= 0;
36         }
37         else if (tp == 'S') {
38             return dcmp(len) >= 0 && dcmp(1 - len) >=0;
39         }
40         return 1;
41     }
42 };

其余类(算法类,点类,圆类)

对于点和圆,也是抽象成基本对象来描述。

 1 struct Point {
 2     double x, y;
 3     Point(double x = 0, double y = 0) : x(x), y(y) {}
 4     void read() {
 5         cin >> x >> y;
 6     }
 7     void print() {
 8         printf("%.10lf %.10lf\n", x, y);
 9     }
10     
11     bool operator == (const Point& rhs) const { // for unique
12         return (dcmp(x - rhs.x) == 0) && (dcmp(y - rhs.y) == 0);
13     }
14 15     bool operator < (const Point& rhs) const { // for sort
16         int d = dcmp(x - rhs.x);
17         if (d < 0) return true;
18         if (d > 0) return false;
19         if (dcmp(y - rhs.y)) return true;
20         return false;
21     }
22 23     friend Point operator + (const Point& lhs, const Point& rhs) {
24         return Point(lhs.x + rhs.x, lhs.y + rhs.y);
25     }
26 27     friend Point operator - (const Point& lhs, const Point& rhs) {
28         return Point(lhs.x - rhs.x, lhs.y - rhs.y);
29     }
30 31     friend Point operator / (const Point& lhs, const double& d) {
32         return Point(lhs.x / d, lhs.y / d);
33     }
34 35     friend Point operator * (const Point& lhs, const double& d) {
36         return Point(lhs.x * d, lhs.y * d);
37     }
38 39     friend double operator * (const Point& lhs, const Point& rhs) { // dot for vector
40         return lhs.x * rhs.x + lhs.y * rhs.y;
41     }
42 43     friend double operator ^(const Point& lhs, const Point& rhs) { // cross for vector
44         return lhs.x * rhs.y - lhs.y * rhs.x;
45     }
46 47     double length() {
48         return sqrt(x * x + y * y);
49     }
50 51     double length2() {
52         return x * x + y * y;
53     }
54     
55     pair<double, double> getPair() { // convert to standard type
56         return make_pair(x, y);
57     }
58 59 };

可以看到点类和pair<double,double>具有很好的相容性和相似性,不过我们定义了一些相关的几何任务函数,不再是没有语义的点对,同样也可以很方便的转换到std::pair<double,double>

 1 struct Circle {
 2     Point c;
 3     double r;
 4     Circle() {
 5         c = Point(0, 0);
 6         r = 0;
 7     }
 8     Circle(Point c, double r) :c(c), r(r) {}
 9     Point point(double a) const {
10         return Point(c.x + cos(a) * r, c.y + sin(a) * r);
11     }
12 };

比较有意义的是我们定义了算法类solve,这实际上是一种策略模式的思路把对于同一个需求的策略集中起来

struct solve {
    
    // 异常相关服务
    int isType(string); // 检查输入是何种类型
    int isSame(string,stsring) // 检查输入是否unique
    
    // 工厂服务
    Line getLine(string); // 根据输入的类型创建适当类型的直线、射线、线段的函数工厂接口
    Circle getCircle(string); // 根据输入的类型创建适当类型的圆的函数工厂接口
    
    // 原子操作
    void getLineInter(Line,Line,vector<Point> &); // 求得线交点,传引用的方式回收答案
    void getCircleInter(Circle,Circle,vector<Point> &); // 求得圆交点,传引用的方式回收答案
    void getLcInter(Linc,Circle,vector<Point> &); // 求得线圆交点,传引用的方式回收答案
    
    // 集合操作
    vector<Point> count_line(vector<Line>); // 求得线集合的交
    vector<Point> count_cir(vector<Circle>); // 求得线集合的交
    
    // 面向UI的接口,由于对面的数据结构未知,需要convert to标准c++支持的类型
    vector< pair<double,double> > count(vector<str>); // 给定需求求得交点集合
    int count_int(vector<str>); // 给定需求求得交点个数
}

通过solve我们把不同层次的需求,封装在了一起,可以很方便的适合多种粒度的询问与测试。粗粒度的GUI测试可以实现,超细粒度的单元测试,和中等粒度的命令行文件测试,都可以应对。

核心封装

注意到solve我们使用的全部是没有副作用的方式的管理和组织任务,而每一次GUI做图,实际上是一次向core提交任务,那么也就是说我们实现了利用消息传递的方式解决繁杂的调用问题。

故可以把上述核心类封装为dll文件和lib文件,方便快速部署在若干个复杂环境下。

同时我们也支持使用.h.hpp的静态调用方式,便于快速部署命令行应用程序。

五. 核心模块内的设计——UML阐释

UML 图显示计算模块部分各个实体之间的关系。

图中详细展示了坐标&点与几何对象两个系统内部的实体关系和两个系统之间的联系 - 在逻辑上Line,Circle都是可相交的几何对象 - 相交的逻辑结构和对象分离设计 - 维护Point所需的相关参数

考虑独立性,我们的部署结构如下

可以便于我们快速的部署

六. 性能评估与改进

记录在改进计算模块性能上所花费的时间,描述你改进的思路,并展示一张性能分析图,并展示你程序中消耗最大的函数。

参数的传递优化

取消hashset或者其他容器的使用

 

 

其中,主要的使用是在vector的插入这一步中

七. Design by Contract与Code Contract

契约式设计,即要求软件设计者为软件组件定义正式的、精确的并且可验证的接口,为传统的抽象数据类型增加了先验条件、后验条件和不变式。

  • 先验条件:为了调用函数,必须为真的条件,在其违反时,函数决不调用,传递好数据是调用者的责任。

  • 后置条件:函数保证能做到的事情,函数完成时的状态,函数有这一事实表示它会结束,不会无休止的循环

  • 不变式:从调用者的角度来看,该条件总是为真,在函数的内部处理过程中,不变项可以为变,但在函数结束后,控制返回调用者时,不变项必须为真。

  • 优点

    • 将前后端测试解耦,前后端可以分别在对方还没有完成工作的时候就开展测试;

    • 将测试过程前移,加速或者取代集成测试;

    • 保证数据的一致性,让后端服务返回的数据就是前端想要得到的。

  • 缺点

    • 严格按照契约式设计可能需要更长的设计时间,时间开销较大

  • 本次项目中的应用

    • 接口函数vector< pair<double, double> > count(vector<string> s),将所有的数据数据整合成一个vector<string>的形式,传输给计算模块,存储格式为type data1 data2 data3,返回值设定为vector<pair<double, double>>格式,即包含了所有需要在图上标注处的点的坐标,同时可以通过vector.size()获得点的个数,即要求的输出

八. 单元测试部分展示

覆盖率展示

在覆盖率上,我们有极其优秀的效果,可以看到solve的特性使得我们可以展开超细粒度、中等粒度和粗粒度多样化的测试。

basic.h基础库测试,覆盖率,逐行覆盖

calc.h计算任务相关测试,覆盖率,逐行覆盖

circle.h圆相关函数测试,覆盖率,逐行覆盖

core测试,覆盖率,逐行覆盖

point.hline.h是由于我们有一些接口供其他测试小组测试,所以并未完全覆盖,理论上在其他测试小组的系统下也进行了覆盖测试。

覆盖测试思路

  1. 使用随机强数据

    • 这部分有多个组的同学和我们讨论数据生成

      • 相离线段

      • 相切线段

      • 相交线段

        • 部分重叠线段

        • 完全重叠线段

        • 左对齐线段

        • 右对齐线段

        • 组合测试

      • 相切的圆和线段

      • 相离的圆和线段

        • 相离的圆和线段的延长线有交点

  2. 多种测试的综合测试

    • 随机测试组合

    • edge case组合测试

    • 多次回归测试

    • 其他组的错误数据集测试

  3. 需求测试和异常测试

九. 异常测试

我们支持异常任务和标准任务同时测试,测试效果如下

为了使core模块具有高度的兼容性和可扩展性,我们不仅仅是兼容了异常类、try-catch等主流写法,同时我们支持原生的类似windows中的写法:将异常错误码和错误信息打包进struct,在每个函数返回error code

在上述截图中,我们独立测试了

  1. 不合法的线段

  2. 不合法的直线

  3. 不合法的圆

  4. 不合法的射线

  5. 不合法的奇怪符号

  6. 超过范围的特殊数据

  7. 不合法的调用

可以看到,所有测试均通过。

十. 界面模块的详细设计过程

界面模块采用Qt进行开发,如上图所示

  • 左边为输出图像的窗口,采用QCustomPlot模块,可以将一个QWidget提升为画布,提供了一个精美的可以随意移动或是放缩的坐标系,在图中对于所有的交点以及各个线条的端点给出了标注,

    • 直线:QCPItemStraightLine

      QCPItemStraightLine *line = new QCPItemStraightLine(ui.plot);
    • 线段:QCPItemLine

      QCPItemLine *line = new QCPItemLine(ui.plot);
    • 射线:并未提供射线,因此选择使用射线的端点,到其最远点画线段来代替,由于本次所有的参数都存在一个范围,因此可以计算出最远端交点的坐标轴范围,以此来画出线段,代替直线

                  QCPItemLine *line = new QCPItemLine(ui.plot);
                  ui.plot->graph(0)->addData(x, y);
                  line->start->setCoords(x[0], y[0]);
                  x[1] = x[0] + 1000000000000000 * (x[1] - x[0]);
                  y[1] = y[1] + 1000000000000000 * (y[1] - y[0]);
                  line->end->setCoords(x[1], y[1]);
                  RLines.push_back(line);        
    • 圆:并未提供画圆的功能,但存在画曲线,因此选择画出圆上均匀的200个点,画出曲线来代替圆

              QCPCurve *circle = new QCPCurve(ui.plot->xAxis, ui.plot->yAxis);
              const int pointCount = 200;
              QVector<QCPCurveData> dataSpiral1(pointCount);
              for (int j = 0; j < pointCount; ++j)
              {
                  double theta = j / (double)(pointCount-1)*2*M_PI;
                  dataSpiral1[j] = QCPCurveData(j, r * qCos(theta) - x[0], r * qSin(theta) - y[0]);
              }
              circle->data()->set(dataSpiral1, true);
  • 右侧为控制台,供用户进行数据的添加、删除、加载文件以及获取输出

    • btn_run:当用户完成数据的编辑后,点击run按钮,即可在左边绘制图像,同时在下方的文本框中读取到数据。

    • btn_import:为用户提供导入文件的接口,用户点击import后,会弹出文件选择窗口,选择恰当的文本文件,即可将文本文件内的内容读取到下方的显示框中,进行进一步的操作

    • Line:为了防止用户的随意输入,增加程序的运行压力,此处选择下拉框将用户的选择局限在正确的类型中,并采用数字输入框,保证用户输入数字的范围,尽量避免不必要的麻烦,需要用户输入两个端点的横纵坐标

    • Circle:同上,但圆并没有过多的选项,同时需要用户输入圆的圆心,以及圆的半径

    • descript_list:显示用户对于输入图像的描述,格式为Type Data1 Data2 Data3

    • btn_delete:可以对descript_list中的对象进行删除

    • btn_clear:可以清楚descript_list中的所有对象

    • btn_add:根据所在部份调整好参数后,将描述添加到descript_list

  • Num of Nodes:输出交点个数

十一. 界面模块与计算模块的对接

core模块为UI提供了两个外部接口

  • vector< pair<double, double> > count(vector<string> s)

    • 将所有的数据数据整合成一个vector<string>的形式,传输给计算模块,存储格式为type data1 data2 data3,返回值设定为vector<pair<double, double>>格式,即包含了所有需要在图上标注处的点的坐标

  • int count_int

    • 将所有的数据数据整合成一个vector<string>的形式,传输给计算模块,存储格式为type data1 data2 data3,返回值设定为int格式,即问题要求的所有交点的个数

十二. 描述结对的过程

  • 在项目一开始,两人都先暴力实现了新增要求的功能,并进行对拍测试,队友pmxm的正确率较高,因此进行分工,他负责后端core模块的编写,我负责前端UI模块的编写

  • 使用微信、QQ进行项目的交流,同时有使用Tim的屏幕共享功能进行连线,讨论了扫描线算法的原理,以及具体实现,但在后续的具体实现中,发现扫描线算法的代码中漏洞过多,于是选择保证程序正确性为前提,放弃了扫描线算法,选择使用暴力计算,并在暴力的基础上对程序优化

  • 以下为交流过程中的截图

十三. 看教科书和其它参考书,网站中关于结对编程的章节,说明结对编程的优点和缺点。同时描述结对的每一个人的优点和缺点在哪里(要列出至少三个优点和一个缺点)

  • 优点:

    • 两个人共同完成项目,好的分工给可以减轻双方工作量,大大提高工作的完成的速度

    • 两人可以相互监督,相互提醒,避免出现dll选手的情况

    • 可以相互检查代码,自己写的bug往往自己难以看出,而互相检查代码就比较容易找到隐藏的bug

  • 缺点:

    • 两人代码风格存在差异,需要时间去磨合

    • 两人作息时间存在差异,且由于疫情原因,无法十分便捷的找到对方,可能出现一段时间找不到对方的情况,需要实现约好

    • 两人编写代码,需要给对方讲解代码的使用,需要耗费一定的时间

我与队友的缺点:

 队友
优点 有一定的时间观念,会push队友,尽量抓紧时间完成 算法基础扎实,且编程能力强,代码速度快
缺点 算法能力比较不足,之前没有接触过扫描线算法 缺少一些耐心

十四. PSP表格

见 二 中PSP表格

十五. 支持松耦合

我们选择和 xsy团队 完成附加题松耦合的工作

我们两组事先进行了大概的交流,UI的设计类似,并且警经过后期的修改,完成了双方接口的支持,几乎共用一套接口,除了对方UI还支持直接在图像中完成线段的删除,因此我们多增加了deletecCirle()以及deleteLine()的接口,并在互换头文件以及动态链接库后,实现了双方UI的正常工作。

此处展示为使用对方dll文件以及.h文件后的运行结果,与之前的运行结果完全一致

此处展示为对方UI加上我们的core核心模块的运行结果,可以正常运行

 

posted @ 2020-03-24 18:51  F_M_L  阅读(262)  评论(6编辑  收藏  举报