项目
内容
课程:北航2020春软件工程
作业:阅读并撰写博客回答问题
我在这个课程的目标是
积累两人结对编程过程中的经验
这个作业在哪个具体方面帮助我实现目标
体验两人结对编程过程,努力提高沟通效率和代码匹配度
教学班级:006
目录结构:
IntersectProject/
├── bin
│ ├── intersect.exe
│ └── dllProject.dll
├── README.md
├── src
│ ├── main.cpp
│ └── core
│ │ ├── container.h
│ │ ├── dot.h
│ │ ├── exception.h
│ │ ├── framework.h
│ │ ├── graph.h
│ │ ├── IOHandler.h
│ │ ├── line.h
│ │ ├── pch.h
│ │ ├── radial.h
│ │ ├── segment.h
│ │ ├── container.cpp
│ │ ├── dllmain.cpp
│ │ ├── dot.cpp
│ │ ├── IOHandler.cpp
│ │ ├── line.cpp
│ │ ├── pch.cpp
│ │ ├── radial.cpp
│ │ └── segment.cpp
└── test
├── pch.h
├── pch.cpp
├── UnitTestIntersect.cpp
├── UnitTestInterface.cpp
└── UnitTestException.cpp
预估耗时
PSP2.1
Personal Software Process Stages
预估耗时(分钟)
实际耗时(分钟)
Planning
计划
· Estimate
· 估计这个任务需要多少时间
10
Development
开发
· Analysis
· 需求分析 (包括学习新技术)
500
· Design Spec
· 生成设计文档
15
· Design Review
· 设计复审 (和同事审核设计文档)
10
· Coding Standard
· 代码规范 (为目前的开发制定合适的规范)
5
· Design
· 具体设计
15
· Coding
· 具体编码
140
· Code Review
· 代码复审
15
· Test
· 测试(自我测试,修改代码,提交修改)
120
Reporting
报告
· Test Report
· 测试报告
30
· Size Measurement
· 计算工作量
5
· Postmortem & Process Improvement Plan
· 事后总结, 并提出过程改进计划
5
合计
920
改进了计算几何对象交点的程序,将其封装到Container内,其条件判断、求解方法不公开。
设计了IOHandler模块,作为输入输出的总控制器。将实现细节封装进模块中,比如文件指针、文件路径、具体读写文件的方法和读写时的异常情况。
Container只暴露Size()
,Getgraphs()
,GetDots()
,AddGraph()
和DeleteGraph()
五个接口。分别用于取得交点个数、取得几何图形、取得交点、添加几何图形和删除几何图形。足以涵盖包括UI在内所有要使用信息。
IOHandler类只暴露四个接口readNum()
,readLine()
,readGraphType()
和outputInt()
,分别用于读入一个范围在\((-100000,100000)\)的整数、读入几何对象数量,读入一个表示几何对象类型的字符和输出一个整数。四个接口涵盖了IO方面所有的功能。
模块和模块之间必然存在着必要的信息交流,比如计算交点的Container类必须知道直线的表示方法是\(ax+by+c=0\),所以我们也只透露这点信息给它,它也只能使用一定的接口获得\(a\),\(b\),\(c\)三个参数。只要能够保证外界其他类能够获得直线\(ax+by+c=0\)的表达式,我们可以任意修改直线类里面的任何实现。IOHandler模块也是,只暴露4个必要的接口。
但是对于异常,我是实现将所有的异常都给出,并没有将其封装成一个“异常”类。如果某个异常的含义发生了变化,那么对应的,需要修改所有使用到该异常的地方。这确实是软件在仓促设计时候的一个小不足。
根据题目所给定的信息,直线、射线和线段均由线上两点表示。
在求交点时,为方便计算,所有几何形状统一表示成直线,在求出交点后再判断是否满足射线、线段的限定条件。
为方便表示所有类型的直线,同时便于判断平行和求交点,采用直线的一般式方程来描述:
\[Ax+By+C=0
\]
这里,\(A,\ B,\ C\) 三个量的计算方法为:
在这种情况下,两直线平行时:
\[A_1B_2=A_2B_1
\]
在用直线的一般式方程来描述时,两直线交点坐标:
\[(\frac{B_1C_2-B_2C_1}{A_1B_2-A_2B_1},\ \frac{A_2C_1-A_1C_2}{A_1B_2-A_2B_1})
\]
如果两形状中存在射线,则判断射线的端点与交点的位置关系,交点是否在端点沿射线方向的一侧。
如果两形状中存在线段,则判断线段的两端点与交点的位置关系,交点是否在两端点之间。
目前没有想到好的判断方法,所使用的就是暴力枚举,每加入一个新几何形状,将其与集合中每一个元素求交点。
因为是暴力枚举,复杂度仅能达到 \(O(n^2)\)。
交点类通过继承 pair<double, double>
实现,操作比较简单。
为了判断重复交点,需要重写 equals
函数。
class Dot : public pair<double, double>;
其中考虑到浮点数的精度问题,定义了一个宏函数 DoubleEquals
。
#define DoubleEquals(a, b) (fabs((a) - (b)) < 1e-10)
所有几何形状的共同父类,由于统一用直线的一般式表示,定义获取三个参数的虚函数。
class Graph {
public:
virtual double GetA() = 0;
virtual double GetB() = 0;
virtual double GetC() = 0;
};
通过继承 Graph
实现。
class Line : public Graph {
private:
double A, B, C;
};
通过继承 Line
实现。
class Radial : public Line {
private:
Dot* end_point;
Dot* cross_point;
};
通过继承 Line
实现。
class Segment : public Line {
private:
Dot* end_point1;
Dot* end_point2;
};
用于存储图形,存储并除去重合的交点。
class Container {
private:
vector<Graph*>* graphs;
set<Dot>* dots;
};
两直线平行直接使用公式 \(A_1B_2=A_2B_1\) 判断。
double denominator = A1 * B2 - A2 * B1;
if (!DoubleEquals(denominator, 0)) {
……
}
交点计算同样套用公式 \((\frac{B_1C_2-B_2C_1}{A_1B_2-A_2B_1},\ \frac{A_2C_1-A_1C_2}{A_1B_2-A_2B_1})\)。
double x = (B1 * C2 - B2 * C1) / denominator;
double y = (A2 * C1 - A1 * C2) / denominator;
Dot* intersect = new Dot(x, y);
两几何对象平行而且共线的时候,也有可能存在有限的交点(即恰有一个交点)或存在异常:
else if (平行而且共线) {
if (都是直线) {
throw 无穷交点异常();
} else if (都是射线) {
if (垂直于x轴){
if (相离) {;
} else if (恰有一个交点){
计算交点();
} else {
throw 无穷交点异常();
}
} else if(不垂直于x轴){...}
} else if(g1是射线&&g2是线段){...}
else if(g1是线段&&g2是射线){...}
else if(g1是线段&&g2是线段){...}
}
即出现如下情况
交点是否在射线上,其实就是交点是否在端点沿射线方向的一侧。
通过判断交点与端点组成的向量是否与射线射线上一点与端点组成的向量同向,即可得到答案。
return IsSameSymbol((cross_point->GetX() - end_point->GetX()), (intersect->GetX() - end_point->GetX()))
&& IsSameSymbol((cross_point->GetY() - end_point->GetY()), (intersect->GetY() - end_point->GetY()));
交点是否在线段上就非常简单了,暴力判断交点的横纵坐标是否在线段两端点的横纵坐标范围内。
return intersect->GetX() >= min(end_point1->GetX(), end_point2->GetX())
&& intersect->GetX() <= max(end_point1->GetX(), end_point2->GetX())
&& intersect->GetY() >= min(end_point1->GetY(), end_point2->GetY())
&& intersect->GetY() <= max(end_point1->GetY(), end_point2->GetY());
封装后只暴露 Container
类,设计以下接口:
接口
功能
输入
输出
AddGraph
添加一个几何图形
图形类型,两点坐标
IntersectCalculate
交点计算
两集合图形
交点
Size
获取交点个数
交点个数
Getgraphs
获取图形集合
图形集合
GetDots
获取交点集合
交点集合
DeleteGraph
删除一个几何图形
图形类型,两点坐标
删除的图形
由于计算的核心模块并非本人所写,故引用合作伙伴张xz同学的素材
Dot
继承了 pair<double, double>
Line
继承了 Graph
Radial
和 Segment
继承了 Line
Radial
和 Segment
关联了Dot
Container
关联了 Graph
和 Dot
对一组10000个随机几何对象共约250000个交点的运行结果如下
其中,我们关注到,耗时最长的单体方法是 AddGraph
,但是考虑到其中很大部分是因为调用了 IntersectCalculate
方法,计算两个几何对象之间的交点,所以分析该方法。
这个函数有三个地方耗时可能比较多:
可能的解决办法为使用 <unorder_map>
来存储交点,将时间复杂度将为 O(1)
。
这种方法要求软件设计者为软件组件定义正式的,精确的并且可验证的接口。分为前置条件、后置条件和类不变项三大部分。
在我写的那部分代码中,对于后置条件有一定的实现。每个方法都考虑到部分输入可能的情况,在遇到问题时,它能够抛出指定的异常,比如文件不存在或者两个几何对象有无穷个交点。但是对于这方面的问题,双方事先并没有进行沟通,没有对每一个模块都设定规范的输入和输出。同时因为考虑到本次作业的数据流较为单一,每个函数的输入输出都相对种类单一,时间紧迫,讨论后没有没有采用契约式设计。
但是对于每个暴露出来的接口,我都写了接口文档,说明了每个模块的作用、参数、返回值和期望的异常,也许在一个侧面反映了契约式设计的一些特点。
//IOHandler
/**
* read an integer, ensure in range(-100000, 100000)
* params: None
* returns: aa integer in range(-100000,100000) from ifstream
* exceptions: not_integer_exception - if is not an integer(decimal, char, ect)
* over_range_exception - if integer is not in range (-100000, 100000)
**/
int readNum();
/**
* read an integer, ensure in range[1, inf), which means how many graphs in the map
* params: None
* returns: an integer in range[1,inf) from ifstream
* exceptions: not_integer_exception - if is not an integer(decimal, char, ect)
* not_valid_integer_exception - if not in range[1, inf)
**/
int readLine();
/**
* read a char which means graph type, which must in [L, R, S, C]
* params: None
* returns: a char in [L, R, S, C]
* exceptions: undefined_graph_exception - if is not a valid type of graph
**/
char readGraphType();
/**
* print an integer into ofstream
* params: const int n
* returns:
* exceptions:
**/
void outputInt(const int n);
此处我们也学习到了很多细节知识,比如在封装 dll 时使用的 fstream
和vector
需要写成指针,以防止不同平台上模板不同导致的问题。
测试 \(Dot\) 类的 5 个函数
Dot(double, double)
Dot(const Dot&)
double GetX()
,double GetY()
bool equals(Dot b)
其中测试重点在相等判断上,需要修正 \(double\) 的精度误差,例如
测试图形类的函数
这方面比较简单,无需赘述。
测试交点的平行、计算与位置关系,包括
一般情况
平行
segment = new Segment(Dot(1, 1), Dot(1, -1));
line = new Line(Dot(2, 2), Dot(2, 0));
radial = new Radial(Dot(3, 3), Dot(3, 4));
交点与交点重合
segment = new Segment(Dot(-1, 3), Dot(2, -1));
line = new Line(Dot(-2, 2), Dot(3, 0));
radial = new Radial(Dot(-3, 0), Dot(4, 2));
端点与端点重合
segment = new Segment(Dot(0, 2), Dot(3, -1));
line = new Line(Dot(4, 2), Dot(5, 0));
radial = new Radial(Dot(0, 2), Dot(3, -1));
端点与交点重合
segment = new Segment(Dot(0, 2), Dot(3, -1));
line = new Line(Dot(0, 2), Dot(2, 2));
radial = new Radial(Dot(0, 2), Dot(3, -1));
回归测试,同时测试模块接口
container->AddGraph('S', -1, 3, 2, -1);
container->AddGraph('L', -2, 2, 3, 0);
container->AddGraph('R', -3, 0, 4, 2);
Assert::AreEqual(container->Size(), 1);
container = new Container();
container->AddGraph('S', 1, 1, 1, -1);
container->AddGraph('L', 2, 2, 2, 0);
container->AddGraph('R', 3, 3, 3, 4);
Assert::AreEqual(container->Size(), 0);
……
为了防止输入小数或者异常字符的情况
WRITE("wordsssss");// 将wordsssss写入文件
auto func0 = [&] {io->readNum(); };// 尝试读取文件
Assert::ExpectException<not_integer_exception>(func0);// 捕获该异常
WRITE("0.136");// 将0.136写入文件
auto func2 = [&] {io->readNum(); };// 尝试读取文件
Assert::ExpectException<not_integer_exception>(func2);// 捕获该异常
wordsssss不为整数,0.135也不是整数,故抛出not_integer_exception
异常。
为了防止输入几何对象的数目时,自然数\(N<1的情况\)
WRITE("0");// 将0写入文件
auto func0 = [&] {io->readLine(); };// 尝试读取文件
Assert::ExpectException<not_valid_integer_exception>(func0);// 捕获该异常
输入的整数0代表有0个几何对象,不合法,故抛出not_valid_integer_exception
异常。
为了防止输入的整数超过\((-100000,1000001)\)的范围
WRITE("-100000");// 将-100000写入文件
auto func = [&] {io->readNum(); };// 尝试读取文件
Assert::ExpectException<over_range_exception>(func);
输入的-100000超出了\((-100000,100000)\)的坐标限制,故抛出over_range_exception
异常。
为了防止描述几何对象时,第一位的字符不为L、R、S、C中的一个的情况
WRITE("l");// 将l写入文件
auto func2 = [&] {io->readGraphType(); };// 尝试读取文件
Assert::ExpectException<undefined_graph_exception>(func2);
输入的小写字母l不能够代表任何一种几何对象,故抛出undefined_graph_exception
异常。
为了防止描述几何对象时,点
Dot d0(4.123, 0.1341);
Dot d1(3.123 + 0.9 + 0.1, -8.1341 + 8.2682);
auto func0 = [&] {Graph* g = new Line(d0, d1); };
Assert::ExpectException<dot_superposition_exception>(func0);
新建一条直线时,发现直线经过两个点\((4.123, 0.1341)\)和点\((4.123,01241)\)是同一个点,他们不能够描述一条特定的直线,故抛出dot_superposition_exception
异常。
为了防止输入的任意两个几何对象之间,存在无数的交点(重合)
Dot d0(0, 0);
Dot d1(1, 1);
Dot d2(2, 2);
Dot d3(3, 3);
auto func = [&] {
Container* c = new Container();
Graph* s = new Segment(d0, d2);
Graph* r = new Radial(d1, d3);
c->IntersectCalculate(s, r);
};
Assert::ExpectException<infinate_intersect_exception>(func);
经过\((0,0)\)和\((2,2)\)的线段,与起点为\((1,1)\)通过\((3,3)\)的射线,在\(x\in (1,2)\)的范围内有无数个交点,故抛出infinate_intersect_exception
异常。
为了防止命令行输入的输入文件不存在的情况
auto func = [&] {IOHandler* io = new IOHandler(0, NULL, 1); };
Assert::ExpectException<file_not_exist_exception>(func);
手动将输入文件删除,IOHandler找不到该文件,故抛出file_not_exist_exception
异常。
为了防止图形界面输入的删除对象不存在的情况
auto func0 = [&] {container->DeleteGraph('R', -1, 3, 2, -1); };
Assert::ExpectException<graph_not_exist>(func0);
Containter
中不存在 'R', -1, 3, 2, -1
,故抛出 graph_not_exist
异常.
首先发现一个问题:
可执行文件路径/gui/UI.exe
在 Qt 的 Widgets
中,我们使用两个 QLineEdit
模块作为输入要添加和删除的几何对象的属性、使用三个QPushButton
分别用来打开文件、删除几何对象和添加几何对象。其具体的工作流程如下:
点击 打开文件
按钮
QString fileName = QFileDialog::getOpenFileName(this, QString("choose a file"), ".");
if (fileName.isEmpty())
return;
io = new IOHandler(fileName.toStdString(),"output.txt");
点击 打开文件
按钮时,该代码块在 slot
(自带的回调机制)内被调用,使用 QFileDialog::getOpenFileName
打开一个熟悉的文件选择目录框,然后使用封装好的 IOHandler
模块读取该文件和创建默认的输出文件 output.txt。
在输入框输入 L 0 0 1 1
,然后点击 添加几何对象
stringstream buf(ui->addText->text().append('\n').toStdString());
...
char type = readGraphType(&buf);
int x1 = readNum(&buf);
int y1 = readNum(&buf);
int x2 = readNum(&buf);
int y2 = readNum(&buf);
container->AddGraph(type,x1,y1,x2,y2);
updateList();
updateGraph();
updateIntersect();
点击 添加几何对象
按钮时,该代码块被调用,其作用为将 addText
文本框中的内容(即 L 0 0 1 1
)读取,末尾添加一个 \n
以强调结束,并转化为一个 stringstream
流。使用类似于文件读取的方法,用 >>
操作将 buf
中的值写入相应的变量中,如果有异常就抛出异常(readGraphType()
和readNum()
负责)。然后直接调用接口 AddGraph
添加图形。
添加完成后,调用三个 update*
函数,从 Container
中取出当前已有几何图形列表、所有图形和所有交点,然后在三个 update*
中,绘制列表框、图形曲线和交点数目。
在输入框内输入 L 0 0 1 1
,然后点击“删除几何对象”
同理“添加几何对象”,只是将 AddGraph()
换成了 DeleteGraph()
接口。
绘制列表框
QStringList list;
for(Graph *g: container-> GetGraphs()){
list<<QString::fromStdString(g->ToString());
}
QStringListModel *model = new QStringListModel(list);
ui->list->setModel(model);
更新列表框方法很简单,从 Container
中取出所有的几何对象,将其 toString()
为 S 10 2 5 8
这样的格式,然后压入一个 QStringList
对象,将其转化为 QStringListModel
后展示。
绘制几何曲线
QPainter painter(&image);
// 画框
painter.setRenderHint(QPainter::Antialiasing, true);
painter.drawLine(0, SIZE/2,SIZE+10,SIZE/2);//绘制x轴
painter.drawLine(SIZE/2, 0,SIZE/2,SIZE+10);//绘制y轴
// 画图
for (Graph* g : container->GetGraphs()) {
if (g->type=='L') {
绘制直线();
} else if (g->type=='R') {
绘制射线();
} else if (g->type=='S') {
绘制直线();
}
//画点
for (pair<double,double> d:dots=container->GetDots()){
drawDot(&painter, d.first,d.second);
}
update();
在绘制射线时,使用了一种类似跳棋的方法绘制射线:设端点为 A,射线通过点为 B,那么以 B 为台阶,让 A 跳到 C 点(即 B 为 AC 的中点);令 B=C,再让 A 跳,直到超过图像边界。然后再以 A 和 B 两点利用 QPainter::drawLine
绘制线段来表示射线。
由于 VS 的 dll 文件无法与 Qt 对接,需要在 Qt 新建一个 C++ Library 项目,加入所有的 .h 头文件和 .cpp 源文件(再加上 Qt 自带的两个用于支持 dll 的 .h 头文件)进行编译。
将编译得到的 .lib 和 .dll 文件,复制到 Qt 主项目目录下,并在 Qt 主目录点击右键“添加库”,选择外部库->选择 .lib 文件-> windows ->动态,其自动在 .pro(项目文件)下添加以下几行
# 假设.lib文件命名为dllQtProject.lib
win32: LIBS += -L$$PWD/./ -ldllQtProject
INCLUDEPATH += $$PWD/.
DEPENDPATH += $$PWD/.
再导入所有的 .h 头文件,即可再界面模块使用计算模块。
在界面模块中,使用到了计算模块提供的如下几个接口:
int Container::Size();
vector<Graph*>* Container::GetGraphs();
set<pair<double,double>>* Container::GetDots();
void Container::AddGraph(char type, int x1, int y1, int x2, int y2);
Graph* Container::DeleteGraph(char type, int x1, int y1, int x2, int y2);
int IOHandler::readNum();
int IOHandler::readLine();
char IOHandler::readGraphType();
因为我们两组没有提前商量好统一的接口,导致我们无法在无任何修改的情况下使用对方的 core 模块。
在核心模块部分,我们使用的是自定义的类,而他们使用的是基本数据类型的数据流,其他行为基本一致。所以我们编写了一个接口文件对接双方的图形类的行为,在进行少量的改动之后,我们的 DLL 便可以配合他们的界面模块一起使用了。
通过这次松耦合测试,我认为的有关耦合的问题主要还是在设计的不一致性上,虽然需求明确,但由于忘记了预先商量接口,导致仍然需要一定的预处理。此外,在观摩对方项目的代码后,我认为从外部库的角度来说对方项目组的接口设计更加合理,更符合松耦合的概念,是我们需要学习的地方。
我们的 DLL 和他们的界面模块交换测试运行结果如下:
在结对过程中,使用了一段时间的屏幕共享,但是由于两人微小地域时差和作息不一致,并没有全程使用。
还使用了github上的issue和pull request来管理仓库
自我感觉,在编写核心模块中,自己更像一个领航员。在这次的作业中,核心功能的设计和编码并不是我完成的,自己只是写了IO部分、异常部分和修复了核心功能的一些bug;但是在项目规范的设计和博客的分工方面,一直都是我用了“断言”的方式来制定规范的,所以感觉自己更像一个领航员。
在编写UI模块中,自己是一个实践者。从零学习、搭建Qt环境、设计UI、接入核心计算模块,都是我一个人全权负责,伙伴则修改模块接口的不足和缺陷。
但是我也反思,自己这两个星期以来,对于任务进度的把控并不是很好,没有明确任务每一个进度的规划时间,而是做到哪算到哪,导致任务提交前十分匆忙;第二是,对于结对伙伴不够信任,常常会心理担心代码中是不是有bug,会被查出然后扣分。
在这次的体验中,我感受到的结对编程的一些优缺点:
优点:
缺点
我:push力十足,督促着项目的进展;乐于奉献自己的时间,完成更多的任务;愿意探索新事物新技术新方法,但是做事效率比较低,遇到困难时心态容易急躁
伙伴:做事效率比较高;善于沟通;代码简洁思路清晰,但是有些特殊条件的考虑不够充分,对于细节不够重视
PSP2.1
Personal Software Process Stages
预估耗时(分钟)
实际耗时(分钟)
Planning
计划
· Estimate
· 估计这个任务需要多少时间
10
5
Development
开发
· Analysis
· 需求分析 (包括学习新技术)
500
660(包括搭建、学习Qt)
· Design Spec
· 生成设计文档
15
20
· Design Review
· 设计复审 (和同事审核设计文档)
10
5
· Coding Standard
· 代码规范 (为目前的开发制定合适的规范)
5
3
· Design
· 具体设计
15
5
· Coding
· 具体编码
140
195
· Code Review
· 代码复审
15
10
· Test
· 测试(自我测试,修改代码,提交修改)
120
240
Reporting
报告
· Test Report
· 测试报告
30
15
· Size Measurement
· 计算工作量
5
2
· Postmortem & Process Improvement Plan
· 事后总结, 并提出过程改进计划
5
5
合计
920
1165
时间相差了245分钟,将近4个小时。我回忆了一下,主要是这几个方面占用了太多的时间:1) Qt的安装,因为依赖没有安装导致四处碰壁 2) Qt不能使用vs导出的.dll库,摸索许久 3) 核心模块的设计并没有对UI方面做好足够的支持,发现了一些接口的不足和缺陷 4) Qt绘制曲线图有一定的难度,如果使用plot相关的插件,相信效果会好很多。
这次结对过程,本人就是很吃了设计上的亏,在具体编码过程中,遇到了不少意料之外的问题,急急忙忙去寻求解决方案。因此,我得到的教训最重要的就是,必须事先在设计阶段就将大部分的问题发现并解决。这个过程需要合作伙伴一起头脑风暴,利用白板等工具画出依赖关系、业务流程等,然后试图发现其中的问题,并且对于已经发现的问题找到可能的解决方案,未雨绸缪。
手机扫一扫
移动阅读更方便
你可能感兴趣的文章