软件工程结对作业——交点求解PLUS
项目 | 内容 |
---|---|
这个作业属于哪个课程 | 课程链接 |
这个作业的要求在哪里 | 作业链接 |
教学班级 | 006 |
项目地址 | Github地址 |
我的结对伙伴是滕琦同学,学号17373059。
在开始实现程序之前,在下述 PSP 表格记录下你估计将在程序的各个模块的开发上耗费的时间。
在你实现完程序之后,在附录提供的PSP表格记录下你在程序的各个模块上实际花费的时间。
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
· Estimate | · 估计这个任务需要多少时间 | 10 | 10 |
Development | 开发 | ||
· Analysis | · 需求分析 (包括学习新技术) | 300 | 600 |
· Design Spec | · 生成设计文档 | 30 | 30 |
· Design Review | · 设计复审 (和同事审核设计文档) | 10 | 30 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 30 |
· Design | · 具体设计 | 30 | 40 |
· Coding | · 具体编码 | 200 | 500 |
· Code Review | · 代码复审 | 100 | 60 |
· Test | · 测试(自我测试,修改代码,提交修改) | 150 | 100 |
Reporting | 报告 | ||
· Test Report | · 测试报告 | 30 | 20 |
· Size Measurement | · 计算工作量 | 10 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 40 |
合计 | 910 | 1470 |
严重超时的部分有(1)学习MFC新技术(2)具体编码,由于不熟悉C++的面向对象特性而花了特别的的时间。
3.看教科书和其它资料中关于 Information Hiding,Interface Design,Loose Coupling 的章节,说明你们在结对编程中是如何利用这些方法对接口进行设计的。
- 信息隐藏
- 有关计算交点的信息全部封装在dll库中,通过接口与UI进行交互,数据和计算方法全部为隐藏的,禁止对核心的数据和方法进行修改。
- 接口设计
- UI部分与核心部分通过readFile,addGeometryObject,removeGeometryObject,getResult四个接口联系,核心部分其他的方法是隐藏的。
- 良好的接口设计可以降低耦合度,有利于单元测试。在单元测试中可以对核心部分的功能全面测试,同时良好的继承关系使接口可以复用,降低程序的复杂度。
- 松耦合
- 各个图形之间求交点之外没有其他联系,不会修改其他类的属性,图形由更高层的容器统一管理,实现了类之间的松耦合。
- 界面部分和核心部分只由简单的函数通信,不会修改对方的属性,也无法看到内部的实现方法。同时可以做到只进行简单修改就能和其他小组互相使用核心部分。
4. 计算模块接口的设计与实现过程。
1.几何图形模块
相较于上次作业,本次作业多出射线和线段,需要对几何图形的类结构进行增量设计。
我们约定直线方程如下:
经过归一化后的直线系数能为后续的直线共线判断提供便利。
我们约定圆方程如下:
对于所有的几何对象(除点),都需要实现如下功能
- 从规格化字符串或浮点数参数建立几何对象
- 求交点
- 判断点是否在对象上
- 重写
==
运算符 - 重写
<
运算符 - 生成格式化字符串
下面将针对和上次作业不同的部分进行阐述
类设计
几何对象 | 设计 |
---|---|
直线 | 实际上是3个系数的封装 |
射线 | 在直线的基础上添加起点和方向 |
线段 | 在直线的基础上添加两个端点 |
圆 | 圆心和半径的封装 |
点 | pair<double, double> 的封装 |
判断相等
不同种类的几何对象肯定不相等
求交点
直线 | 射线 | 线段 | 圆 | |
---|---|---|---|---|
直线 | 直接联立求交点 | 联立求出交点后判断交点是否在射线上 | 联立求出交点后判断交点是否在线段上 | 直接联立求交点 |
射线 | 联立求出交点后判断交点是否在射线上 | 1.不共线:联立求出交点后分别判断是否在射线上。2.共线:判断是否只有一个交点。 | 1.不共线:联立求出交点后分别判断是否在射线和线段上。2.共线:判断是否只有一个交点。 | 联立求出交点后判断交点是否在射线上 |
线段 | 联立求出交点后判断交点是否在线段上 | 1.不共线:联立求出交点后分别判断是否在射线和线段上。2.共线:判断是否只有一个交点。 | 1.不共线:联立求出交点后分别判断是否在线段上。2.共线:判断是否共端点。 | 联立求出交点后判断交点是否在线段上 |
圆 | 直接联立求交点 | 联立求出交点后判断交点是否在射线上 | 联立求出交点后判断交点是否在线段上 | 直接联立求交点 |
射线-射线共线有交点
射线-线段共线有交点
2.几何对象容器
几何对象容器的作用是组织几何对象和交点,其功能如下
-
存储组织几何对象与交点
-
增删几何对象
-
求解交点
几何对象容器在内部完成复杂的几何对象交互逻辑,对外部只需要开放简单接口即可。这样设计屏蔽几何对象相关细节使得修改具体实现变得容易,体现出information hiding
和Loose Coupling
的思想。
几个对象容器的接口如下
接口名 | 作用 |
---|---|
insertFromString |
由规格化字符串插入几何对象 |
deleteFromString |
由规格化字符串删除几何对象 |
getPointNum |
获得交点数量 |
getPointSet |
取出交点集合 |
getLineSet |
取出线(规格化字符串)集合 |
getCircleSet |
取出圆(规格化字符串)集合 |
3.IO类
检查文件是否合法(有无n,n和实际几何对象数量是否相符),处理IO异常(文件不存在、IO错误等)。屏蔽IO细节,错误局部化。
5.画出 UML 图显示计算模块部分各个实体之间的关系
6.计算模块接口部分的性能改进。
1.算法层面
暴力求解,算法复杂度为(O(n^2)),在短时间内没有改进的空间。
2.实现层面
2000个几何对象,四类对象出现概率均等。
- 优化一:从哈希到红黑树。在上个项目中使用的是
unordered_set
,理论上是(O(1))的复杂度,但是实际应用起来结果却惨不忍睹。猜测可能是哈希操作带来的大量主存访问占用大量时间。采用set
之后,虽然容器访问仍是时间占用主体,但是相比哈希占比已经下降很多。 - 优化二:优化正则表达式。在仔细阅读作业要求博客之后,将一些正则表达式中不必要的部分去除,从而略微减小正则表达式匹配所占用的时间。
3.接口问题
在引入前后端接口之后,带来了一定的性能损失。因为现在所采用的接口是两个小组通用的,所以两个小组都为适配接口增加一层适配函数。在不对后端进行重构的情况下,我们很难将这部分性能损失完全抹除。只能在适配时采用内联等措施减小性能损失。
完全消除性能损失也许需要大量的项目经验和相关思考,但遗憾的是我们没能在此次作业中做到。
7. Design by Contract,Code Contract
契约式设计和代码协定的优点有
- 接口要求明确,可以很好的对程序的行为进行预期。
- 代码用途明确,约定的信息可以帮助改写函数。
- 可以通过插件方法进行快速全面的测试。
缺点有
- 设计复杂,难以书写,对于比较复杂的功能难以给出合理的约定。
- 目前似乎还不流行
本次作业中主要对UI和核心的接口进行了协定,两个人在沟通之后制定了接口的规格和行为,便于分块开发和测试。这样应该是使用了契约式设计的核心思想,但因为不熟悉相关知识,并没有给出具体的前置条件,后置条件和不变式。
8. 计算模块部分单元测试展示。
针对不同的功能点和错误点,我们准备了不同的单元测试。
1.单元测试的整体情况如下
测试覆盖率92%
2.具体测试用例(部分展示)
针对几何对象相交功能直接测试
// 射线端点与直线相交
TEST_METHOD(TestMethod1)
{
Table table = Table();
Straight s(0, 0, 1, 1);
Ray r(0, 0, 0, 1);
int result = s.GetCrossPoint(table.getPointSet(), &r);
Assert::AreEqual(result, 1);
}
// 射线和线段端点与圆相交
TEST_METHOD(TestMethod3)
{
Table table = Table();
Circle c(Point(0, 0), 1);
Segment s(0, -1, 0, 1);
Ray r(-1, 0, 5, 0);
int result = c.GetCrossToLine(table.getPointSet(), s);
Assert::AreEqual(result, 2);
Assert::AreEqual(c.GetCrossToLine(table.getPointSet(), r), 2);
Assert::AreEqual(r.GetCrossPoint(table.getPointSet(), &s), 1);
}
// 圆之间内切与外切
TEST_METHOD(TestMethod4)
{
Table table = Table();
Circle c(Point(0, 0), 1);
Circle c1(Point(3, 0), 2);
Circle c2(Point(2, 0), 1);
c.GetCrossToCircle(table.getPointSet(), c1);
c1.GetCrossToCircle(table.getPointSet(), c2);
Assert::AreEqual(1, (int)(table.getPointNum()));
}
整体性测试 + 异常测试
TEST_METHOD(TestMethod6)
{
Table table = Table();
ofstream output = ofstream("output.txt");
output << "4
C 0 0 1
C 3 0 2
C 2 0 1
L 0 0 3 0
" << endl;
output.close();
ifstream open = ifstream("output.txt");
// IO模块测试
readFromFile(table, open);
Assert::AreEqual(4, (int)table.getPointNum());
Circle circle = Circle(Point(0, 0), 1);
// 删除圆功能测试
table.eraseCircle(circle);
Assert::AreEqual(3, (int)table.getPointNum());
circle = Circle(Point(2, 0), 1);
// 删除圆功能测试
table.eraseCircle(circle);
Assert::AreEqual(2, (int)table.getPointNum());
Line* line = new Line(0, 0, 1, 0);
try
{
// 无穷多交点异常测试
table.insertLine(*line);
}
catch (const Doublication& e)
{
Assert::AreEqual(line->toString(), (string)(e.what()));
}
Assert::AreEqual(2, (int)table.getPointNum());
// 删除线功能测试
table.eraseLine(line);
Assert::AreEqual(0, (int)table.getLineSet().size());
}
TEST_METHOD(TestMethod7)
{
Table table = Table();
ofstream output = ofstream("output.txt");
output << "4
C 0 0 1
C 3 0 2
C 2 0 1
L 0 0 3 0
" << endl;
output.close();
ifstream open = ifstream("output.txt");
readFromFile(table, open);
Circle circle = Circle(Point(0, 0), 1);
try
{
// 无穷多交点异常测试
table.insertCircle(circle);
}
catch (const Doublication & e)
{
Assert::AreEqual(circle.toString(), (string)(e.what()));
}
}
Table table = Table();
ofstream output = ofstream("output.txt");
output << "1
L 0 0 0 0
" << endl;
output.close();
ifstream open = ifstream("output.txt");
try
{
// 定义点重合异常测试
readFromFile(table, open);
}
catch (const pointDoublication& pd)
{
Assert::AreEqual(Line(0, 0, 0, 0).toString(), (string)pd.what());
}
易错点测试
TEST_METHOD(TestMethod11)
{
Table table = Table();
ofstream output = ofstream("output.txt");
// 线段垂直于X轴,射线垂直于y轴,可能会造成点位置判断错误
output << "2
R -1 0 1 0
S 0 1 0 2" << endl;
output.close();
ifstream open = ifstream("output.txt");
readFromFile(table, open);
Assert::AreEqual(0, (int)table.getPointNum());
}
新增需求特殊点测试
// 线段-线段共线有焦点
TEST_METHOD(TestMethod15)
{
Table table = Table();
ofstream output = ofstream("output.txt");
output << "2
S 0 0 1 1
S 1 1 2 2" << endl;
output.close();
ifstream open = ifstream("output.txt");
readFromFile(table, open);
Assert::AreEqual(2, (int)table.getLineSet().size());
Assert::AreEqual(1, (int)table.getPointNum());
}
// 线段-射线共线有焦点
TEST_METHOD(TestMethod16)
{
Table table = Table();
ofstream output = ofstream("output.txt");
output << "2
S 0 0 1 1
R 1 1 2 2" << endl;
output.close();
ifstream open = ifstream("output.txt");
readFromFile(table, open);
Assert::AreEqual(2, (int)table.getLineSet().size());
Assert::AreEqual(1, (int)table.getPointNum());
}
// 射线-射线共线有交点
TEST_METHOD(TestMethod17)
{
Table table = Table();
ofstream output = ofstream("output.txt");
output << "2
R 1 1 0 0
R 1 1 2 2" << endl;
output.close();
ifstream open = ifstream("output.txt");
readFromFile(table, open);
Assert::AreEqual(2, (int)table.getLineSet().size());
Assert::AreEqual(1, (int)table.getPointNum());
}
9.计算模块部分异常处理说明。
1.IO异常处理
文件不存在或打开失败
if (!(infile.is_open()))
{
cout << "open file fail" << endl;
exit(0);
}
文件读取异常或N值大于实际几何对象数目
if (!getline(infile, getLine))
{
cout << "n is bigger than the true number of geometry or IO error" << endl;
break;
}
N值小于实际几何对象数目
// 循环结束之后
if (getLine(infile, getLine))
{
cout << "n is smaller than the true number of geometry" << endl;
break;
}
2.输入合法性处理
N值合法性处理
regex reg1("\s*\+?0*[1-9]\d*\s*"); // 利用正则检查N输入的合法性:形式和范围
if (!regex_match(getLine, reg1))
{
cout << "Error: the first line of input file is not a regular positive number." << endl;
}
几何对象格式化字符串合法性处理
用正则从形式和数值范围上约束几何对象格式化字符串
static string number = "([1-9]\d{0,4})";
static string radius = "(\+?" + number + ")";
static string number0 = "((0)|(" + number + "))";
static string snumber0 = "([+-]?" + number0 + ")";
static regex reg(
"\s*("
"([LRS]\s+" + snumber0 + "\s+" + snumber0 + "\s+" + snumber0 + "\s+" + snumber0 + ")"
"|"
"(C\s+" + snumber0 + "\s+" + snumber0 + "\s+" + radius + ")"
")\s*"
);
if (!regex_match(erase, reg)) {
throw domain_error(erase);
}
3.几何对象容器异常处理
重复几何对象输入处理
注意,我们认为有无穷多交点但不重复的情况并不算做异常,只是在计算时不会考虑这种情况。
if (lineSet.count(&l) > 0)
{
throw Doublication(l.toString());
}
if (circleSet.count(circle) > 0)
{
throw Doublication(circle.toString());
}
定义点重合处理
if (start == end)
{
throw pointDoublication(this->toString());
}
10. 界面模块的详细设计过程。
- 界面模块使用VS的mfc库进行设计,因为作业要求的界面并不复杂,所以使用不带菜单栏的对话框作为设计的基础。微软的官方mfc文档是设计时主要学习和参考的对象。最终的界面如下:
界面第一个Import按钮实现从文件导入的功能,通过用户输入读取的值获得文件路径,再通过接口与由计算模块实现具体的解析操作。在用户点击按钮时更新编辑框的控件变量,将得到的文本作为输入参数调用接口即可。
UpdateData(TRUE);
std::string path;
path = CT2A(m_FILEPATH.GetString());
try
{
readFile(path);
}
catch (const std::exception&)
{
...
}
m_EXCEPTMESSAGE = CString(_T("读取文件成功"));
UpdateData(FALSE);
-
添加,删除图形
通过ADD和DELETE按钮可以进行图形的手工添加和删除,只需按照规定格式输入图形数据即可。
在点击按钮后获取编辑框的控件变量,输入计算模块。以添加直线为例,具体代码为
UpdateData(TRUE); std::string line; line = CT2A(m_LineValue.GetString()); try { addGeometryObject(line); } catch (const std::exception&) { ... }
-
求解与绘制
通过Solve按钮可以计算图形的交点个数,此功能也是通过调用计算模块实现的。在获得交点个数后反向更新控件变量即可显示数字。
通过Draw按钮可以绘制现有图形的图像,默认是以网格坐标轴为背景显示图形。这部分先从计算模块获取图形信息,再调用相关的图形API完成绘图。为了解决坐标与像素大小转换的问题,在程序中设置了一个比例尺,用来调整坐标与像素的对应关系。具体的,在得到图形属性后,通过以下方式调整图形大小,resize即比例尺:
circle(xc * resize, yc * resize, r1 * resize)
-
调整坐标比例
由于作业数据上限较大,所以默认的窗口大小可能不能显示完整的图形,通过两个按钮scale+,scale-来调整内部的比例尺,以达到图片缩放的效果。每次点击这两个按钮都会改变比例尺。下图是具体的比例尺增大的效果。
11.界面模块与计算模块的对接
在对接部分最重要的是接口的设计,良好的接口设计可以避免过多的操作,从而节省资源,提高性能。本作业的接口声明为
// parameter = file_name
__declspec(dllexport) void readFile(string);
// add from standard string fromat
__declspec(dllexport) void addGeometryObject(string);
// remove by standard string format, strictly equals
__declspec(dllexport) void removeGeometryObject(string);
// trigger calculation
__declspec(dllexport) pair<vector<string>, vector<Point>> getResult();
由于设计接口时有比较好的协商,界面模块与计算模块的对接只需要将数据按照约定的格式调用相关接口即可。当然,在运行前需要把dll文件和h文件添加到项目的包含目录中。生成与导入dll文件的步骤可以参考vs的官方文档。具体的说,导入文件,添加图形都可以经过类似的过程完成,以添加图形为例,大致的对接过程是:
UpdateData(TRUE);//更新输入数据
std::string line;
line = CT2A(m_LineValue.GetString());//由输入获取数据
try
{
addGeometryObject(line);//调用接口
}
catch (const std::exception&)
{
//异常处理,此部分较繁琐,故略去
}
绘图部分需要进行图形解析,由于商议的接口返回的是图形的几何,所以此部分对数据进行了分类和处理,再通过绘图方法绘制图形。主要逻辑是:
stringstream ss("");
string str = result;
ss.clear();
ss << str;
char type;
ss >> type;
switch (type)
{
case 'C': {
setcolor(BLUE);
double xc, yc, r1;
ss >> xc >> yc >> r1;
circle(xc * resize, yc * -resize, r1 * resize);//resize为上文所说的比例尺
break;
//绘图部分以调用API为主,步骤类似,为了博客的可读性其余绘图部分省略了
}
case 'L': {//绘图}
case 'R': {//绘图}
case 'S': {//绘图}
default:{
break;
}
}
12. 描述结对的过程 。
在前期的开发过程中我们是按照Pair Work的方法进行的,即一个人作为驾驶员控制键盘输入,另一个人是领航员,起到领航,提醒的作用。而在后期的测试环节我们通过互相交流,使测试更快速全面。以下是使用腾讯会议进行屏幕共享和交流的截图
GitHub使用截图:
13. 说明结对编程的优点和缺点。同时描述结对的每一个人的优点和缺点在哪里
结对编程
- 优点:
- 两人合作,在开发时可以互相学习,相互传递经验。
- 结对编程在开发时不易出现错误,能提高更高的代码质量。
- 两个人互相监督,提高编程的效率
- 缺点:两个人的磨合很重要,需要时间去习惯对方的风格和设计思路。
我 | 结对伙伴 | |
---|---|---|
优点 | (1)能接受各种方法和设计思路(2)有耐心测试和学习(3) | (1)擅长交流,经常在编程中提出看法(2)擅长整体的设计(3)开发能力熟练 |
缺点 | 开发能力不足,常常陷入困境 | 刚开始时配合的不是很好,有项目结构的重构 |
14. 在你实现完程序之后,在附录提供的PSP表格记录下你在程序的各个模块上实际花费的时间。
见上文表格
模块之间的松耦合
与我们合作小组的是杜林峰 17373067 & 诸子钰 16021142
因为商议好了接口的规格,在替换接口时只需要更改VS的链接库设置即可。运行截图:
在运行中因为异常的处理不同,会出现异常未捕获的情况,在修改catch语句后可正常运行。因为计算部分的结构已经设计好了,所以没有更改核心模块。