Author: 17373051 郭骏
3.28添加:4.计算模块接口的设计与实现过程部分,PairCore实现的细节
项目 | 内容 |
---|---|
这个作业属于哪个课程 | 2020春季计算机学院软件工程(罗杰 任健) |
这个作业的要求在哪里 | 结对项目作业 |
我在这个课程的目标是 | 学习软件工程的开发知识,培养工程化开发能力 |
这个作业在哪个具体方面帮助我实现目标 | 通过实操掌握结对开发基础 |
1.前言
- 教学班级:005
- 项目地址:https://github.com/abTaoTao/Pair_project_git
给定 N 个几何图形,询问平面中有多少个点在至少 2 个给定的图形上。
在此处先展示Code Quality Analysis的零警告图片。
2.PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 5 | 5 |
· Estimate | · 估计这个任务需要多少时间 | 5 | 5 |
Development | 开发 | 390 | 570 |
· Analysis | · 需求分析 (包括学习新技术) | 60 | 60 |
· Design Spec | · 生成设计文档 | 20 | 20 |
· Design Review | · 设计复审 (和同事审核设计文档) | 5 | 5 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 5 | 5 |
· Design | · 具体设计 | 60 | 120 |
· Coding | · 具体编码 | 120 | 120 |
· Code Review | · 代码复审 | 60 | 60 |
· Test | · 测试(自我测试,修改代码,提交修改) | 60 | 180 |
Reporting | 报告 | 70 | 100 |
· Test Report | · 测试报告 | 30 | 60 |
· Size Measurement | · 计算工作量 | 10 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 30 |
合计 | 465 | 675 |
3.接口设计思想
-
信息隐藏
这条原则指导我们,应当将类的属性私有化,通过访问函数来实现,并且使用接口来连接层与层。我们在设计的过程中,做到了将类的属性全部私有化。接口的实现方面,我们使用类的部分公有函数作为接口,做到了类与类之间的交互。
-
接口设计
我们在设计过程中,通过Point—Line/Circle—Core三层类的结构环环相扣,每两层之间使用有限的公有函数进行交互。Line和Circle有互相使用对方求交点的方法,能够轻松的调用交点求解程序,Line也包含了线段、射线和直线,在外层写程序时不用关心到底是哪两种图形在求交点。
-
松耦合
类与类、层与层之间有着较好的隔离,如果出现问题或者需要增加需求,只需要修改一个类中的代码,而不需要去修改其调用的其他类。如Line和Circle,虽然需要互相求交点,但是修改其中一个类的代码,可以无需改动另外一个类,因为接口是完善的,类之间的耦合度也是低的。
在C++中,我们并没有使用抽象类的特性来帮助我们构建接口,因为抽象类在作为函数参数时的支持并不够方便。在此提到的“接口”,指的是每个类的公有方法。
4.计算模块接口的设计与实现过程
我们的程序由Point类打底,代表解题过程中的点,可以是线段/射线的端点,也可以是图形之间的交点。我们为其预设了比较函数和赋值方法,作为程序的基础。
然后是Line类和Circle类。Line类包含线段、射线和直线,作为基础题部分的解题骨干。Line类的核心方法是Line::getIntersect(Line l)
,可以帮助我们轻松求出两条线之间的交点。Circle类是圆类,用于附加题。该类有对直线和圆求交点的方法。
主类为PairCore。这个类中包含了命令行参数分析函数,输入正则匹配函数,以及求交点输出到文件的函数。有PairCore::parser(int argc, char* argv[])
方法,用于处理命令行参数的输入,解析方式是简单的字符串相等的条件判断。PairCore::text_handle()
方法是用于从文件中读入数据,按行读入,第一行是数字,之后的每一行采用正则表达式的方式进行读取,如果格式与正则表达式不匹配则返回报错。同时我们也需要对输入范围进行特判。PairCore::getIntersectionCount()
是用于计算几何图形交点的函数,其中包含三个循环,分别用于计算线与线、线与圆、圆与圆的交点。
本次作业中的核心函数,也是和上次作业很不一样的地方,就是判断求出来的点是否是两个图形的交点。由于线段、射线的范围有限制,所以我们需要判断点是否在线上。我们有函数Line::isOnline(Point p)
来判断点是否在线上。判断的方法是与线的两个端点进行比较。
同时,我们需要判断输入的线是否重合,即便在同一条直线上,也有可能是没有交点或者有一个交点。我们有函数Line::relation(Line l)
来判断两条线的关系。如果两条线在同一直线上,则我们会对两条线的端点坐标进行判断,从而能够判断出他们真实的交点个数。
算法的独到之处在于,早早为Point类写好了比较函数,将此后的许多位置关系判断转化为端点坐标的位置判断,简化了问题。
5.UML图
UML类图如下所示。
6.性能改进
我们使用VS的性能分析器对2000条线、500个圆的情况进行了分析,得到的图如下所示:
可以看到,在Line::getIntersect
这个方法上耗费的CPU达到32%,让我们感到不可思议。我们进入代码之后,查看了语句的CPU使用率。
可以看到,语句大部分时间花在了对vector的创建、修改和返回上。由于两条线之间的交点只能是1个或者0个,所以我们可以不用vector<Point>作为返回值,而是用Point作为返回值,并判断该Point是否存在。修改之后的占比如图所示。
我们在优化上大概耗费了20分钟。
7.契约式设计
契约式设计要求设计者对软件设计正式、准确、可验证的接口规范,定义了先验条件、后验条件和不变式,这些规范称为“契约”。
这种方法的优点:
- 通过规范化的注释,能够直接验证程序正确性。
- 设计时着重功能而非具体实现,不用担心具体的实现流程。
- 明确接口的功能之后,设计者和开发者都能够得到足够的信息。
这种方法的缺点:
- 程序的正确性验证有时代价会非常大,甚至可能无法检错。
- 正确而规范的设计十分困难,会出现满足设计而不满足功能的情况。
- 存在一些难以或无法用契约设计的功能。
在结对作业中,这种思想确实可以让我们写出正确性较为完备的程序,但是却难以帮助我们对程序进行优化。同时,在从设计到实现的过程中,我们常常需要绞尽脑汁,甚至不得已去修改设计。因而,在结对的过程中,我们没有过分依赖契约式设计,只是做到了一般的设计—开发的流程。
8.单元测试展示
部分单元测试代码展示:
单元测试通过截图:
单元测试覆盖率截图:
我们构造测试数据的思路是:从一般到特殊,从简单到复杂,从白箱到黑箱。
我们的测试代码有题目的样例代码、最简单情况的代码、特殊情况(重合、平行、端点相交、射线相背等)的样例设计,以及一些能够触发异常的样例。此外,我们还使用随机数据生成器生成了一点随机且复杂的情况加入测试,用于检测我们的考虑是否完备。
9.异常处理说明
我们设计了13种异常,每种异常都有其对应的错误码。错误码和异常的对应关系在程序中有所体现,代码如下:
错误代码 | 错误信息 | 测试样例 |
---|---|---|
-1 | 线段之间存在重合 | 2 S 0 0 2 2 S 1 1 3 3 |
-2 | 线段和射线之间存在重合 | 2 S 0 0 2 2 R 1 1 3 3 |
-3 | 射线之间存在重合 | 2 S 0 0 2 2 S 1 1 3 3 |
-4 | 直线与某条线重合 | 2 L 0 0 2 2 S -1 -1 -2 -2 |
-5 | 圆与圆重合 | 2 C 0 0 2 C 0 0 2 |
-6 | 输入的两个交点重复 | 1 L 1 1 1 1 |
-7 | 坐标超出(-100000,100000)的范围 | 1 L 100000 1 1 1 |
-8 | 输入圆的半径不大于0 | 1 C 0 0 -2 |
-9 | 分析图形信息时出错 | 1 asdfg |
-10 | 分析图形个数时出错 | L 1 1 2 2 |
-11 | 几何图形个数与输入的数目不匹配 | 2 L 1 1 2 2 |
-12 | 多余的不在末尾的换行符 | 2 L 2 2 3 3 L 0 5 5 0 |
-13 | 命令行参数错误 | intersect.exe -p point.txt |
如果在命令行模式下发生异常,则程序会将错误信息输出到同目录下的error.txt中。
10.界面模块设计
界面模块采用C#的Winform来开发。由于这是我们第一次使用C#,也是第一次开发GUI程序,在设计上难免会存在一些不足。
界面设计如图所示,我们的界面模块有两个窗口。
第一个窗口(Form1)是输入输出图形信息的窗口。如果GUI使用不当,会有相应的错误提示。右边的列表是现有的图形信息,可以通过鼠标点选来进行删除。
从文件导入之后不会直接开始绘制,而是将文件中的点信息加入到现有的点列表中,提供了相对高的灵活性和可操作性。
我们确实支持图形的绘制,也支持图形的添加和删除,但是这两个过程在我们的界面中是分离的,即没有做到像GeoGebra一样能够实时绘制图形和交点。
第二个窗口(Form2)是我们的绘制结果窗口。该窗口使用Winform的Panel进行绘制,整个绘制过程为造*过程,即没有成熟的坐标系模块供我们使用,所以效果相对简陋,且不支持缩放和平移。不过由于结对编程只有短短的两周,且中间经历了很多别的作业,所以没有在此处过于苛求。程序默认将所有的图形都画进窗口中,所以如果图形的横纵轴跨度较大,则观感会欠佳。
*中的关键代码是如何掌握好窗口的边界位置。我们相当于要将所有图形的坐标范围(minx,miny)-(maxx,maxy)映射到我们的画板(0,0)-(720,720)上。映射过程的核心代码如下:
float k, dx, dy;
if (maxx - minx < 1 && maxy - miny < 1)
{
k = 1;
dx = minx;
dy = miny;
}
else
{
if ((maxx - minx) > (maxy - miny))
{
k = maxx - minx;
dx = minx;
dy = miny - (maxx - maxy - minx + miny) / 2;
}
else
{
k = maxy - miny;
dx = minx - (-maxx + maxy + minx - miny) / 2;
dy = miny;
}
}
这里的k是缩放比例,dx和dy是x轴和y轴的平移距离。对于每个点的坐标,我们只需要进行如下变换:x = (x0 - dx) * 720 / k, y = (y0 - dy) * 720 / k
即可。
11.模块对接
dll为前端留下了两个接口,分别用于GUI展示和命令行测试。
/*GUI展示
dll从同目录下的lines.pair按格式读入几何图形的信息,
求解后将得到的点的坐标存入同目录下的points.pair文件。
函数的返回值>=0,代表交点个数。<0则代表出现异常,返回异常代码。
*/
int solve();
/*命令行展示
dll从同目录下的commands.pair按格式读入命令行参数,按要求处理。
函数的返回值>=0,代表交点个数。<0则代表出现异常,返回异常代码,不写文件。
报错部分的代码由前端完成,将错误信息存在同目录下error.txt中。
*/
int command();
可以看到这两个接口的交互几乎全部依赖于文件和返回值,没有进行参数传递。
主要的原因在于,C#和C++的标准有诸多不同,比如C#不支持指针,char为两个字节长度,string类和C++的string类不能公用等。我们尝试无论怎么传参数,都无法达到我们想要的效果,只能传一些简单的数字。所以我们干脆制定了文件读入的标准。
如果我们使用C++的GUI开发,可能不会出现这样的问题,这是我们需要反思的一点。
12.结对过程
结对过程主要通过QQ语音+Live Share的方式完成。由于网络原因,常常出现Live Share断线的情况,极大的影响了我们的工作效率。不过在我们的不懈努力之下,最终还是完成了本次作业。证据如下所示:
13.结对编程
- 优点
- 编程过程互相监督,不会存在摸鱼的情况,随时思考随时交流,思维更加集中
- 两人共审同一份代码,对代码的质量和正确性都有更高的保证。
- 两人一起工作,集思广益,对于困难的问题可能会更快产出解答。
- 缺点
- 度过磨合期很困难,要互相习惯对方的编程速度、思维、风格。
- 对于“驾驶员”来说,写代码的“领航员”过度插手可能会带来反效果。
结对人员 | 我 | 队友 |
---|---|---|
优点 | 思维较快,思考更迅速连贯 | 思考全面,单元测试完善 |
缺点 | 难以习惯队友的代码风格、编程习惯等,磨合期较难受 | 节奏难以同步 |
14.附加题:支持松耦合
我们选择交换dll的团队是(17373052, 17373053)团队。
他们的dll设计与我们大致相同,除了文件的命名方式以外,其他大致相同。
合并时遇到的问题有:
- 他们并非由GUI和command两个函数构成,而是只有一个函数
run()
,由命令行参数进行区分。无论是否需要绘制,都会将点的信息存在points.txt中。我们修改了我们的读写方式,以适配他们的核心模块。 - 他们的异常处理返回信息和我们不一样。他们的
run()
函数只定义了两种异常:输入异常和重合异常。我们为此修改了异常的返回信息。
经测试,命令行和GUI都可以正常使用。