2020BUAA软工结伴项目作业
17373010 杜博玮
项目 | 内容 |
---|---|
这个作业属于哪个课程 | 2020春季计算机学院软件工程(罗杰 任健) |
这个作业的要求在哪里 | 结伴项目作业 |
我在这个课程的目标是 | 学习软件工程,培养工程开发能力、团队协作能力,开阔视野 |
这个作业在哪个具体方面帮助我实现目标 | 通过结伴进行项目来进一步实践,提高软工水平 |
教学班级 005
项目地址 https://github.com/17373432/Pairng-Project
PSP表格:
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
Estimate | 估计这个任务需要多少时间 | 15 | 10 |
Development | 开发 | ||
Analysis | 需求分析 (包括学习新技术) | 540 | 510 |
Design Spec | 生成设计文档 | 45 | 40 |
Design Review | 设计复审 | 15 | 20 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 15 | 10 |
Design | 具体设计 | 90 | 75 |
Coding | 具体编码 | 540 | 450 |
Code Review | 代码复审 | 60 | 40 |
Test | 测试(自我测试,修改代码,提交修改) | 150 | 180 |
Reporting | 报告 | ||
Test Reporting | 测试报告 | 45 | 40 |
Size Measurement | 计算工作量 | 15 | 10 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 25 | 30 |
合计 | 1555 | 1415 |
3. 看教科书和其它资料中关于 Information Hiding,Interface Design,Loose Coupling 的章节,说明你们在结对编程中是如何利用这些方法对接口进行设计的。
Information Hiding,即信息隐藏,在模块内需要进行封装,提供给外部的接口需要保证其他模块调用时不必关心其中的实现细节。这一点在我们的GUI与Core的接口中表现最为明显,GUI向core模块提供输入流,即可得到图中交点的数组,并不需要使用core模块中的任何数据结构。
Interface Design,接口设计。我们在接口设计中主要保证输入尽可能简洁,各模块分工明确,不会产生不同的模块进行类似的工作的情况。
Loose Coupling,即松耦合。我们在设计过程中保证模块之间只通过模块自身定义的接口进行调用,而不是一个模块直接访问另一个模块内部的数据,从而尽可能减少各模块之间的依赖关系。
4.模块接口的设计与实现过程。设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图?说明你的算法的关键(不必列出源代码),以及独到之处。
-
主类:将各个类联系起来,完成基本功能以及错误处理。
- 属性:
- 线的集合,用于存储所有的直线、线段、射线的信息,用vector储存。
- 圆的集合,用于储存所有的圆的信息,用vector储存。
- 点的集合,用于储存所有交点的信息,用unordered_set储存。
- 其他用于辅助判断数据是否合法的属性。
- 方法:求解交点,判断两条线是否重合等方法。
- 属性:
-
Line类:采用一般式(Ax+By+C=0),避免了斜率为正无穷的讨论。
- 属性:基本的A,B,C的值,线的种类,线的两个端点坐标,以及其他简化计算的属性。
- 方法:求与直线的交点,判断点是否在线上,以及构造函数,返回属性的值,重载小于运算符等方法。
-
Point类:
- 属性:横坐标x,纵坐标y,交于此点的直线集合。
- 方法:构造函数,返回属性的值,添加经过这个点的直线,重载小于、等于运算符等方法。
-
Circle类:
- 属性:圆心(使用Point),半径,以及其他简化计算的属性。
- 方法:求与直线的交点,求与圆的交点,以及构造函数,返回属性的值,重载小于运算符等方法。
其他方法:计算Point类的hash值,计算pair的hash值,保留四位有效数字,浮点数比较等方法。
关键函数:
-
判断两条线是否重合:
因为对于线来说只有截距相同才可能会重合,所以事先储存线的截距到线的映射,通过截距将线分组,这样只用同一组的线是否重合,具体判断如下:
result = false; if (两条线都为线段) { if (某线段某端点在另一条线段两端点之间) { if (两线段只有一个点p重合) { pointSet.insert(p); } else { result = true; } } } else if (两条线都为射线) { if (某射线端点在另一条射线上) { if (两射线只有一个点p重合) { pointSet.insert(p); } else { result = true; } } } else if (两条线分别为一条射线与一条线段) { if (线段某端点在射线上) { if (两条线只有一个点p重合) { pointSet.insert(p); } else { result = true; } } } return result;
伪代码看起来很简单,但实现起来十分繁琐,这个函数写了140多行,通过画图将各种情况画出来,然后再写的代码,这样写条理十分清楚,也并没有写出bug。
更重要一点,在这里将所有截距相同的线的交点全部算出来了,所以之后算线与线的交点时可以不必考虑平行的线的交点,可直接复用上一次代码,也节省了计算。
-
判断点是否在线上:
因为我设计的射线和线段只是在直线的基础上加了一个属性来判断线的种类,所以算点是否在线上是通过先假定该线为直线,计算出直线的交点坐标(x, y),之后需比较坐标是否在范围内即可,具体实现过程如下:
bool isOnLine(double x, double y) { bool result = true; if (是线段) { if (线段的两端点横坐标不相等) { result = (x是否在线段两端点横坐标之间); } else { result = (y是否在线段两纵坐标之间); } } else if (是射线) { if (构造射线的两端点横坐标不相等) { result = (x是否在射线延伸的一边); } else { result = (y是否在射线延伸的一边); } } return result; }
这里我一开始只传了横坐标
x
,认为只用x
就能判断点是否在线上,这样就导致了当线段或者射线两端点横坐标相同时,我会把这些交点全当成在线上的点(应为两端点横坐标相同,所以交出来的点横坐标一定在两点之间),这样就导致我的点算多了,我花了很久才定位出这个bug,所以说不能为了省时间没考虑全面就想当然的写代码,否则debug浪费的时间就更多。另外,我在这个函数中加入了点是否在直线上的判断后,最终求得的点数量变少了,从理论上进行分析加不加这个判断对结果应该是没有影响的,通过调试才发现浮点误差通过加减乘除运算之后,误差会变得更大,导致超过了一开始设置的浮点精度(10^{-12}),所以才判断出计算出直线的交点不在直线上的情况,通过计算器验算,误差达到了(0.03),说明四则运算对浮点误差的影响很大,然而我只考虑了比较时的浮点误差,没有考虑计算时误差会变化的情况,这个地方虽然可以不用加点是否在直线上的判断从而忽略这个问题,但这个问题是普遍存在的,也不好避免,我想到最后也只有用不同的精度:计算前取高精度,比较时用低精度。但这样也不能完全解决问题,这次作业数据范围是(10^5),假设
double
类型精度为(10^{-15}),计算直线时直线的参数(a,b)的范围会变成(10^5),精度变成(10^{-14}),(c)的范围变成(10^{10}),精度变成(10^{-9}),然后在计算交点时,分子的范围会变成(10^{15}),精度变成(10^{-3}),这已经到了无法忽视的地步了,因为没考虑精度变化,所以很有可能两个点相同但是判为不同或者是不同的点判为相同的情况。这个问题我并没有想到一个好的方法解决。 -
浮点数的hash函数:
注意浮点数在hash前要保留有效数字,防止浮点误差对hash值产生影响。
-
接口:
通过读取数据信息,返回交点个数或者抛出异常。
extern "C" void __declspec(dllexport) getPoints(vector<pair<double, double>> & points, stringstream & in);
一开始的想法是设计
addObj()
,deleteObj()
这些接口的,但是考虑到删除可能和重新导入一遍数据的复杂度差不了多少,所以干脆就将删除操作定义为先删除数据再将数据重新导入,这样设计了之后,感觉单单为了一个增加操作而在GUI
上再写一遍错误处理不值得,所以索性就把增加操作也换成导入数据信息,最后将几个接口合而为一,变成上述单一的接口,虽然在改动时开销更大,但是实现起来简单了许多,也减少了代码的冗余。
5. 阅读有关 UML 的内容:https://en.wikipedia.org/wiki/Unified_Modeling_Language。画出 UML 图显示计算模块部分各个实体之间的关系(画一个图即可)。
6.计算模块接口部分的性能改进。记录在改进计算模块性能上所花费的时间,描述你改进的思路,并展示一张性能分析图(由VS 2015/2017的性能分析工具自动生成),并展示你程序中消耗最大的函数。
这是(6000)个数据,最终交点个数为(10146161)个点的性能分析图:
因为只有一个接口,所以这个接口开销最大:
7. 看 Design by Contract,Code Contract 的内容:
- http://en.wikipedia.org/wiki/Design_by_contract
- http://msdn.microsoft.com/en-us/devlabs/dd491992.aspx
描述这些做法的优缺点,说明你是如何把它们融入结对作业中的。
Design by Contract,即契约式设计,主要是规定接口中各参数的格式,保证一个函数的输入和输出都在我们的期望值之内。
优点:我们可以通过JML等比较规范化的注释来直接验证程序正确性,有助于代码的测试;使功能需求更为明确,代码质量更高。
缺点:这种方式会使程序的开销增大;这种编程方式依赖于编程语言的支持;编程方式没有统一的规格,可能会导致代码风格的混乱;有时程序的输入输出无法准确确定,难以使用契约式设计。
这次结对作业中我们在GUI的设计中就借助了契约式设计,将输入框使用正则表达式进行规范,强迫使用者输入符合我们程序要求的输入。
8. 计算模块部分单元测试展示。展示出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路。并 将单元测试得到的测试覆盖率截图,发表在博客中。要求总体覆盖率到 90% 以上,否则单元测试部分视作无效。
部分单元测试代码以及测试的函数如下所示。
TEST_METHOD(TestMethod14)
{
//测试平行于x轴直线,测试line类
Proc p;
string s = "2 L 1 0 2 0
L -1 1 -1 2";
std::stringstream ss(s);
p.process(ss);
vector <pair<double, double>> result;
p.getPointSet(result);
Assert::AreEqual(1, (int)result.size());
}
TEST_METHOD(TestMethod15)
{
//测试直线、线段、射线之间冲突,测试line类
Proc p;
string s = "7 L 1 3 4 2
L -1 4 5 2
S 2 4 3 2
R 2 5 -1 2
C 3 3 3
C 2 2 1
C 3 2 2";
std::stringstream ss(s);
p.process(ss);
vector <pair<double, double>> result;
p.getPointSet(result);
Assert::AreEqual(20, (int)result.size());
}
TEST_METHOD(TestMethod16)
{
//大型暴力测试,主要测试proc类
Proc p;
string s = "34
L 1 3 4 2
L -1 4 5 2
S 2 4 3 2
R 2 5 -1 2
C 3 3 3
C 2 2 1
C 3 2 2
L 99999 99999 -99999 -99999
L -99998 99998 99998 -99998
R 0 99 -1 100
S 0 99 1 98
S 2 97 1 98
S 2 97 3 96
S 4 95 3 96
S 4 95 5 94
S 6 93 5 94
R 99 0 100 -1
R 99 0 100 1
R 0 99 -1 -100
S 0 -99 1 -98
S 1 -98 2 -97
S 99 0 98 -1
S 3 -96 4 -95
S 2 -97 3 -96
S 99 0 98 1
S 11 88 10 89
S 12 87 11 88
S 10000 10000 99999 10000
S 10000 9999 10000 10000
R 8888 8888 8888 8889
S 1245 1245 1244 1247
S 1244 1244 1243 1246
S 2444 2444 2443 2447
S 2442 2442 2443 2445
";
std::stringstream ss(s);
p.process(ss);
vector <pair<double, double>> result;
p.getPointSet(result);
Assert::AreEqual(54, (int)result.size());
}
我们构造单元测试的思路主要是首先针对小数据,特定情况,对特定情况的测试大概覆盖完全后进行更大规 模的测试。
单元测试通过截图:
单元测试覆盖率截图:
9. 计算模块部分异常处理说明。在博客中详细介绍每种异常的设计目标。每种异常都要选择一个单元测试样例发布在博客中,并指明错误对应的场景。
公共父类:
class InputException :public exception {
private:
string msg;
public:
InputException(string e) {
msg = e;
}
const char* what() const throw() {
cout << msg << endl;
return msg.data();
}
string getMsg() const {
return msg;
}
};
N不在范围内:
(1<=N<=500000)
class NumberException :public InputException {
public:
NumberException() :
InputException("N is out of range!
") {};
const char* what() const throw() {
cout << getMsg() << endl;
return getMsg().data();
}
};
样例1:
0
输出1:
N is out of range!
样例2:
500001
输出2:
N is out of range!
参数不在范围内:
范围为 ((-100000, 100000))
class OutOfRangeException :public InputException {
public:
OutOfRangeException(int i, string str) :
InputException("Line " + to_string(i) + ": "" + str + ""
The parameter(s) is(are) out of range!
") {};
const char* what() const throw() {
cout << getMsg() << endl;
return getMsg().data();
}
};
样例3:
4
C 3 3 3
S 222222 4 3 2
L -1 4 5 2
R 2 5 -1 2
输出3:
Line 2: "S 222222 4 3 2"
The parameter(s) is(are) out of range!
构造线的两点重合:
class CoincideException :public InputException {
public:
CoincideException(int i, string str) :
InputException("Line " + to_string(i) + ": "" + str + ""
The two points coincide!
") {};
const char* what() const throw() {
cout << getMsg() << endl;
return getMsg().data();
}
};
样例4:线段两点重合
4
C 3 3 3
S 2 4 2 4
L -1 4 5 2
C 2 5 1
输出4:
Line 2: "S 2 4 2 4"
The two points coincide!
图形重叠:
class CoverException :public InputException {
public:
CoverException(int i, string str) :
InputException("Line " + to_string(i) + ": "" + str + ""
Overlap with added drawings!
") {};
const char* what() const throw() {
cout << getMsg() << endl;
return getMsg().data();
}
};
样例5:圆与圆重合
5
C 3 3 3
S 2 4 3 2
L -1 4 5 2
R 2 5 -1 2
C 3 3 3
输出5:
Line 5: "C 3 3 3"
Overlap with added drawings!
样例6:以线段与线段重合为例
5
C 3 3 3
S 2 4 4 0
L -1 4 5 2
R 2 5 -1 2
S 3 2 5 -2
输出6:
Line 5: "S 3 2 5 -2"
Overlap with added drawings!
对象数目不等于N:
class NumOfObjException :public InputException {
public:
NumOfObjException() :
InputException("The number of geometric objects is not equal to N!
") {};
const char* what() const throw() {
cout << getMsg() << endl;
return getMsg().data();
}
};
样例7:对象数大于N
3
C 3 3 3
S 2 4 3 2
L -1 4 5 2
R 2 5 -1 2
输出7:
The number of geometric objects is not equal to N!
样例8:对象数小于N
5
C 3 3 3
S 2 4 3 2
L -1 4 5 2
R 2 5 -1 2
输出8:
The number of geometric objects is not equal to N!
输入格式错误:
class FormatException :public InputException {
public:
FormatException(int i, string str) :
InputException("Line " + to_string(i) + ": "" + str + ""
The format of input is illgal!
") {};
const char* what() const throw() {
cout << getMsg() << endl;
return getMsg().data();
}
};
样例9:参数有前导零
4
C 3 3 3
S 2 4 3 2
L -01 4 5 2
R 2 5 -1 2
输出9:
Line 3: "L -01 4 5 2"
The format of input is illgal!
样例10:标识字母小写
4
C 3 3 3
s 2 4 3 2
L -1 4 5 2
R 2 5 -1 2
输出10:
Line 2: "s 2 4 3 2"
The format of input is illgal!
圆的半径不大于零:
class LessThanZeroException :public InputException {
public:
LessThanZeroException(int i, string str) :
InputException("Line " + to_string(i) + ": "" + str + ""
Radius of circle must be greater than zero!
") {};
const char* what() const throw() {
cout << getMsg() << endl;
return getMsg().data();
}
};
样例11:半径小于0
4
C 3 3 3
S 2 4 3 2
L -1 4 5 2
C 2 5 -1
输出11:
Line 4: "C 2 5 -1"
Radius of circle must be greater than zero!
样例12:半径等于0
4
C 3 3 3
S 2 4 3 2
L -1 4 5 2
C 2 5 0
输出12:
Line 4: "C 2 5 0"
Radius of circle must be greater than zero!
空文件:
class EmptyFileException :public InputException {
public:
EmptyFileException() :
InputException("The input file is empty!
") {};
const char* what() const throw() {
cout << getMsg() << endl;
return getMsg().data();
}
};
样例13:
输出13:
The input file is empty!
文件第一行不是N:
class NoNException :public InputException {
public:
NoNException() :
InputException("The first line of input file is not a number!
") {};
const char* what() const throw() {
cout << getMsg() << endl;
return getMsg().data();
}
};
样例14:
C 3 3 3
S 2 4 2 5
L -1 4 5 2
C 2 5 1
输出14:
The first line of input file is not a number!
命令行参数有误:
样例15:
./intersect.exe
输出15:
Right Format: intersect.exe -i <path to input file> -o <path to output file>
文件无法打开:
样例16:
./intersect.exe -i dir -o output.txt
输出16:
Input File Cannot Open!
最后错误处理的单元测试:
10. 界面模块的详细设计过程。在博客中详细介绍界面模块是如何设计的,并写一些必要的代码说明解释实现过程。
界面模块利用VS的Qt插件开发。GUI共含4个窗口。分别如下所示:
第一个窗口为主窗口,在其中我们进行交互、输出图像信息。剩下三个窗口全部用来进行手动导入数据。
主窗口的图像绘制是我们自己手绘的,因为没有找到很好的绘制图像的类。我们将paintEvent函数重定向到widget组件中,使widget组件收到重绘的指令后执行widgetPaint函数对整个画面重新绘制。
绘制过程中比较重要的是坐标与像素之间的转换。
double QtPairProject::transferX(double x) {
int pointx = 20;
int width = length - 20 - pointx;
double kx = (double)width / (x_max - x_min );
return pointx + kx * (x - x_min);
}
double QtPairProject::transferY(double y) {
int pointy = wide - 20;
int height = wide - 40;
double ky = (double)height / (y_max - y_min );
return pointy - (y - y_min) * ky + offset;
}
交互控件主要控制以下功能,修改坐标轴的范围、从文件导入数据、手动导入数据、删除lineWidget中被选中的数据、清空lineWidget,坐标图和输出栏、以及绘制交点功能。右下角的输出框用于程序运行情况的输出,例如异常报告、绘制交点情况反馈。
其余三个窗口用于手动导入数据。在这四个窗口之间我们借助signals从子窗口向父窗口发送信息。
例如在主窗口QtPairProject.h中使用
signals:
void sendsignal(QString);
在QtPairProject.cpp中使用
void QtPairProject::OnOpenButtonClick() {
win->show();
}
void QtPairProject::getData(QString data) {
this->show();
}
接收子窗口向父窗口发送的信息。
在子窗口NewQtGuiClass.h中定义
public slots:
void sendData();
在NewQtGuiClass.cpp中使用
NewQtGuiClass::NewQtGuiClass(QWidget *parent)
: QWidget(parent)
{
ui.setupUi(this);
connect(ui.pushButton, SIGNAL(clicked()), this, SLOT(pushButton()));
}
void NewQtGuiClass::sendData() {
emit sendsignal(buf);
buf = "";
this->close();
}
向父窗口传递信息。
在子窗口的数据输入以及修改坐标范围的输入中,我们采用了正则表达式检验以及整型数验证控件来限制输入的范围。
QRegExp rx("^-?\d{1,6}$");
QRegExpValidator* v = new QRegExpValidator(rx, this);
QIntValidator *intValidator = new QIntValidator;
intValidator->setRange(-100000, 100000);
ui.lineEdit->setValidator(v);
ui.lineEdit_2->setValidator(v);
intValidator->setRange(0, 100000);
ui.lineEdit_3->setValidator(intValidator);
11. 界面模块与计算模块的对接。详细地描述 UI 模块的设计与两个模块的对接,并在博客中截图实现的功能。
我们通过输入流来实现界面模块与计算模块的对接。
extern "C" void __declspec(dllexport) getPoints(vector<pair<double, double>> & points, stringstream & in);
即GUI组件将输入打包成控制台输入的格式通过流传输给计算模块,由计算模块传回计算得到的交点数组。
实现的功能:
修改坐标范围:
从文件导入:
手动导入
删除所选数据
绘制交点
清空:
12. 描述结对的过程,提供两人在讨论的结对图像资料(比如 Live Share 的截图)。关于如何远程进行结对参见作业最后的注意事项。
我们的结对过程主要通过QQ的屏幕共享以及语音进行,同时由于网络及工作时间的差别,我们也经常通过微信进行交流。
屏幕共享:
微信交流:
13. 看教科书和其它参考书,网站中关于结对编程的章节,例如:http://www.cnblogs.com/xinz/archive/2011/08/07/2130332.html ,
说明结对编程的优点和缺点。同时描述结对的每一个人的优点和缺点在哪里(要列出至少三个优点和一个缺点)。
结对编程优点:
优点在于随时都在进行测试,代码的错误率低。同时开发效率在有队友监督的情况下也得到了很大的提高,连摸鱼的时间都没了。
整个编程过程都是给自己的极限压力测试,整个项目做下来感觉收获颇多。
结对编程缺点:
刚开始的磨合期有些磕磕绊绊,配合上不够舒服。
两人的写代码习惯不同时可能会有些别扭。
如果两人的编程能力差距太差就会变成较强的人的一个人的舞台,失去了结对编程的优势。
每个人的优点:
杜博玮:经常与对方进行沟通、善于查找资料、有耐心
陈驰:代码能力强、细心、工作效率高
每个人的缺点:
杜博玮:代码错误处理不够好
陈驰:有一些腼腆