项目
内容
课程:北航-2020-春-软件工程
要求:求交点个数
班级:005
GitHub地址
北航网盘地址
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的远程交流,真是太太太太不方便了!而结队编程本身并未达到其应有的效果。
看教科书和其它资料中关于 Information Hiding,Interface Design,Loose Coupling 的章节,说明你们在结对编程中是如何利用这些方法对接口进行设计的。(5')
信息隐藏、接口设计、松耦合都是面向对象设计的重要方法,都是使程序设计时更接近日常认识,在大模块之间关系中不用过于担心细节,只需在模块设计时下功夫。
信息隐藏:
接口设计:
松耦合:
当然,面向接口应当适度使用,也为很多情况下,接口的实现是定死的,比如说,如果线型只有直线、线段、射线三种,都有两个端点属性,就不需要单独创建Ray和Segment两个类了,只需要在Line中添加一个type字段,否则显得更累赘。“为了接口而写接口”的做法是愚蠢的,应该是“为了需求而写接口”。
设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图?说明你的算法的关键(不必列出源代码),以及独到之处。(7')
计算模块实现扩展射线与线段
,添加/删减图形
,计算交点
,进行部分错误处理
的核心功能。
首先为了存储交点,建立了类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:
输入格式错误
L 1 2 3 4
R 0 1 -1 0
缺少数量参数
why not input a N?
图形类型未定义
1
A 25 72 25 23
未定义类型A
line 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();</code></pre>
实现相应的槽函数:
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;思考全面
缺点
监督编程可能会干扰到对方,双方的代码风格及习惯可能不兼容,磨合期不能成功渡过就无法完成项目
轻视软件测试部分
代码书写速度较慢
由于和对方团队(15061025、 17373263 )提前商量好了接口,因此模块的替换较为容易,基本无需更改。
但是由于对方没有计算直线与边界的端点,我们绘制的图像只能按照线段的方式来绘制。如下图所示:
虽然绘制出来还是线段,点都标明的很清楚。但经过与对方小组的讨论,我们发现有QT的第三方库支持直线的绘制,不像我傻傻地去计算直线与边界的交点。
基本上这次模块交换非常便捷,我们时限就定义好了接口。只需要根据对方的定义的改一下名称和习惯,导入对应的库,就能很快的生成:
他们的接口:
我们的接口:
手机扫一扫
移动阅读更方便
你可能感兴趣的文章