结对项目:求交点pro
[2020 BUAA 软件工程]结对项目作业
项目 | 内容 |
---|---|
课程:北航2020春软件工程 | 博客园班级博客 |
作业:阅读并撰写博客回答问题 | 结对项目作业 |
我在这个课程的目标是 | 积累两人结对编程过程中的经验 |
这个作业在哪个具体方面帮助我实现目标 | 体验两人结对编程过程,努力提高沟通效率和代码匹配度 |
1. 项目地址
-
教学班级: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
2. 时间规划
预估耗时
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 |
3. 接口设计
信息隐藏
改进了计算几何对象交点的程序,将其封装到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个必要的接口。
但是对于异常,我是实现将所有的异常都给出,并没有将其封装成一个“异常”类。如果某个异常的含义发生了变化,那么对应的,需要修改所有使用到该异常的地方。这确实是软件在仓促设计时候的一个小不足。
4. 计算模块接口的设计与实现过程
解题思路
几何形状表示方法
根据题目所给定的信息,直线、射线和线段均由线上两点表示。
在求交点时,为方便计算,所有几何形状统一表示成直线,在求出交点后再判断是否满足射线、线段的限定条件。
为方便表示所有类型的直线,同时便于判断平行和求交点,采用直线的一般式方程来描述:
这里,\(A,\ B,\ C\) 三个量的计算方法为:
- \(A=y_2-y_1\)
- \(B=x_1-x_2\)
- \(C=x_2*y_1-x_1*y_2\)
在这种情况下,两直线平行时:
交点计算
在用直线的一般式方程来描述时,两直线交点坐标:
如果两形状中存在射线,则判断射线的端点与交点的位置关系,交点是否在端点沿射线方向的一侧。
如果两形状中存在线段,则判断线段的两端点与交点的位置关系,交点是否在两端点之间。
判断方法
目前没有想到好的判断方法,所使用的就是暴力枚举,每加入一个新几何形状,将其与集合中每一个元素求交点。
复杂度
因为是暴力枚举,复杂度仅能达到 \(O(n^2)\)。
设计实现过程
交点类 Dot
交点类通过继承 pair<double, double>
实现,操作比较简单。
为了判断重复交点,需要重写 equals
函数。
class Dot : public pair<double, double>;
其中考虑到浮点数的精度问题,定义了一个宏函数 DoubleEquals
。
#define DoubleEquals(a, b) (fabs((a) - (b)) < 1e-10)
图形类
Graph
所有几何形状的共同父类,由于统一用直线的一般式表示,定义获取三个参数的虚函数。
class Graph {
public:
virtual double GetA() = 0;
virtual double GetB() = 0;
virtual double GetC() = 0;
};
Line
通过继承 Graph
实现。
class Line : public Graph {
private:
double A, B, C;
};
Radial
通过继承 Line
实现。
class Radial : public Line {
private:
Dot* end_point;
Dot* cross_point;
};
Segment
通过继承 Line
实现。
class Segment : public Line {
private:
Dot* end_point1;
Dot* end_point2;
};
交点容器类 Container
用于存储图形,存储并除去重合的交点。
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 | 删除一个几何图形 | 图形类型,两点坐标 | 删除的图形 |
5. UML
由于计算的核心模块并非本人所写,故引用合作伙伴张xz同学的素材
Dot
继承了pair<double, double>
Line
继承了Graph
Radial
和Segment
继承了Line
Radial
和Segment
关联了Dot
Container
关联了Graph
和Dot
6. 计算模块接口部分的性能改进
对一组10000个随机几何对象共约250000个交点的运行结果如下
其中,我们关注到,耗时最长的单体方法是 AddGraph
,但是考虑到其中很大部分是因为调用了 IntersectCalculate
方法,计算两个几何对象之间的交点,所以分析该方法。
这个函数有三个地方耗时可能比较多:
- 计算交点的浮点数计算模块,涉及大量的浮点计算;
- 对于很多特殊情况的判断,尤其是需要考虑到线段和射线共线的几种情况,做了非常多的if-else判断,在运行时会增加很多跳转;
- 因为AddDot方法使用了内置的 set 容器,使用红黑树作为存储结构,在添加一个点时,需要判断多次的 \(<\) 关系,其时间复杂度为 \(O(logn)\)。
可能的解决办法为使用 <unorder_map>
来存储交点,将时间复杂度将为 O(1)
。
7. 契约式设计和代码协定
这种方法要求软件设计者为软件组件定义正式的,精确的并且可验证的接口。分为前置条件、后置条件和类不变项三大部分。
- 优点是程序员对于代码有着全面的思考和把控,能够确定每一个代码块能够在任何条件下实现它的功能,拥有很好的鲁棒性;同时对每个模块都明确约定好的输入和输出,编码的逻辑相对比较简单
- 缺点是工作效率太低。首先在设计阶段并不能预知许多实际遇到的问题,在实际开发过程中也许会被迫修改契约;另外,如果要对不同的输入条件做各种异常处理,代码无疑十分臃肿,开发和运行效率可能较低
在我写的那部分代码中,对于后置条件有一定的实现。每个方法都考虑到部分输入可能的情况,在遇到问题时,它能够抛出指定的异常,比如文件不存在或者两个几何对象有无穷个交点。但是对于这方面的问题,双方事先并没有进行沟通,没有对每一个模块都设定规范的输入和输出。同时因为考虑到本次作业的数据流较为单一,每个函数的输入输出都相对种类单一,时间紧迫,讨论后没有没有采用契约式设计。
但是对于每个暴露出来的接口,我都写了接口文档,说明了每个模块的作用、参数、返回值和期望的异常,也许在一个侧面反映了契约式设计的一些特点。
//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);
8. 计算模块部分单元测试展示
消除 Code Quality Analysis 中的所有警告
此处我们也学习到了很多细节知识,比如在封装 dll 时使用的 fstream
和vector
需要写成指针,以防止不同平台上模板不同导致的问题。
单元测试
覆盖率
扩展计算交点功能
TestDot
测试 \(Dot\) 类的 5 个函数
- 构造函数
Dot(double, double)
- 拷贝构造
Dot(const Dot&)
- 获取坐标
double GetX()
,double GetY()
- 相等判断
bool equals(Dot b)
其中测试重点在相等判断上,需要修正 \(double\) 的精度误差,例如
- \(77940 / 11959+1.11111111- 1.11111111\)
- \(78005 / 11969 +0.33333333- 0.33333333\)
- \(16451 / 16447 + 0.99999999 - 0.99999999\)
- \(48119 / 48121 + 0.000000003 - 0.000000003\)
Test Line, Radial, Segment
测试图形类的函数
- 构造函数
- 获取参数
- 获取端点
这方面比较简单,无需赘述。
TestSolve
测试交点的平行、计算与位置关系,包括
- 一般情况
- 平行
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);
……
9. 计算模块部分异常处理说明
输入不是整数的异常
为了防止输入小数或者异常字符的情况
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或大于等于100000的异常
为了防止输入的整数超过\((-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
异常。
输入同一图形时两个点重合的异常
为了防止描述几何对象时,点<x1,y1>和点<x2,y2>为同一个点的情况
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
异常.
10. 界面模块的详细设计过程
首先发现一个问题:
- VS 封装的 dll 文件和 Qt 并不兼容,不能够直接导入使用.
- 需要在 Qt 内新建一个c++ library项目,将所有的 .h 和 .cpp 源文件导入并编译成 dll 和 lib 文件,才能够在 UI 模块被 Qt 所使用。
可执行文件路径/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
绘制线段来表示射线。
11. 界面模块与计算模块的对接
由于 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 和他们的界面模块交换测试运行结果如下:
12. 结对的过程
在结对过程中,使用了一段时间的屏幕共享,但是由于两人微小地域时差和作息不一致,并没有全程使用。
还使用了github上的issue和pull request来管理仓库
13. 结对编程感想
感想
自我感觉,在编写核心模块中,自己更像一个领航员。在这次的作业中,核心功能的设计和编码并不是我完成的,自己只是写了IO部分、异常部分和修复了核心功能的一些bug;但是在项目规范的设计和博客的分工方面,一直都是我用了“断言”的方式来制定规范的,所以感觉自己更像一个领航员。
在编写UI模块中,自己是一个实践者。从零学习、搭建Qt环境、设计UI、接入核心计算模块,都是我一个人全权负责,伙伴则修改模块接口的不足和缺陷。
但是我也反思,自己这两个星期以来,对于任务进度的把控并不是很好,没有明确任务每一个进度的规划时间,而是做到哪算到哪,导致任务提交前十分匆忙;第二是,对于结对伙伴不够信任,常常会心理担心代码中是不是有bug,会被查出然后扣分。
在这次的体验中,我感受到的结对编程的一些优缺点:
优点:
- 代码复审的机制能够帮助双方少写bug,提供更好的代码质量和测试质量
- 双人合作的模式,对于双方都更有动力,更加富有责任感,愿意把自己的事情尽快做完不拖累进度
缺点
- 额外多了很多沟通的成本,包括规范的设计和遇到一个问题之后双方如何承担责任并修复
- 系统环境一致性问题,由于双方项目的目录结构不同,开发初期双方在文件路径上出现了分歧
- 工作时间匹配问题,由于地域时差和作息重合时间少,故屏幕共享直播编程的机会并不会特别多,很多交流是通过微信、pull request或者issue等异步的方式解决
成员评价
我:push力十足,督促着项目的进展;乐于奉献自己的时间,完成更多的任务;愿意探索新事物新技术新方法,但是做事效率比较低,遇到困难时心态容易急躁
伙伴:做事效率比较高;善于沟通;代码简洁思路清晰,但是有些特殊条件的考虑不够充分,对于细节不够重视
14. 时间记录
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相关的插件,相信效果会好很多。
这次结对过程,本人就是很吃了设计上的亏,在具体编码过程中,遇到了不少意料之外的问题,急急忙忙去寻求解决方案。因此,我得到的教训最重要的就是,必须事先在设计阶段就将大部分的问题发现并解决。这个过程需要合作伙伴一起头脑风暴,利用白板等工具画出依赖关系、业务流程等,然后试图发现其中的问题,并且对于已经发现的问题找到可能的解决方案,未雨绸缪。