项目 | 内容 |
---|---|
课程:北航-2020-春-软件工程 | 博客园班级博客 |
要求:求交点个数 | 结对项目做业 |
班级:005 | Sample |
GitHub地址 | intersect |
北航网盘地址 | SE结队项目 |
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
· Estimate | · 估计这个任务须要多少时间 | 10 | 10 |
Development | 开发 | ||
· Analysis | · 需求分析 (包括学习新技术) | 30 | 180 |
· Design Spec | · 生成设计文档 | 30 | 30 |
· Design Review | · 设计复审 (和同事审核设计文档) | 5 | 0 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 5 | 0 |
· Design | · 具体设计 | 60 | 120 |
· Coding | · 具体编码 | 240 | 400 |
· Code Review | · 代码复审 | 60 | 0 |
· Test | · 测试(自我测试,修改代码,提交修改) | 120 | 240 |
Reporting | 报告 | ||
· Test Report | · 测试报告 | 60 | 120 |
· Size Measurement | · 计算工做量 | 10 | 10 |
· Postmortem & Process Improvement Plan | · 过后总结, 并提出过程改进计划 | 240 | 240 |
合计 | 870 | 1350 |
之因此实际耗时远在估计耗时之上,是由于结队双方没有充分交流,由于互相之间干扰强烈,最后变成了各作各的项目。最后实际上全部部分(包括计算模块和UI模块)都是一我的完成的。我只能说:经过live的远程交流,真是太太太太不方便了!而结队编程自己并未达到其应有的效果。html
看教科书和其它资料中关于 Information Hiding,Interface Design,Loose Coupling 的章节,说明大家在结对编程中是如何利用这些方法对接口进行设计的。(5')node
信息隐藏、接口设计、松耦合都是面向对象设计的重要方法,都是使程序设计时更接近平常认识,在大模块之间关系中不用过于担忧细节,只需在模块设计时下功夫。c++
信息隐藏:git
接口设计:github
松耦合:算法
固然,面向接口应当适度使用,也为不少状况下,接口的实现是定死的,好比说,若是线型只有直线、线段、射线三种,都有两个端点属性,就不须要单首创建Ray和Segment两个类了,只须要在Line中添加一个type字段,不然显得更累赘。“为了接口而写接口”的作法是愚蠢的,应该是“为了需求而写接口”。编程
设计包括代码如何组织,好比会有几个类,几个函数,他们之间关系如何,关键函数是否须要画出流程图?说明你的算法的关键(没必要列出源代码),以及独到之处。(7')数据结构
计算模块实现扩展射线与线段
,添加/删减图形
,计算交点
,进行部分错误处理
的核心功能。app
首先为了存储交点,创建了类Dot
,使用C++STL的set。由于C++的set采用红黑树生成,必须重载<
及=
,实现方法同SE_Work2_交点个数。又由于C++不支持double相等运算,必须本身写equals
方法。框架
#define equals(a, b) (fabs((a) - (b)) < EPS) bool operator<(const Dot &p) const {return !equals(first, p.first) ? first < p.first : second < p.second;} bool operator==(const Dot &p) const {return equals(first, p.first) && equals(first, p.first);}
保存四类不一样的图形,创建了三个类:Diagram
,Line
,Circle
,为了统一接口,咱们的Diagram
是全部图形的统一接口。咱们必须使父类为抽象类(使用virtual函数),Diagram *
才可以动态匹配到子类上。
class Diagram { public: ... virtual ~Diagram() = default; virtual string tostring() = 0; void intersect(Diagram *diagram); };
本次做业扩展了线段及射线两种图形,为了实现射线及线段与其余图形的交点,必须判断交点是在两点之间仍是在射线之上。因此须要给Dot类设定两个方法:
inline bool onray(Dot *s, Dot *t) { return (first - s->first) * (t->first - s->first) >= 0 && (second - s->second) * (t->second - s->second) >= 0; } inline bool onsegment(Dot *s, Dot *t) { return (first - s->first) * (first - t->first) <= 0 && (second - s->second) * (second - t->second) <= 0; }
s和t分别对应射线或线段的起点和终点,经过以上方式能够判断该点是否在该射线或线段上,而在intersect
方法中也只用加一句:
void Line::intersect(Line *l) { try { Dot *d = intersect0(l); if (!has_dot(d) || !l->has_dot(d)) return; add_pair(this, l, d); } catch (exception e) {} }
这是本次设计中最为精彩的地方,能够说核心模块一半的工程量都在这里!
界面模块:支持几何对象的添加、删除。
是的,添加容易,可是删除一个几何对象,难道不是须要从头开始对其他每一个对象从新计算一次吗?若是已经有了上千个几何对象,删除一个对象都须要几分钟的时间!虽然这一需求在界面模块,可是若是我不提供一个高效接口来删除一个几何对象,根本不可能实现这一需求!
最开始,咱们但愿每一个图形和每一个点之间有一个对应关系。也就是创建map
,可是,若是有上万个节点和上千个图形,就意味着有上万个map,而map的每个value都是集合!在空间复杂度上是彻底不能接受的。
后来,咱们想到,其实上节点和图形之间其实是一个巨大的稀疏矩阵。若是节点在图形上意味着对应的位置为1,不然为0。实际上存储这样一个庞大的矩阵有更高效的方式——舞蹈链
舞蹈链是一种双向循环十字链表。在如图所示的样例中:四个图形(1圆2线段3直线4)有五个交点(\(I_1-I_5\)),图形做为舞蹈链的列首,交点做为舞蹈链的行首。交点在图形上则图形节点的列链上和交点的行链上同时出现一个节点。
这种数据结构可以清晰地看到某个节点是哪几个图形相交得来的,同时经过图形,咱们也能够很是便捷地找到对应的节点。同时对于舞蹈链的动态构建和变化也十分灵活。
然而,正如“舞蹈链是一种指针的舞蹈”所说,一旦出现处理不到为的地方,很容易出现空指针或者未定义的现象。虽然舞蹈链对于时间和空间的占用并不大,维护一个舞蹈链的复杂度仍是很高的。
舞蹈链实现过程
Node结构:
因为是十字双向链表,含有指向上下左右四个指针,同时含有diagram
和dot
字段表示该节点对应的图形和交点。
除了Head
之外,其余Node分为三种,图形对应的Node,交点对应的Node,和(图形,交点)这种联系对应的Node。以下所示为三种状况下的构造方法。
class Node { public: Node *up; Node *down; Node *left; Node *right; Diagram *diagram; Dot *dot; Node() : diagram(nullptr), dot(nullptr), left(this), right(this), down(this), up(this) {} Node(Diagram *d) : Node() { diagram = d; } Node(Dot *d) : Node() { dot = d; } Node(Diagram *d1, Dot *d2) : Node() { diagram = d1; dot = d2; } };
(图形,交点)关系的构建:
在求出一个交点后,要分别构建(diagram1,diagram2,dot)对应Node节点,在构建以前,须要判断是否已经有(图形,交点)关系
。分为如下三步:
void add_pair(Diagram *d1, Diagram *d2, Dot *dot) { Node *n = get_node(dot); // 1. 找到交点对应的节点 Node *d = n->right; bool valid1 = true, valid2 = true; while (d != n) { // 2. 对于两个图形是否已经存在该关系 if (d->diagram == d1) valid1 = false; if (d->diagram == d2) valid2 = false; d = d->right; } if (valid1) { // 3. 若是不存在则须要从新构建 Node *p = new Node(d1, dot); n->left_insert(p); get_node(d1)->up_insert(p); } ... }
图形的删除:
在删除一个图形时,经过图形的节点,对其全部的(图形,交点)关系
判断(如1),中间节点对应的交点只有少于两个图形则删除该交点(如2)。
void Node::invalify() { if (dot == nullptr) { // 1. 该节点是Diagram头结点 Node *d = down; Node *dd = d->down; while (d != this) { d->invalify(); d = dd; dd = d->down; } } else { // 2. 该节点是中间结点 if ((right->diagram == nullptr && left->left == right) || (left->diagram == nullptr && right->right == left)) { left->remove(); right->remove(); } } remove(); }
阅读有关 UML 的内容:https://en.wikipedia.org/wiki/Unified_Modeling_Language。(画一个图便可)。(2’)
记录在改进计算模块性能上所花费的时间,描述你改进的思路,并展现一张性能分析图(由VS 2015/2017的性能分析工具自动生成),并展现你程序中消耗最大的函数。(3')
N | 时间(ms) |
---|---|
200 | 16 |
400 | 71 |
600 | 188 |
800 | 334 |
1000 | 604 |
2000 | 3242 |
3000 | 6998 |
4000 | 14559 |
如表所示,本次核心模块几乎是上次功能耗时的两倍。经过性能分析工具获得耗时函数以下:
最耗时的是Line的构造函数:由于在构造内部还进行了边界点的计算,为了跟UI部分进行对接,须要计算直线或射线在(-10000,10000)边界上的点来替代其端点。
Line::Line(int x0, int y0, int x1, int y1, char ty) { // 转换成通常式,并保证互质,同时a要非负,a为0,b要非负 double divider = gcd(gcd(abs(y1 - y0), abs(x0 - x1)), abs(x1 * y0 - x0 * y1)); if (equals(divider, 0)) handle_error("Line::Line\ttwo dots coincide!"); a = (y1 - y0) / divider; b = (x0 - x1) / divider; c = (x1 * y0 - x0 * y1) / divider; if (a < 0 || (a == 0 && b < 0)) { a = -a; b = -b; c = -c; } s = new Dot(x0, y0); t = new Dot(x1, y1); type = ty; // 更新端点值,以便后续做图 if (type == 'L') { if (equals(b, 0)) { s = new Dot(-c / a, -REIGN); t = new Dot(-c / a, REIGN); } else if (equals(a, 0)) { s = new Dot(-REIGN, -c / b); t = new Dot(REIGN, -c / b); } else { set<Dot> dot_stack; if (INREIGN((-c + a * REIGN) / b)) dot_stack.insert(Dot(-REIGN, (-c + a * REIGN) / b)); if (INREIGN((-c + b * REIGN) / a)) dot_stack.insert(Dot((-c + b * REIGN) / a, -REIGN)); if (INREIGN((-c - a * REIGN) / b)) dot_stack.insert(Dot(REIGN, (-c - a * REIGN) / b)); if (INREIGN((-c - b * REIGN) / a)) dot_stack.insert(Dot((-c - b * REIGN) / a, REIGN)); auto it = dot_stack.begin(); s = new Dot((*it).first, (*it).second); it++; t = new Dot((*it).first, (*it).second); } } else if (type == 'R') { if (equals(b, 0)) { t->second = s->second < t->second ? REIGN : -REIGN; } else if (equals(a, 0)) { t->first = s->first < t->first ? REIGN : -REIGN; } else { Dot *dot = new Dot(-REIGN, (-c + a * REIGN) / b); if (dot->onray(s, t)) { t = dot; return; } dot = new Dot((-c + b * REIGN) / a, -REIGN); if (dot->onray(s, t)) { t = dot; return; } dot = new Dot(REIGN, (-c - a * REIGN) / b); if (dot->onray(s, t)) { t = dot; return; } dot = new Dot((-c - b * REIGN) / a, REIGN); if (dot->onray(s, t)) { t = dot; return; } } } }
看 Design by Contract,Code Contract 的内容:
- http://en.wikipedia.org/wiki/Design_by_contract
- http://msdn.microsoft.com/en-us/devlabs/dd491992.aspx
描述这些作法的优缺点,说明你是如何把它们融入结对做业中的。(5')
契约设计采起前置条件,后置条件和对象不变式的形式。实际上这种设计方式起源于合同,该“合同”定义:
优势:
缺点:
关于JML的实现,在面向对象课程中OO_Unit3_JML规格模式已经有所领教,本次做业主要是使用契约设计进行了接口设计。在UI模块只须要计算模块的两个函数及一个map,计算模块自己保证了其实现求交点、添加图形、删减图形的功能正确性。
展现出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路。并将单元测试获得的测试覆盖率截图,发表在博客中。
单元测试的设计主要在对于不一样形状的增添与删减上。在原有基础上增添一个图形,或者删减一个图形一共8种状况分别进行了单元测试。测试的对象为 add_diagram
和sub_diagram
。
如图所示,虽然最后整体覆盖率为88.89%,但测试的样例基本上已经覆盖8种状况,因为时间的缘由,没有进行深刻覆盖。
在博客中详细介绍每种异常的设计目标。每种异常都要选择一个单元测试样例发布在博客中,并指明错误对应的场景。(5')
错误类型 | 输入(其中一种) | 描述 | 输出 |
---|---|---|---|
线型图形的重合 | 2 L 1 2 3 4 R 0 1 -1 0 |
线型图形共线,有无数个交点 | add_diagram repeated lines or collinear lines |
圆的重合 | 2 C 0 0 1 C 0 0 1 |
- | add_diagram repeated circles |
线型图形的输入点重合 | 1 L 25 72 25 72 |
端点重合,不能肯定 | Line::Line two dots coincide! |
文件没法打开 | intersect.exe | 没法读取文件 | cannot open file: <name> |
输入格式错误 | L 1 2 3 4 R 0 1 -1 0 |
缺乏数量参数 | why not input a N? |
图形类型未定义 | 1 A 25 72 25 23 |
未定义类型A | line <i> undefined type! |
(UI)删除未定义图形 | - | 在UI界面内删除某图形,但该图像不存在 | required diagram not found! |
(cmd)不合要求的命令行参数 | intersect.exe in.txt | 在cmd界面没有命令行参数选项 | please type right input! |
在博客中详细介绍界面模块是如何设计的,并写一些必要的代码说明解释实现过程。(5')
我使用了QT进行图像绘制,QT基于C++开发,自己也是一门很复杂的编程软件,光是学习QT的使用方法,就花了整整半天,能够说本次做业量实在是太大了,而且有问题的是:QT的dll文件与VS不兼容!须要在QT中从新封装模块。基于QWidget
组件进行坐标系及图形绘制,UI模块须要支持的功能:
拖拽文件进入界面做为输入
在Widget
类中定义相应函数实现文件拖拽进行输入
///判断是否为有效的文件 virtual bool IsValidDragFile(QDropEvent *e); ///接受目录 /// @note 遍例目录,调用AcceptFile virtual void AcceptFolder(QString folder); ///接受文件 virtual void AcceptFile(QString pathfile);
在AcceptFile
中进行详细的输入定义及错误处理:
void Widget::AcceptFile(QString pathfile) { ifstream file; cout<<"reading " << pathfile.toStdString()<<endl; file.open(pathfile.toStdString()); if(!file) handle_error("cannot open file: "+pathfile.toStdString()); char s; int num, x0, y0, x1, y1; try{ file>>num; } catch(exception()) { handle_error("why not input a N?"); } for (int i = 0; i < num; i++) { if (file >> s) { if (s == 'L' || s == 'R' || s == 'S') { if (file >> x0 >> y0 >> x1 >> y1) add_diagram(s, x0, y0, x1, y1); } else if (s == 'C') { if (file >> x0 >> y0 >> x1) add_diagram(s, x0, y0, x1, 0); } else { handle_error("line " + DoubleToString(i + 1) + " format error"); } } else { handle_error("need more lines"); } } }
在文字框中输入,可使用“添加图像”或“删减图像”
定义槽,并设计UI界面:
private slots: void on_add_diagram_clicked(); void on_sub_diagram_clicked();
实现相应的槽函数:
void Widget::on_add_diagram_clicked() { stringstream streambuf(ui->input->text().toStdString()); char s; int x0, y0, x1, y1; if (streambuf >> s) { if (s == 'L' || s == 'R' || s == 'S') { if (streambuf >> x0 >> y0 >> x1 >> y1) { add_diagram(s, x0, y0, x1, y1); return; } } else if (s == 'C') { if (streambuf >> x0 >> y0 >> x1) { add_diagram(s, x0, y0, x1, 0); return; } } } handle_error("input format error"); }
绘制圆、线型、点,并显示出全部交点的个数
在paintEvent()
函数中实现刷新绘制功能,该函数每帧调用一次,能实现窗口视图的实时刷新:
void Widget::paintEvent(QPaintEvent *event) { ... QPainter painter2(&image); QRectF rec(DisplayPtoObjectP(rect_topl), DisplayPtoObjectP(rect_bottomr)); // cout<<"repaint! "<<circles.size()<<" "<<lines.size()<<endl; for(auto &it:circles) { drawCircle(it.x, it.y, it.r, &painter2); } for (auto &it:lines) { drawLine(it.s->first, it.s->second, it.t->first, it.t->second, &painter2); } for (auto &it:point_map) { drawPoint(it.first.first, it.first.second, &painter2); } ui->textBrowser->clear(); ui->textBrowser->append(QString::number(point_map.size())); ... painter.drawImage(paint_org, image); }
因为绘图坐标系(相对)与QWidget
坐标系(绝对)之间存在转换关系,故必须对绘制的图形和点进行坐标系变换,同时由于点在屏幕上现实太小,必须在点周围画一个小圆来强调,这种圆不会随着图像的缩放而变更:
QPointF Widget::ValuePtoObjectP(QPointF valPoint) { return DisplayPtoObjectP(QPointF(valPoint.rx() * pixel_per_mm + offsetv_x, valPoint.ry() * pixel_per_mm + offsetv_y)); } void Widget::drawLine(double x1, double y1, double x2, double y2, QPainter* painter) { painter->drawLine(ValuePtoObjectP(QPointF(x1, y1)), ValuePtoObjectP(QPointF(x2, y2))); } void Widget::drawCircle(double x, double y,double r, QPainter* painter){ painter->drawEllipse(ValuePtoObjectP(QPointF(x, y)), r * pixel_per_mm, r * pixel_per_mm); } void Widget::drawPoint(double x, double y, QPainter* painter){ painter->drawPoint(ValuePtoObjectP(QPointF(x,y))); painter->drawEllipse(ValuePtoObjectP(QPointF(x, y)), 3, 3); }
标出坐标系及相应刻度,而且能进行缩放,平移
这方面较为复杂,要实现如下函数,在此略:
QPointF scaleIn(QPointF pos_before, QPointF scale_center, double scale_value); QPointF scaleOut(QPointF pos_before, QPointF scale_center, double scale_value); void paintEvent(QPaintEvent *event); void wheelEvent(QWheelEvent *event); void mousePressEvent(QMouseEvent *event); void mouseMoveEvent(QMouseEvent *event);
详细地描述 UI 模块的设计与两个模块的对接,并在博客中截图实现的功能。(4')
计算模块封装成dll文件,其中头文件有以上全局变量和函数。有circles
和lines
两个集合是为了绘制图形,有point_map
是为了绘制交点。调用add_diagram
及sub_diagram
便可进行图像的增长和删除。
void Widget::paintEvent(QPaintEvent *event)
中调用了point_map
,circles
,lines
进行绘图on_add_diagram_clicked()
中调用了add_diagram
加入图形on_sub_diagram_clicked()
中调用了sub_diagram
删去图形注:以上窗口中坐标系可经过缩放及平移,而且咱们能够经过拖拽.txt
文件进行输入。
提供两人在讨论的结对图像资料(好比 Live Share 的截图)。关于如何远程进行结对参见做业最后的注意事项。(1')
如图是使用了腾讯会议的桌面共享功能和QQ交流的截图。
看教科书和其它参考书,网站中关于结对编程的章节。例如:http://www.cnblogs.com/xinz/archive/2011/08/07/2130332.html ,说明结对编程的优势和缺点。同时描述结对的每个人的优势和缺点在哪里(要列出至少三个优势和一个缺点)。(5')
结队编程 | 我 | 结队伙伴 | |
---|---|---|---|
优势 | 1.两我的考虑问题的方式会比一我的更全面;2.有监督效果使得编程不会那么放松,更能集中注意力;3.通过双人复审,有效减小bug数 | 代码熟练;执行力快;擅长学习新知识 | 心细踏实;能很快找到软件bug;思考全面 |
缺点 | 监督编程可能会干扰到对方,双方的代码风格及习惯可能不兼容,磨合期不能成功渡过就没法完成项目 | 轻视软件测试部分 | 代码书写速度较慢 |
因为和对方团队(1506102五、 17373263 )提早商量好了接口,所以模块的替换较为容易,基本无需更改。
可是因为对方没有计算直线与边界的端点,咱们绘制的图像只能按照线段的方式来绘制。以下图所示:
虽然绘制出来仍是线段,点都标明的很清楚。但通过与对方小组的讨论,咱们发现有QT的第三方库支持直线的绘制,不像我傻傻地去计算直线与边界的交点。
基本上此次模块交换很是便捷,咱们时限就定义好了接口。只须要根据对方的定义的改一下名称和习惯,导入对应的库,就能很快的生成:
他们的接口:
咱们的接口: