2020软件工程-结对项目作业

2020软件工程-结对项目作业

1. 项目简介

github链接

项目 | 内容

2. PSP时间花费

PSP2.1 Personal SoftWare Process Stages 预计耗时 实际耗时
Planning 计划 120 60
Estimte 估计这个任务需要多少时间 1800 1700
Development 开发 1100 1000
Analysis 需求分析 0 0
Desing Spaec 生成设计文档 100 40
Design Review 设计复审 100 10
Coding Standard 代码规范 0 0
Design 具体设计 60 40
Coding 具体编码 1100 180
Code Review 代码复审 60 60
Test 测试 180 180
Reporting 报告 100 120
Test Report 测试报告 0 0
Size Measurement 计算工作量 5 0
Postmortem & Process Improvement 事后总结 60 50
- 合计 1800 1700

3. 接口设计

经过查阅资料和学习, 我了解到在接口设计中有常用的一些原则, 本次作业的设计中, 我们用到了这些原则。

  • 单一职责原则

    Single Responsibility Principle: There shuld never be more than one reason for class to change

    单一职责原则要求一个接口, 一个功能, 如果在private 方法和public接口中, 改动过多, 那么容易引起错误。 在结对编程的设计中, 计算模块里我们尽量保证一个函数只做一个事情, 比如Line Circle Point分出三个类, 每个求交点的函数只处理一种类型等。

  • 里氏替换原则

    Liskov Substitution Principle: Functions that use pointers or references to base classed must be able to use objecsts of derived classes without knowing it

    这一法则是多态的体现, 所有父指针的使用点都可以用子指针, 这要求了子类只扩展父类, 但是不可以用在覆写父类函数的时候让函数的应用范围减少。 在最初的版本中, 我们采用了Line Circle继承父类的方式实现, 后来发现这种继承影响性能, 就放弃了继承。

  • 依赖倒置原则

    依赖倒转原则是TDD测试驱动开发的体现, 要求高层模块不依赖特定的底层模块, 而是依赖其抽象, 这要求编程者提前想好接口。

  • 接口隔离原则

    clients should not be forced to depend upon interfacess that they don't use

    保证接口单一, 不要过于臃肿, 在计算模块和UI模块的对接时, 我们尽量细化接口, 让模块的每个按钮对应了刚好一个接口, 同时接口之间的功能达到了隔离。

  • 迪米特法则

    law of Demeter: least knowledge
    loose coupling: Loose coupling means they mostly independent

    这一原则要求两个类之间要低耦合, 达到loose coupling, 一个类不需要知道另一个类是怎么实现的, 更不需要因为一个类内部代码的修改二更改自己的方法。 在我们的结对编程中, 为了实现接口的高效对接, 对于接口进行和抽象和封装

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

类设置

  共设置4个类,分别为表示直线、射线、线段的Line类(通过成员m_type区分),表示圆的Circle类,表示交点的Node类,以及负责维护几何对象、计算交点及输出结果的主类Intersect类。几何对象类的成员包含构造参数以及为求解交点时减少计算量而预存的衍生量。此外,Line类还包含一个判断输入的交点是否在该线上的online成员函数。

主类接口

  主类Intersect在实例化后可以通过addGeoFromFile函数从目标路径文件中读入几何对象,也可以通过addGeoByString函数由指定格式的字符串添加几何对象。还可以通过removeGeoByString函数删除容器中和指定格式的字符串所表示的几何对象相同的几何对象。主类提供CalculateIntersections函数用于触发交点的计算,这个函数通过调用相应的交点计算函数计算每两个几何体所形成的交点并保存在容器中。在交点计算完毕后可以通过GetIntersectionNumber函数得到交点个数,或使用GetGeoObjectsGetIntersections函数输出几何对象的具体信息,也可通过ViewIntersections函数打印交点至标准输出以快速调试。

  主类在实例化时生成存放几何对象及交点的容器,并在后续过程中不断更新。UML图如下:

交点计算

  交点的计算根据代数或几何方法实现。其中,线间交点的求解依赖行列式,较为工整规范;线圆交点的求解通过将圆心平移至原点,简洁地计算出交点后再平移回去的方法实现(可视为坐标系的线性变换);圆圆的交点则通过相交部分的三角关系求解。具体如下:

LineLineIntersect

  对于\(L_1\{(x_1,y_1), (x_2,y_2)\},L_2\{(x_3,y_3),(x_4,y_4)\}\),交点为:

\[x=\frac{ \left|\begin{array}{} \left|\begin{array}{} x_1 & y_1\\ x_2 & y_2 \end{array}\right| & x_1-x_2\\ \left|\begin{array}{} x_3 & y_3\\ x_4 & y_4 \end{array}\right| & x_3-x_4 \end{array}\right| } { \left|\begin{array}{} x_1-x_2 & y_1-y_2\\ x_3-x_4 & y_3-y_4 \end{array}\right| }\\ y=\frac{ \left|\begin{array}{} \left|\begin{array}{} x_1 & y_1\\ x_2 & y_2 \end{array}\right| & y_1-y_2\\ \left|\begin{array}{} x_3 & y_3\\ x_4 & y_4 \end{array}\right| & y_3-y_4 \end{array}\right| } { \left|\begin{array}{} x_1-x_2 & y_1-y_2\\ x_3-x_4 & y_3-y_4 \end{array}\right| } \]

判据为\(\Delta= \left|\begin{array}{} x_1-x_2 & y_1-y_2\\ x_3-x_4 & y_3-y_4 \end{array}\right|\),有:

\[\Delta\begin{cases} =0 & no\space intersection\\ \ne0 & intersection \end{cases} \]

为减少计算,读入直线时预存\(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\}\),交点为:

\[x=\frac{ Dd_y\pm sign(d_y)d_x\sqrt{r^2{d_r}^2-D^2} }{ {d_r}^2 }+x_c\\ y=\frac{ -Dd_x\pm\left|d_y\right|\sqrt{r^2{d_r}^2-D^2} }{ {d_r}^2 }+y_c \]

其中:

\[d_x=x_2-x_2\\ d_y=y_2-y_1\\ d_r=\sqrt{{d_x}^2+{d_y}^2}\\ D=\left|\begin{array}{} x_1-x_c & x_2-x_c\\ y_1-y_c & y_2-y_c \end{array}\right|\\ sign(x)=\begin{cases} -1 & x<0\\ 1 & otherwise \end{cases} \]

判据为\(\Delta=r^2{d_r}^2-D^2\),有:

\[\Delta\begin{cases} <0 & no\space intersection\\ =0 & tangent\\ >0 & intersection \end{cases} \]

为减少计算,读入圆时预存\(r^2\)

CircleCircleIntersect

  对于\(C_1\{(x_1,y_1),r_1\},C_2\{(x_2,y_2),r_2\}\),交点为:

\[x=x_0\pm h(y_2-y_1)/d_c\\ y=y_0\mp h(x_2-x_1)/d_c \]

其中:

\[h=\sqrt{r_1^2-a^2}\\ d_c=\sqrt{(x_1-x_2)^2+(y_1-y_2)^2}\\ a=({r_1}^2 - {r_2}^2+{d_c}^2)/d_c\\ x_0=x_1+a(x_2-x_1)/d_c\\ y_0=y_1+a(y_2-y_1)/d_c \]

判据为\(\Delta={r_1}^2-a^2\),有:

\[\Delta\begin{cases} <0 & no\space intersection\\ =0 & tangent\\ >0 & intersection \end{cases} \]

参考资料:

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中。其中:

\[(x,y)\in Segment\{(x_1,y_1),(x_2,y_2)\}\space iff\space min(x_1,x_2)\le x\le max(x_1,x_2)\and min(y_1,y_2)\le y\le max(y_1,y_2)\\(x,y)\in Ray\{(x1_1,y_1),(x_2,y_2)\}\space iff\space (x_2-x_1,y_2-y_1)·(x-x_1,y-y_2)\ge0 \]

为节省计算,在构造线段时预存横纵坐标的最值。

  特别地,需要额外考虑非直线的线型对象之间的相交。当其判据\(\Delta\)为零时(向量平行)仍可能相交。若数据合法,则只可能相交于端点处,因此予以特判。

计算模块的封装

  将计算模块封装为动态连接库后,对外接口有三。一为readFile函数,用于从文件中输入几何对象;二为addGeometryObject函数,用于通过字符串输入单个几何对象;三为getResult函数,用于触发交点的计算并返回一个由包含所有几何对象描述串的列表和由所有交点构成的列表所组成的元组,形如pair<vector<string>, vector<Point>>

5. UML图

UML图是用来表示类之间设计关系的图表, 主要有以下五种关系

  • 依赖关系
  • 关联关系
  • 聚合关系
  • 组合关系
  • 继承关系

用UML图表示计算模块的关系如下图所示

一共用三个类来存数据, Line, Circle, Node(点), Intersect里面聚合了这三种数据, 进行一系列运算

6. 模块性能改进

记录在改进计算模块性能上所花费的时间,描述你改进的思路,并展示一张性能分析图(由VS 2015/2017的性能分析工具自动生成),并展示你程序中消耗最大的函数。(3')

交点存储数据结构的选择

  通过随机生成的数据进行性能测试,占用CPU时间最多的子函数往往是与交点存储所用数据结构相关的函数,如选用unordered_set时哈希值的计算,选用set时元素之间的比较等。因此交点存储数据结构的选择对性能的提升十分关键,备选方案及可能的问题如下:

  • unordered_set:需要考虑哈希桶的数量(根据几何对象数目确定)和哈希函数的设计。
  • set:虽然红黑树的更新较哈希表慢,但不存在rehash的问题。
  • vector+sort+unique:重复的交点较多时会超过内存限制,需要随时检测去重。

因此最终的问题在于在给定的数据条件下哪种数据结构更优。经过比较,采用了unordered_set,并在Intersect类初始化时使用reserve函数预设哈希桶数量为\(5000000\)以避免rehash开销。在随机生成的\(2000\)个几何对象的测试中(交点数在1M以上)性能分析图如下:

可见,CPU占用率最高的函数为存放交点的unordered_set调用的equal_to函数。这说明哈希值的碰撞较多,可以考虑优化哈希值的计算。查阅资料后选取了较为合适的:

\[hash(Node)=hash(Node.x)\hat{}hash(Node.y)<<1 \]

需要注意的是,浮点数的哈希会导致在精度范围内相等的数被哈希至不同哈希桶,导致重复计算。针对这个问题将哈希函数修改为:

\[hash(Node)=hash(round(Node.x * 1.0 / EPS))\hat{}hash(round(Node.y * 1.0 / EPS)) \]

参考资料:

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. Design By Contract

Design By Contract: 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.

在上学期的OO中, 我了解到和约设计需要定义以下的条件

  • 前置条件 函数的输入应该满足什么条件
  • 不变式 在函数计算过程中 应该满足哪些恒定的条件
  • 后置条件 函数的结束时应满足什么条件

由于对于每个函数用和约设计过于复杂, 并不实用, 因此我们对于模块进行和约设计, 各自保证计算模块和UI模块的正确性, 独立测试。

8. 计算模块部分单元测试展示

展示出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路。并将单元测试得到的测试覆盖率截图,发表在博客中。要求总体覆盖率到 90% 以上,否则单元测试部分视作无效。(6')

  本次单元测试继承了上次的测试代码,放入OldTest类中。其包含三个TEST_METHOD,分别在仅有直线、仅有圆以及线圆混合的情况下针对几何对象之间的相离、相切、相交、共交点等情况各测试\(5\)个样例。其中测试直线的部分如下:

  新增功能的测试则包含在NewTest类中,其下辖三个TEST_METHOD针对射线和线段之间的位置关系及特殊情况(平行交于端点)共测试了\(14\)个样例。其中测试线段的部分如下:

  此外还设置了精度和溢出测试。精度损失来源于sqrt操作;溢出是当坐标接近\(10^5\)时,\(\Delta=r^2{d_r}^2-D^2\)等判据可能超过long long的表示范围,因此圆相关一切计算改用double进行。

  测试样例完全覆盖了核心计算代码:

9. 计算模块部分异常处理说明

在博客中详细介绍每种异常的设计目标。每种异常都要选择一个单元测试样例发布在博客中,并指明错误对应的场景。(5')

异常类型 设计目标 样例
CMDFormatException 命令行输入错误 ./intersect.exe -p xx
FileNotFoundException 输入文件不存在 ./intersect.exe -i nowhere -o out.txt
FileFromatException 文件内容格式错误 C
L 1 1 -1
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) void removeGeometryObject(string);

__declspec(dllexport) pair<vector<string>, vector<Point>> getResult();

这三个接口是我们之前商定好的接口, 尽量隐藏内部实现细节, 只暴露三个接口

  • readFile从文件中读取集合对象
  • addGeometryObject添加几何对象
  • getResult用来获取当前的所有交点和几何对象, 用于画图

功能如下图

图中有三个按钮, 输入文件调用readFile, 添加几何对象调用addGeometryObject, 计算交点调用getResult 并且绘制

实现以上功能后, 对接工作完成。

12. 描述结对的过程

使用工具: 腾讯会议

具体过程:

  • 在结对编程过程中, 我们首先没有区分领航员和驾驶员的角色, 而是整体对于项目进行讨论, 比如用什么样的架构, UI的框架等。
  • 在明确了分工之后, 我们进入了探索期, 查阅文档, 了解技术细节, 并且写设计文档
  • 完成了需求分析和设计后, 我们开始了几轮的编码。 一个人编程, 另外一个人负责测试和提修改意见, 经过几轮迭代后, 我们有了基本可用的版本。
  • 在复审, 测试阶段, 我们轮流提供测试样例, 进行压力测试
  • 最后一个阶段是代码优化, 我们采取的模式是一个人负责找优化方法, 一个人负责实现, 及时保持沟通, 得到性能提升的反馈。

13. 结对编程优缺点

结对编程的优点

  • 两个人可以互相学习, 完善对c++语法, 标准库使用等知识
  • 结对工作可以提升代码的质量
  • 缩短了复审周期, 在公司的code review阶段, 有时因为tech lead事情繁忙, 无法给出fd, 一段code review会经历1-2天才能进入下阶段的持续开发测试, 结对编程可以加速这一流程
  • 有同伴一起编程可以让人更专注

结对编程的缺点:

  • 两个人同时做一件事, 有些浪费时间, 不利于团队的冲刺
  • 往往领航员数量少, 驾驶员多, 成本高

在结对编程的时候, 每一个人的优缺点如下:
对驾驶员的优点

  • 代码可以及时得到反馈, 架构问题可以及时调整
  • 可以找人讨论, 提升工作专注度
  • 快速学习, 能够迅速提升软件质量

对驾驶员的缺点

  • 有时候旁边有人会紧张, 影响工作状态

对领航员的优点

  • 可以了解junior一些的SDE的开发历程
  • 统一项目代码风格
  • 巩固自己代码架构, 算法知识

对领航员的缺点

  • 浪费了太多时间

14.模块互换

我们互换的组是 https://www.cnblogs.com/starmiku/p/12560565.html

我们选择的组和我们有相似的接口, 在替换核心模块的时候经历了一下几个步骤:

1.修改dll, lib路径, 成功引入头文件, 能够链接到库

2.修改输入文件, 添加几何对象, 计算交点的接口名称

3.运行

运行结果如下:

posted @ 2020-03-22 16:49  edward-crazy  阅读(330)  评论(2编辑  收藏  举报