软工结对项目——图形交点PLUS
软件工程结对项目(图形交点Pro)
一、教学班级和GitHub地址
项目 | 内容 |
---|---|
这个作业属于哪个课程 | 2020春季计算机学院软件工程(罗杰 任健) |
这个作业的要求在哪里 | 结对项目作业 |
教学班级 | 006 |
项目链接 | https://github.com/notasadsong/pairproject_intersection |
二、PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 30 |
· Estimate | · 估计这个任务需要多少时间 | 10 | 10 |
Development | 开发 | ||
· Analysis | · 需求分析 (包括学习新技术) | 350 | 400 |
· Design Spec | · 生成设计文档 | 20 | 20 |
· Design Review | · 设计复审 (和同事审核设计文档) | 10 | 20 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | 30 |
· Design | · 具体设计 | 30 | 30 |
· Coding | · 具体编码 | 200 | 250 |
· Code Review | · 代码复审 | 30 | 40 |
· Test | · 测试(自我测试,修改代码,提交修改) | 150 | 250 |
Reporting | 报告 | 80 | 80 |
· Test Report | · 测试报告 | 20 | 20 |
· Size Measurement | · 计算工作量 | 20 | 20 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 10 | 20 |
合计 | 990 | 1220 |
本次作业和上次作业一样,时间的大头花费在了熟悉新工具方面,例如QT,dll动态库相关等,遇到了多次编辑器设置导致无法生成的情况(例如预编译头设置,附加库设置等),导致花费时间大大增加。
三、看教科书和其它资料中关于 Information Hiding,Interface Design,Loose Coupling 的章节,说明你们在结对编程中是如何利用这些方法对接口进行设计的。
1、信息隐藏
由于各个类中所包含的属性在本次作业中调用频繁且本次作业规模较小,所以并未将类中的属性和方法私有化。但是将具体的求解交点的函数实现封装在了核心计算模块内,隐藏了部分实现的代码信息。
2、接口设计
关于交点求解的函数在头文件中先设计好10中不同类型几何对象的求解方法,目的是为了实现大部分函数的复用,只关注接口需要什么以及接口返回什么,就可以先完成了主函数的书写,再回来对接口进行详细的实现过程。例如在本次作业中接口需要的是存入交点的容器的地址以及待求解的两个几何对象。
3、松耦合
松耦合的设计差不多就是接口的设计,商量好相同的预留接口之后就可以各自实现核心的接口,然后互相之间都可以进行调用。
四、计算模块接口的设计与实现过程
1、代码主要包含了三个类:
-
Line:用于表示线类的数据结构,其属性type有 L,S,R三种,分别代表了直线,线段和射线。其余的属性诸如一般式的 a,b, c就不过多描述。
-
Circle:用于表示圆类的数据结构。与个人项目中的圆类基本相同。
-
Solve:用于求解交点的方法类。在solve中有求解直线、射线、线段、圆互相之间的焦点的方法,其接口如下:
class _declspec(dllexport) Solve { public: void LLintersect(set<pair<double, double>, cmp>* intersections, Line l1, Line l2); void LRintersect(set<pair<double, double>, cmp>* intersections, Line l, Line r); void LSintersect(set<pair<double, double>, cmp>* intersections, Line l, Line s); void RRintersect(set<pair<double, double>, cmp>* intersections, Line r1, Line r2); void SSintersect(set<pair<double, double>, cmp>* intersections, Line s1, Line s2); void SRintersect(set<pair<double, double>, cmp>* intersections, Line s, Line r); void LCintersect(set<pair<double, double>, cmp>* intersections, Line l, Circle c); void RCintersect(set<pair<double, double>, cmp>* intersections, Line r, Circle c); void SCintersect(set<pair<double, double>, cmp>* intersections, Line s, Circle c); void CCintersect(set<pair<double, double>, cmp>* intersections, Circle c1, Circle c2); };
2、计算函数说明
- LLintersect:直接使用公式法求解,不多赘述
- LRintersect:使用LLintersect求得交点,再判断交点是否在射线上
- LSintersect:判断线段的两个端点是否在直线的两侧,若在同侧则没有交点,异侧则代表有交点,可以使用LLintersect求得。
- RRintersect:先判断两条射线是否共线,若不共线则使用LLintersect求得交点后再判断交点是否在射线上;若共线则判断是否会出现端点相同方向相反的极端情况(该情况下有一个交点),否则抛出异常。
- SSintersect:先判断两条线段是否都在彼此所在的直线的两侧(包括端点刚好在线上的情况);再判断是否共线,不共线就使用LLintersect求解,共线则进一步判断是否有特殊情况(共线又只有一个交点)。若无穷交点则抛出异常。
- SRintersect:先判断是否共线,不共线就直接使用LLintersect求得交点并判断交点是否同时在在射线和线段上;共线则判断是否是特殊情况(共线又只有一个交点),若无穷交点则抛出异常。
- LCintersect:分情况相离相切相交三种情况,使用投影向量法,与个人项目相同,不多赘述。
- RCintersect:使用LCintersect求得交点后,判断交点是否在射线上。
- SCintersect:使用LCintersect求得交点后,判断交点是否在线段上。
- CCintersect:方法与个人项目相同,不多赘述。
五、UML图
六、计算模块接口部分的性能改进
1、射线线段相关计算简化
一开始使用的方法是将射线/线段全部当作直线求解出交点以后,判断交点是否会在射线/线段上。这样的写法简单直接,但是一些没有交点的情况是可以提前判断免去计算的,改进了之后的计算方法节约了一些计算资源,可以概括为:
- 射线:判断延伸方向。
- 线段:判断是否两个端点在另一条直线的两侧。
2、尝试改用容器
可以看出在代码中性能消耗最大的是求交点的函数,而在求交点的函数中小号最大的就是set容器的插入操作,占据50%以上。所以我们曾想过将set容器改为hashmap容器,但是后面发现,用hashmap需要重写hash函数,而在这上面花费的时间比创建红黑树还多,所以我们最后还是采用了现在的这种容器。
七、Design by Contract,Code Contract
契约式设计或者Design by Contract (DbC)是一种设计计算机软件的方法。这种方法要求软件设计者为软件组件定义正式的,精确的并且可验证的接口。他的三个关键词是:前置条件,后置条件,类不变项。
优点:
- 代码严格按照约定的条件来运行,程序员能够保证每一块代码在任何情况下都能实现它的功能。
- 将各模块之间的耦合度降低,有很高的可修改性,可维护性和鲁棒性。
缺点:
- 在实际代码开始书写前制定好完整的规范比较困难,需要一步一步完善约束条件。
- 对于小型的项目来说各个条件的约束的完成可能会超过工程核心功能实现的工作量,过于繁琐。
在本次的项目里我们尽可能完整的考虑了各种情况下的输入异常,保证了我们的程序在任何输入的情况下都能够完成功能,返回结果或者抛出异常。而输入的一方并没有进行相应的约束,但是代码能够捕获一定的输入错误抛出异常。我们严格按照所设计的接口进行功能的填充,规定每个模块的输入,返回和异常,放映了部分契约式设计的特点。
八、计算模块部分单元测试展示
在这一次的单元测试中,对求解交点的方法类Solve中的函数进行测试。根据Slove中的方法把测试分为了LL,LR,LS,RR,RS,SS,LC,RC,SC,CC十个部分来构造,针对每个部分的内部根据其几何性质可能还会有不同情况(如平行,共线但只有一个交点,相离相切等等)。
部分的单元测试代码如下:
单元测试的通过情况截图如下:
测试得到的覆盖率截图如下:
九、计算模块部分异常处理说明
-
输入的命令行参数个数不对
-
输入的命令行参数不匹配
-
坐标范围超限/坐标不是整数
-
直线定义中两点重合
-
圆半径不为正数
-
有无穷交点
-
几何对象数目应为整数
-
几何图形只能是L,R,S,C
-
L,R,S只能有4个参数
-
C只能有3个参数
-
几何对象参数均为不含前导零的整数
十、界面模块的详细设计过程
1、首先,布局,我在界面左边有一块白色画布,右边是输入框,添加,删除,从文件导入三个按钮。首先我先画出了坐标系,然后再在坐标系里画线和圆。界面模块最重要的就是信号和槽了,而这也决定了点击相应按钮然后界面做出的反应。而添加信号槽代码如下:
connect(ui->pushButton,SIGNAL(clicked()),this,SLOT(Paint()));
connect(ui->pushButton_2,SIGNAL(clicked()),this,SLOT(Delete()));
connect(ui->pushButton_3,SIGNAL(clicked()),this,SLOT(Fl()));
也就是将按钮的点击信号与自己所定义的信号槽函数所连接。而添加按钮也就对应于绘图操作,删除按钮也就对应于删除操作。
2、然后接下来重要的就是绘图了。而绘图中,我们需要保存界面上已有的数据,由于本次数据量较少,所以我直接就用全局变量存储几何图形的信息。然后每一次点击按钮的操作都将会刷新图像。绘制几何图形的代码如下所示:
for (int i = 0; i < lines.size(); i++)
{
if (lines[i].type == 'R')
{
painter.drawPoint(get_x(lines[i].x0), get_y(lines[i].y0));
}
else if (lines[i].type == 'S')
{
painter.drawPoint(get_x(lines[i].x0), get_y(lines[i].y0));
painter.drawPoint(get_x(lines[i].x1), get_y(lines[i].y1));
}
painter.drawLine(get_x(lines[i].x0), get_y(lines[i].y0), get_x(lines[i].x1), get_y(lines[i].y1));
}
for (int i = 0; i < circles.size(); i++)
{
painter.drawEllipse(get_x(circles[i].x) - 10 * circles[i].r, get_y(circles[i].y) - 10 * circles[i].r, 20 * circles[i].r, 20 * circles[i].r);
}
十一、界面模块与计算模块的对接
1、界面模块与计算模块进行对接则在这次项目中比较简单,因为界面模块只需要展示几何图形的交点即可,所以我们只用调用计算模块的接口写一个绘制交点的函数即可。部分代码如下所示:
for (auto it : intersections)
{
painter.drawPoint(it.first, it.second);
QString s = "(" + QString::number(it.first) + "," + QString::number(it.second) + ")";
painter.drawText(get_x(int(it.first)) + 20, get_y(int(it.second)), s);
}
这里就是将已经计算好的交点容器遍历并绘制。
2、具体功能
-
添加几何图形
-
删除几何图形
-
从文件导入
十二、描述结对过程
我们结对编程的过程一般使用手机打开微信视频或QQ视频进行通话交流,而在电脑上使用VS中的Live Share功能共享对一个代码文件的编辑,两个人共同构思提出想法后,一人写代码,一人观察对方的编写是否有错误或者疏漏,同时思考架构上是否有改进的可能。这样的结对编程的关系对应的是驾驶员和领航员的关系。
相关截图:
十三、说明结对编程的优点和缺点。同时描述结对的每一个人的优点和缺点在哪里(要列出至少三个优点和一个缺点)
1、结对编程模式
- 优点:互相鼓励,互相监督,互相学习,互相查缺补漏减少bug
- 缺点:我们因为大部分工作一起完成其实时间利用率上不高,完成项目花费的时间较长;而且若是不熟悉的人之间结对可能不会像我们这样熟悉的人一样彻底的沟通(可能有碍于面子不好明说等情况)。
2、结对双方:
- 对友:对友的编码能力较强,学习新东西的速度较快,所以将ui部分的代码交给对友完成。对友写代码速度快但是容易漏掉边界信息,比较马虎。
- 我:由于写代码我习惯写一小块反思一小块测试一小块,因此出错的可能性有所降低,所以由我来负责核心的计算模块以及单元测试的书写。但是本人写代码速度较慢,并且欠缺一些灵活的应变。