[敏捷软工结对项目博客]第二版平面图形求交点
项目 | 内容 |
---|---|
所属课程:北航-2020-春-软件工程 | 博客园班级博客链接 |
作业要求:计算平面图形的公共点个数——新需求 | 结对项目作业要求 |
我在这个课程的目标 | 提升在团队合作中开发“好软件”的能力 |
这个作业在哪些具体方面帮助我 | 根据《构建之法》第四章两人合作的内容,完成结对项目。培养软件工程、团队合作相关的能力。 |
教学班级 | 005 |
项目地址 | https://github.com/Cheibniz/PairProject |
一、PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
· Estimate | · 估计这个任务需要多少时间 | 30 | 30 |
Development | 开发 | ||
· Analysis | · 需求分析 (包括学习新技术) | 180 | 120 |
· Design Spec | · 生成设计文档 | 60 | 90 |
· Design Review | · 设计复审 (和同事审核设计文档) | 20 | 30 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 20 |
· Design | · 具体设计 | 180 | 200 |
· Coding | · 具体编码 | 480 | 400 |
· Code Review | · 代码复审 | 30 | 60 |
· Test | · 测试(自我测试,修改代码,提交修改) | 120 | 120 |
Reporting | 报告 | ||
· Test Report | · 测试报告 | 60 | 40 |
· Size Measurement | · 计算工作量 | 30 | 30 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 30 |
合计 | 1230 | 1170 |
二、软件设计原则
1.information hiding
在维基百科中,有这样一段关于information hiding的描述
In computer science, information hiding is the principle of segregation of the design decisions in a computer program that are most likely to change, thus protecting other parts of the program from extensive modification if the design decision is changed. The protection involves providing a stable interface which protects the remainder of the program from the implementation (the details that are most likely to change).
Written another way, information hiding is the ability to prevent certain aspects of a class or software component from being accessible to its clients, using either programming language features (like private variables) or an explicit exporting policy.
总结一下,细节隐藏是对计算机程序中最容易变化部分的设计方法。在细节隐藏原则下,使用者只调用确定的模块接口,而模块内部的具体实现则被隐藏。类的私有成员等和明确规定的接口都是细节隐藏原则的体现。
面向对象思想本就带有很强的工程气息,细节隐藏原则蕴含其中。在用面向对象的编程语言编程之后,个人就能对细节隐藏思想有所领会。c++
、java
、python
等编程语言都有对类和接口/抽象类的语言特性支持。
在本次结对编程项目中,我们在一些层面的设计中体现了信息隐藏原则
层面 | 体现 |
---|---|
类 | 函数和变量尽可能私有,只将完成类功能的函数公有 |
模块(几何图形容器、文件读取模块等) | 模块具有一定复杂度,实现模块功能的函数和方法全都私有/使用者不可见。只对使用者开放完成模块功能的接口(主要是函数) |
前后端接口 | 将几个函数作为接口,并且函数的设计尽量简单灵活(参数和返回值都使用C++ 标准库中存在的类型,如string 、vector 和pair ) |
2.interface design
本次作业的一大挑战是对前后端接口的设计。前后端各自完成复杂且具有一定规模的任务,如何完成灵活高效的接口是一个需要思考的问题。
首先,使用接口不可避免地使得软件的性能降低,所以在设计接口时需要对性能损失有所容忍。相比起少量的软件性能损失,接口赋予软件的可维护性、鲁棒性等更加重要。
在优秀的API接口设计原则及方法有如下论述
一旦API发生变化,就可能对相关的调用者带来巨大的代价,用户需要排查所有调用的代码,需要调整所有与之相关的部分,这些工作对他们来说都是额外的。如果辛辛苦苦完成这些以后,还发现了相关的bug,那对用户的打击就更大。如果API经常发生变化,用户就会失去对提供方失去信心,从而也会影响目前的业务。
可以看出,在设计接口时需要对接口的灵活性加以考量。在本次作业中,附加作业要求前后端接口被两个组共同使用,我们采用规格化字符串的手段使接口灵活、轻量。
#pragma once
#include <vector>
#include <iostream>
using namespace std;
typedef pair<double, double> InterfacePoint;
// parameter = file_name
__declspec(dllexport) void readFile(string);
// add from standard string fromat of geometry component
__declspec(dllexport) void addGeometryObject(string);
// remove by standard string format of geometry component, strictly equals
__declspec(dllexport) void removeGeometryObject(string);
// trigger calculation
__declspec(dllexport) pair<vector<string>, vector<InterfacePoint>> getResult();
使用接口需要共同的约定,而在本次作业中两小组间存在自然的共同约定:作业要求。作业要求博客中给出几何对象的格式化字符串定义,而格式化字符串定义是绝佳的接口交换数据结构。对于返回值中承载多个对象及课程组未进行点的格式化字符串定义,所以我们使用C++
标准库中存在的类vector
和pair
。
在设计结构时我们发现,对接口的要求不仅有输入和返回值,还有函数的行为和可能抛出的异常。对于此方面的考量在Loose Coupling
中描述。
3.Loose Coupling
在Loose Coupling的WIKI百科中,有这样一段描述
In computing and systems design a loosely coupled system is one in which each of its components has, or makes use of, little or no knowledge of the definitions of other separate components. Subareas include the coupling of classes, interfaces, data, and services.[1] Loose coupling is the opposite of tight coupling.
松耦合是老师同学们时常挂在嘴边的“软件设计”原则,但是这与软件中不同模块之间互相依赖关系有所矛盾。怎样才能做到松耦合呢?我们认为“依赖于抽象”是做到松耦合的好方法。
还是以上文提到的前后端接口为例,对接口的考虑不止有输入输出,还有函数的行为和可能抛出的异常。函数的输入和输出能够显示地包含在函数定义中,那函数的行为和异常怎么办呢?答案就是“依赖于抽象”,关注函数在抽象层面的行为,忽略函数具体地行为。例如__declspec(dllexport) void readFile(string);
,我们只需要期望这个函数被调用后,文件中的几何对象都被读取和处理完成,并不需要考虑文件具体情况对函数行为的影响和调用模块内部是怎样完成几何对象的处理和求解的。
异常实质上也是函数的一种返回值,同样可以在函数声明中写出。对异常的约定与对返回值的约定相似,但异常很多地包含函数运行状态的信息。所以对异常的约定需要更多地对函数行为的约定,实际上增加了模块之间的耦合度。异常同样符合“依赖于抽象”原则,如果模块需要给其他模块抛出异常,那么该模块自身不能处理。这种异常一般是与模块实现细节无关的异常,能够影响甚至中断函数执行。所以只要将接口中的异常约定为针对函数抽象行为的异常,就不会显著地增加软件的耦合度。
三、模块接口的设计与实现过程
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细节,错误局部化。
4.UML
四、性能改进
1.算法层面
暴力求解,算法复杂度为\(O(n^2)\),在短时间内没有改进的空间。
2.实现层面
2000个几何对象,四类几何对象出现概率均等。
- 优化一:从哈希到红黑树。在上个项目中使用的是
unordered_set
,理论上是\(O(1)\)的复杂度,但是实际应用起来结果却惨不忍睹。猜测可能是哈希操作带来的大量主存访问占用大量时间。采用set
之后,虽然容器访问仍是时间占用主体,但是相比哈希占比已经下降很多。 - 优化二:优化正则表达式。在仔细阅读作业要求博客之后,将一些正则表达式中不必要的部分去除,从而略微减小正则表达式匹配所占用的时间。
3.接口问题
在引入前后端接口之后,带来了一定的性能损失。因为现在所采用的接口是两个小组通用的,所以两个小组都为适配接口增加一层适配函数。在不对后端进行重构的情况下,我们很难将这部分性能损失完全抹除。只能在适配时采用内联等措施减小性能损失。
完全消除性能损失也许需要大量的项目经验和相关思考,但遗憾的是我们没能在此次作业中做到。
五、Design by Contract和Code Contract
在Design by Contract的WIKI百科中,有如下描述
It prescribes that software designers should define formal, precise and verifiable interface specifications for software components, which extend the ordinary definition of abstract data types with preconditions, postconditions and invariants. These specifications are referred to as "contracts", in accordance with a conceptual metaphor with the conditions and obligations of business contracts.
承接在二、软件设计原则 中的讨论,接口需要规定如下几个部分:参数、返回值、行为、异常,这是针对函数而言。而契约式设计则在ADT的基础上加入前置条件、后置条件和不变式的约束,让开发者和使用者都有更加形式化、准确及可验证的依据。契约式设计的精准性和形式性为程序验证做好了准备,微软爸爸在.NET平台的基础上提供了相应工具。
在去年的面向对象课程中,我们也曾使用过JML进行契约式编程,所以对契约式编程的感受颇深。
契约式编程的优点有
- 为开发者提供明确的依据,能够具体地指导开发和测试过程。
- 为使用者提供可靠的接口,在不了解模块具体实现的条件下就可以对模块的数据行为有所把握。
- 支持自动验证,与形式化验证相似,给出测试之外的保证软件稳定性的手段。
- 责任分明:出现问题时,能够很快找出开发者还是使用者哪一方有问题,不会有扯皮的情况出现。
而其缺点也是不可忽视的
- 极大的增加契约设计者的工作量:设计一个完备恰当的契约需要耗费设计者很多精力,考虑模块可能被使用的各种情形。
- 需要极其成熟可靠兼容性高的验证工具:对于开发者来说,人工手动验证契约需要花费巨大的代价,甚至对开发者是一种折磨。如果验证工具不能工作,那么开发者将陷入窘迫的境地。
在我们的项目中,最能体现契约式编程的部分是几何对象的构建。几何对象的行为都严格按照其抽象特性设计。现在编写几何对象的方法式多次按照直线、射线、线段、圆的抽象特性进行检查,从而保证几何对象在程序中的行为和数学上的行为相同。
结合对象的数学抽象特性使得其自然具有契约,能够为具体设计提供很好的参考和指导。
六、单元测试
针对不同的功能点和错误点,我们准备了不同的单元测试。
1.编译器报错与警告清零(Code Quality Analysis)
2.单元测试的整体情况如下
测试覆盖率92%
测试用例通过情况
3.具体测试用例(部分展示)
针对几何对象相交功能直接测试
// 射线端点与直线相交
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\n C 0 0 1\nC 3 0 2\nC 2 0 1\nL 0 0 3 0\n" << 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\n C 0 0 1\nC 3 0 2\nC 2 0 1\nL 0 0 3 0\n" << 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\nL 0 0 0 0\n" << 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\nR -1 0 1 0\nS 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\nS 0 0 1 1\nS 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\nS 0 0 1 1\nR 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\nR 1 1 0 0\nR 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());
}
七、异常处理
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());
}
八、界面设计
1.界面模块的详细设计过程
- 界面模块使用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); return; } 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&) { m_EXCEPTMESSAGE = CString(_T("添加图形时出现错误,请重启程序")); UpdateData(FALSE); return; }
-
求解与绘制
通过Solve按钮可以计算图形的交点个数,此功能也是通过调用计算模块实现的。在获得交点个数后反向更新控件变量即可显示数字。
通过Draw按钮可以绘制现有图形的图像,默认是以网格坐标轴为背景显示图形。这部分先从计算模块获取图形信息,再调用相关的图形API完成绘图。为了解决坐标与像素大小转换的问题,在程序中设置了一个比例尺,用来调整坐标与像素的对应关系。具体的,在得到图形属性后,通过以下方式调整图形大小,resize即比例尺:
circle(xc * resize, yc * resize, r1 * resize)
-
调整坐标比例
由于作业数据上限较大,所以默认的窗口大小可能不能显示完整的图形,通过两个按钮scale+,scale-来调整内部的比例尺,以达到图片缩放的效果。每次点击这两个按钮都会改变比例尺。下图是具体的比例尺增大的效果。
九、前后端对接
在对接部分最重要的是接口的设计,良好的接口设计可以避免过多的操作,从而节省资源,提高性能。本作业的接口声明为
// 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;
}
}
十、结对过程
在前期的开发过程中我们是按照Pair Work的方法进行的,即一个人作为驾驶员控制键盘输入,另一个人是领航员,起到领航,提醒的作用。而在后期的测试环节我们通过互相交流,使测试更快速全面。以下是使用腾讯会议屏幕共享和交流的截图。
以下是交换使用github管理项目的截图
十一、结对编程感触
1.结对优缺点
新冠肺炎席卷全世界,大三下学期的开头也在家中度过。往常结对编程是对面的进行,双方能通常快捷的交流;但今年的结对编程是在线上进行,隔着屏幕交流给人不自然的感觉。
在特殊的结对编程条件下,我对结对编程的优缺点有了一定的认识。
优点
- 提高效率:领航员可以给驾驶员压力,持续的处于高负荷状态,项目的进度非常快。
- 减少bug:领航员能在编码之外进行考虑,从而能发现驾驶员无法发现的bug,领航员发现的的bug主要集中于:手误、判断条件错误、冗余代码、记忆不清导致的bug。
- 互相学习:在结对编程的过程中能窥见他人的思维方式,从而帮助自己跳出思维盲区,重新审视往常的编程习惯和准测。不合理的习惯可以改掉,而一直坚守的准测也能更加明晰具体的适用范围。
缺点
- 磨合期带来很大的沟通成本:有时需要停下来用文字辅助才能明白对方确切表达的意思,因为很不熟悉对方的说话方式。
- 不是所有时候都高效:当遇到双方都不会的问题时,结对编程演变成领航员和驾驶员一起用搜索引擎搜索的过程。尤其是遇到编译问题时,更是让人摸不着头脑,效率十分低。
- 同步问题:寻找共同的线上结对编程实践比较困难,会有你等我、我等你的情况出现。
2.我与队友
我 | 队友 | |
---|---|---|
优点 | 开发高效、软件设计能力强、对项目整体节奏有所把控 | 特别积极、思路灵活变通、能学习钻研新方法新技术 |
缺点 | 学习新技术(GUI)时容易急躁 | 软件设计意识不强 |
十二、附加作业 模块之间的松耦合
1.结果展示
与我们合作的小组是 杜林峰 17373067 & 诸子钰 16021142
因为商议好了接口的规格,在替换接口时只需要更改VS的链接库设置即可。运行截图:
在运行中因为异常的处理不同,会出现异常未捕获的情况,在修改catch语句后可正常运行。因为计算部分的结构已经设计好了,所以没有更改核心模块。
2. 合并过程
商议接口
因为双方软件的差别很大,所以需要商议一个接口作为桥梁。我们商议出的接口如下:
#pragma once
#include <vector>
#include <iostream>
using namespace std;
typedef pair<double, double> InterfacePoint;
// parameter = file_name
__declspec(dllexport) void readFile(string);
// add from standard string fromat of geometry component
__declspec(dllexport) void addGeometryObject(string);
// remove by standard string format of geometry component, strictly equals
__declspec(dllexport) void removeGeometryObject(string);
// trigger calculation
__declspec(dllexport) pair<vector<string>, vector<InterfacePoint>> getResult();
适配接口
编写新的模块适配接口,从而使前后端能够汇合。适配接口的代码如下
#include "Interface.h"
#include "read_file.h"
#include "table.h"
Table table;
// 适配读文件接口
void readFile(string inFilePath)
{
ifstream infile = ifstream(inFilePath);
readFromFile(table, infile);
infile.close();
}
// 适配添加几何对象接口
void addGeometryObject(string geometryString)
{
table.insertFromString(geometryString);
}
// 适配删除几何对象接口
void removeGeometryObject(string geometryString)
{
table.eraseFromString(geometryString);
}
// 适配得到最终结果接口
pair<vector<string>, vector<InterfacePoint>> getResult()
{
vector<string> geometries = vector<string>(table.getLineSet().size() + table.getCircleSet().size());
vector<InterfacePoint> points = vector<InterfacePoint>(table.getPointNum());
geometries.clear();
points.clear();
for (auto i : table.getLineSet())
{
geometries.push_back(i->toString());
}
for (auto i : table.getCircleSet())
{
geometries.push_back(i.toString());
}
for (auto i : table.getPointSet())
{
points.push_back(InterfacePoint(i.pointX, i.pointY));
}
return pair<vector<string>, vector<InterfacePoint>>(geometries, points);
}
生成DLL
在接口适配完成后,生成后端DLL库。
两组交换对接
该步骤比较顺利一次完成。