【软件工程】结对项目作业
1、简报
项目 | 内容 |
---|---|
这个作业属于哪个课程 | 2020春季计算机学院软件工程(罗杰 任健) |
这个作业的要求在哪里 | 结对项目要求 |
教学班级 | 005 |
项目地址 | BUAASE-PairProgramming |
2、PSP表
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
· Estimate | · 估计这个任务需要多少时间 | 30 | 30 |
Development | 开发 | ||
· Analysis | · 需求分析 (包括学习新技术) | 300 | 350 |
· Design Spec | · 生成设计文档 | 60 | 30 |
· Design Review | · 设计复审 (和同事审核设计文档) | 60 | 30 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 10 |
· Design | · 具体设计 | 60 | 60 |
· Coding | · 具体编码 | 1000 | 800 |
· Code Review | · 代码复审 | 60 | 120 |
· Test | · 测试(自我测试,修改代码,提交修改) | 120 | 120 |
Reporting | 报告 | ||
· Test Report | · 测试报告 | 30 | 30 |
· Size Measurement | · 计算工作量 | 10 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 20 | 20 |
合计 | 1760 | 1610 |
3、接口设计思想
在面向对象课上老师便强调过类设计时的Information Hiding思想——对象调用者不应该能直接访问到类内的具体数据结构,而应该在一个抽象的层面上通过接口使用它。本次在主类设计和前后端接口设计中均体现了这种思想——外界并不关心主类以什么方式存储几何对象和交点,这也使得在后续的优化中可以轻易地更换数据结构做各种尝试。
接口是软件中各部分程序沟通的桥梁。在本次结对编程中最能体现出的接口设计思想是单一职责思想,即一个函数只做一件事,并把它做好。这体现在对各种corner case及各种组合情况的考量,以及异常处理等。在接口设计实现过程中维护详细的文档并严格按文档执行,避免“想到什么就写“增加程序的潜在不一致性风险。
本次项目中我们对松耦合的实践是让数据在前后端间以最原始的string
形式流通,不依赖前/后端定义的数据结构。有利于程序的可移植性。
4、计算模块接口的设计与实现
代码结构
为方便管理,按逻辑结构划分源代码文件,将存放几何对象的数据结构定义在GeoObject.h
中,主类定义在Intersect.h
中,异常类定义在Exception.h
中。工程内分多项目(包含测试模块、DLL接口实现及UI),通过路径设置统一管理源码,避免复制。
类设置
计算模块共设置4个类,分别为表示直线、射线、线段的Line
类(通过成员m_type
区分),表示圆的Circle
类,表示交点的Node
类,以及负责维护几何对象、计算交点及输出结果的主类Intersect
类。几何对象类的成员包含构造参数以及为减少求解交点时的计算量而预存的衍生量。此外,Line
类还包含一个判断输入的交点是否在该线上的online
成员函数。主类在实例化时生成存放几何对象及交点的容器,并在后续过程中不断更新。
主类接口
主类Intersect
在实例化后可以通过addGeoFromFile
函数从目标路径文件中读入几何对象,也可以通过addGeoByString
函数由指定格式的字符串添加几何对象。还可以通过removeGeoByString
函数删除容器中和指定格式的字符串所表示的几何对象相同的几何对象。主类提供CalculateIntersections
函数用于触发交点的计算,这个函数通过调用相应的交点计算函数计算每两个几何体所形成的交点并保存在容器中。在交点计算完毕后可以通过GetIntersectionNumber
函数得到交点个数,或使用GetGeoObjects
和GetIntersections
函数输出几何对象的具体信息,也可通过ViewIntersections
函数打印交点至标准输出以快速调试。
交点计算
交点的计算根据代数或几何方法实现。其中,线间交点的求解依赖行列式,较为工整规范;线圆交点的求解通过将圆心平移至原点,简洁地计算出交点后再平移回去的方法实现(可视为坐标系的线性变换);圆圆的交点则通过相交部分的三角关系求解。具体如下:
LineLineIntersect
对于\(L_1\{(x_1,y_1), (x_2,y_2)\},L_2\{(x_3,y_3),(x_4,y_4)\}\),交点为:
判据为\(\Delta= \left|\begin{array}{} x_1-x_2 & y_1-y_2\\ x_3-x_4 & y_3-y_4 \end{array}\right|\),有:
为减少计算,读入直线时预存\(x_1-x_2,y_1-y_2\)和\(\left|\begin{array}{}x_1 & x_2\\y_1 & y_2\end{array}\right|\)。
LineCircleIntersect
对于\(L\{(x_1,y_1),(x_2,y_2)\},C\{(x_c,y_c),r\}\),交点为:
其中:
判据为\(\Delta=r^2{d_r}^2-D^2\),有:
为减少计算,读入圆时预存\(r^2\)。
CircleCircleIntersect
对于\(C_1\{(x_1,y_1),r_1\},C_2\{(x_2,y_2),r_2\}\),交点为:
其中:
判据为\(\Delta={r_1}^2-a^2\),有:
参考资料:
https://mathworld.wolfram.com/Line-LineIntersection.html
https://mathworld.wolfram.com/Circle-LineIntersection.html
http://paulbourke.net/geometry/circlesphere/
支持新增几何对象
对射线和线段的支持,在不改变求交点算法的前提下实现。在求出交点后由Line
对象使用成员函数online
根据m_type
判断交点是否在该线上,决定是否将其加入交点集m_allIntersections
中。其中:
为节省计算,在构造线段时预存横纵坐标的最值。
特别地,需要额外考虑非直线的线型对象之间的相交。当其判据\(\Delta\)为零时(向量平行)仍可能相交。若数据合法,则只可能相交于端点处,因此予以特判。
计算模块的封装
将计算模块封装为动态连接库后,对外接口有四。一为readFile
函数,用于从文件中输入几何对象;二为addGeometryObject
函数,用于通过字符串输入单个几何对象;三位removeGeometryObject
函数,用于通过字符串删除与之相同的几何对象;四为getResult
函数,用于触发交点的计算并返回一个由包含所有几何对象描述串的列表和由所有交点构成的列表所组成的元组,形如pair<vector<string>, vector<Point>>
。
5、UML图
计算模块各个实体之间的关系如下:
6、 模块性能改进
对计算模块性能的改进主要从交点储存数据结构和一些针对C++程序的优化入手,占据了80%的编码时间。
交点存储数据结构的选择
通过随机生成的数据进行性能测试,占用CPU时间最多的子函数往往是与交点存储所用数据结构相关的函数(如选用unordered_set
时哈希值的计算,选用set
时元素之间的比较)。因此交点存储数据结构的选择对性能的提升十分关键,备选方案及可能的问题如下:
unordered_set
:需要考虑哈希桶的数量(根据几何对象数目确定)和哈希函数的设计。set
:虽然红黑树的更新较哈希表慢,但不存在rehash的问题。vector
+sort
+unique
:重复的交点较多时会超过内存限制,需要随时检测去重。
因此最终的问题在于在给定的数据条件下哪种数据结构更优。经过比较,采用了unordered_set
,并在Intersect
类初始化时使用reserve
函数预设哈希桶数量为\(5000000\)以避免rehash开销。在随机生成的\(2000\)个几何对象的测试中(交点数在1M以上)性能分析图如下:
可见,CPU占用率最高的函数为存放交点的unordered_set
调用的equal_to
函数。这说明哈希值的碰撞较多,可以考虑优化哈希值的计算。查阅资料后选取了较为合适的:
需要注意的是,浮点数的哈希会导致在精度范围内相等的数被哈希至不同哈希桶,导致重复计算。针对这个问题将哈希函数修改为:
参考资料:
https://stackoverflow.com/questions/16792751/hashmap-for-2d3d-coordinates-i-e-vector-of-doubles
https://www.zhihu.com/question/52368555
针对C++程序的优化
通过查阅以下材料,修改了原始程序中可能导致性能下降的代码,改进包含:
- 直接实例化对象而非使用
new
来给指针分配堆空间,减小内存管理开销。 - 使用
emplace
系列函数向容器中插入元素,避免push
的拷贝开销。 - 提前取出重复使用的成员变量避免多次访存。
- 取消继承,删除指针的强制转换,存储时确定几何对象类型。
- 循环控制使用++i;
https://www.cnblogs.com/Leo_wl/p/5724620.html
https://blog.csdn.net/nodeman/article/details/80771789
https://blog.csdn.net/p942005405/article/details/84764104
7、Contracts
It prescribes that software designers should define formal, precise and verifiable interface specifications for software components, which extend the ordinary definition of abstract data types with preconditions, postconditions and invariants. These specifications are referred to as "contracts", in accordance with a conceptual metaphor with the conditions and obligations of business contracts.
Design by Contract要求设计人员设计出正式、准确且可更改的组件接口规程。在本次项目的接口设计过程中,我与同伴充分讨论制定出前后端接口的规程(包括输入输出以及增删过程中对后端的影响),确立了简约精确的原则,并严格按照规程实现接口代码。而Code Contract用以确保函数执行过程的可控性,通过运行时检查等手段随时验证一段代码的前置条件、后置条件以及运行过程中对环境的改变。
规程化的设计有利于保证代码的可控性,在越大型的项目中就越需要这样的合约来保证代码的确定性。但这也会拖慢开发过程、使思维受制。尤其是对invariant的全面验证有时是非常复杂甚至不可行的。
8、 计算模块部分单元测试展示
本次单元测试继承了上次的测试代码,放入OldTest
类中。其包含三个TEST_METHOD
,分别在仅有直线、仅有圆以及线圆混合的情况下针对几何对象之间的相离、相切、相交、共交点等情况各测试\(5\)个样例。其中测试直线的部分如下:
对于新增功能的测试则包含在NewTest
类中,其下辖三个TEST_METHOD
针对射线和线段之间的位置关系及特殊情况(共线交于端点)共测试了\(14\)个样例。其中测试线段的部分如下:
此外还设置了精度和溢出测试。精度损失来源于sqrt
操作;溢出是当坐标接近\(10^5\)时,\(\Delta=r^2{d_r}^2-D^2\)等判据可能超过long long
的表示范围(因此圆相关一切计算改用double
进行)。对于计算模块封装成的接口,以及定义的每一种异常也都进行了比较全面的测试。若均附图略显冗余,详情可移步项目仓库查看。
测试样例完全覆盖了核心计算代码:
9、 计算模块部分异常处理说明
异常类型 | 设计目标 | 样例 |
---|---|---|
CMDFormatException | 命令行输入错误 | ./intersect.exe -p xx |
FileNotFoundException | 输入文件不存在 | ./intersect.exe -i nowhere -o out.txt |
FileFromatException | 文件内容格式错误 | C L 1 1 -C |
CoordinateLimitExceedException | 坐标超限 | 1 C 999999 0 1 |
LineDefinitionException | 两个参数点重合 | 1 L 1 1 1 1 |
CircleDefinitionException | 圆的半径小于等于零 | 1 C 0 0 -1 |
InfinateIntersectionException | 几何对象间存在重叠 | 2 R 0 1 0 2 S 0 3 0 4 |
10、界面模块设计
因为之前没有GUI编程的经验, 在设计界面模块之前,我首先调查了windows下有哪些GUI开发的库可以使用。
1.MFC
2.QT
3.WPF
4.WTL
在调查之后, 据网上评价QT比较好用, 而且是跨平台的, 但是因为安装文件过大, 电脑给windows的分区不足, 最终选择了VS自带的MFC。
MFC有两种设计方式, 一个是dialog app一个是document app, 我选择的是dialog app, 开发一个窗口。
使用visual studio mfc生成dialog app后, 在资源文件里可以找到界面对应的文件, 打开后如下图, 可以添加组件
我主要使用了三种组件
- Button
- Static text
- Edit control
Button组件用来获取用户的点击操作, 每次点击会触发一个onclick函数
void CMFCApplication1Dlg::OnBnClickedButton1()
{
CString geomLine;
edit_geo_obj.GetWindowTextW(geomLine);
CT2CA sa(geomLine);
std::string s(sa);
addGeometryObject(s);
}
函数内可以添加各种操作, 比如在我的界面中, 有以下几个按钮
-
输入文件
-
增加几何对象
-
计算交点(并且绘制)
-
关闭
static text组件用来展示文字
Edit control是一个文本框, 可以用来输入文字, 每个文本框会有一个变量, 用来获取文字。
比如file.GetWindowTextW(geomline)
就是把文件输入框中的文字读入到geomLine这个Ctring中
最后, 点击计算交点后, 会把从计算模块得到的交点绘制出来, 调用draw函数画在GUI的中央, 如下图所示:
11、界面模块与计算模块的对接
由于我们的接口比较少, 在界面模块对接时很流畅。
得到intersectProjectDll.lib和Interface.h后, 我看到有三个接口
__declspec(dllexport) void readFile(string);
__declspec(dllexport) void addGeometryObject(string);
__declspec(dllexport) pair<vector<string>, vector<Point>> getResult();
这三个接口是我们之前商定好的接口, 尽量隐藏内部实现细节, 只暴露三个接口
- readFile从文件中读取集合对象
- addGeometryObject添加几何对象
- getResult用来获取当前的所有交点和几何对象, 用于画图
功能如下图
图中有三个按钮, 输入文件调用readFile
, 添加几何对象调用addGeometryObject
, 计算交点调用getResult
并且绘制
实现以上功能后, 对接工作完成。
12、结对过程
在编码之前,我们讨论了项目整体架构,包括求交算法、模块接口、UI框架等,初步制定一些接口的协议。而后将其总结为设计文档,开始查阅资料,了解技术细节。此后进行了几轮编码,一人编程一人测试并对具体实现提出修改意见。在初版形成后又讨论了一些优化策略,查阅了资料。实现优化版本后进行回归测试和压力测试。
整体上,结对的过程比较高效。和同伴交流畅通,能够及时解决编程语言上的问题。我们通过腾讯会议进行交流,通过其提供的共享屏幕功能进行代码审阅:
13、PP优缺点
PP优点:实时进行代码复审,随时有人负责测试——保证代码正确性;集二人智慧于一体,讨论的过程有助于灵感的产生和快速实现;合作双方互相影响,起到激励作用,有利于发现对方的优点并学习;有利于合作双方增进了解,深化长期合作。缺点:效果因人而异(No silver bullet),磨合时间可能较长;合作中产生意见不一致可能导致分歧。
个人优缺点:我的缺点在于对编程语言的不熟悉以及持续保持专注的能力不足;优点是不错的资料搜索能力,能够从原始文档中学习,且乐于尝试各种方法从中择优。队友的优点是拥有强大的知识面和丰富的编程技巧,并能耐心解答我的问题,但由客观的原因造成比忙。
14.模块互换
我们互换的组是(17373106 柴博,17373059 滕琦)
我们选择的组和我们有相似的接口, 在替换核心模块的时候经历了以下几个步骤:
1.修改dll, lib路径, 成功引入头文件, 能够链接到库
2.修改输入文件, 添加几何对象, 计算交点的接口名称
3.运行
运行结果如下: