[2020 BUAA 软件工程]结对项目作业
项目 | 内容 |
这个作业属于哪个课程 | 2020春北航计算机学院软件工程(罗杰 任健) |
这个作业的要求在哪里 | 结对项目作业 |
我在这个课程的目标是 | 系统学习软件工程相关知识,培养自己的软件开发能力与团队协作能力,接受一定的实战锻炼 |
这个作业在哪个具体方面帮助我实现目标 | 练习结对项目开发 |
1. 教学班级和可克隆的 Github 项目地址
-
教学班级:006
-
目录结构:
- bin 为核心模块可执行文件所在文件夹
- gui 为界面模块可执行文件所在文件夹
IntersectProject/ ├── bin │ ├── intersect.exe │ └── dllProject.dll ├── gui │ └── UI.exe ├── 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. 在下述 PSP 表格记录下估计将在程序的各个模块的开发上耗费的时间
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
· Estimate | · 估计这个任务需要多少时间 | 10 | |
Development | 开发 | ||
· Analysis | · 需求分析 (包括学习新技术) | 360 | |
· Design Spec | · 生成设计文档 | 30 | |
· Design Review | · 设计复审 (和同事审核设计文档) | 20 | |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | |
· Design | · 具体设计 | 20 | |
· Coding | · 具体编码 | 240 | |
· Code Review | · 代码复审 | 30 | |
· Test | · 测试(自我测试,修改代码,提交修改) | 120 | |
Reporting | 报告 | ||
· Test Report | · 测试报告 | 30 | |
· Size Measurement | · 计算工作量 | 5 | |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 15 | |
合计 | 880 |
3. 说明在结对编程中是如何利用教科书和其它资料中的方法对接口进行设计的
Information Hiding
1972年,David Parnas 发表了一篇题为《论将系统分解为模块的准则》的论文首次让公众注意到信息隐藏这一概念。
信息隐藏式结构程序设计与面向对象设计的基础之一。在面向对象设计中,它又引出了封装和模块化的概念,并与抽象的概念紧密相关。
信息隐藏是软件的首要技术使命中格外重要的一种启发式方法,因为它强调的就是隐藏复杂度。
——引用自 CSDN博客
封装包含两层含义:
- 将抽象得到的有关数据和操作代码相结合,形成一个有机的整体,对象之间相对独立,互不干扰。
- 我们改进了第一次作业中关于交点计算的内容,让各个类相互独立,每个类或方法都只有一个明确的指。
- 将对象封闭保护起来,对象中某些部分对外隐蔽,隐藏内部的实现细节,只留下一些接口接收外界的消息,与外界联系 。
- 对外只暴露输入输出功能和交点计算功能。
- 将输入输出控制功能封装到
IOHandler
类。 - 将项目的核心功能交点计算封装到
Container
类中。
Interface Design
Single Responsibility Principle
实际使用时,类很难做到职责单一,但是接口的职责应该尽量单一。
Dependence Inversion Principle
高层模块不应该依赖低层模块,两者都应该依赖其抽象。
Interface Segregation Principle
建立单一接口,不要建立臃肿庞大的接口。 接口要尽量小。接口要高内聚。
——引用自 博客园博客
IOHandler
类设计四个接口readNum()
——读入一个范围在((-100000,100000))的整数readLine()
——读入几何对象数量readGraphType()
——读入一个表示几何对象类型的字符outputInt()
——输出一个整数
Container
类设计以下接口:void AddGraph(char type,int x1, int y1, int x2, int y2)
——添加几何图形void IntersectCalculate(Graph* g1, Graph* g2)
——交点计算int Size()
——获取交点个数vector<Graph*>* Getgraphs()
——获取图形集合set<Dot>* GetDots()
——获取交点集合Graph* DeleteGraph(char type, int x1, int y1, int x2, int y2)
——删除图形
Loose Coupling
Definition - What does Loose Coupling mean?
Loose coupling describes a coupling technique in which two or more hardware and software components are attached or linked together to provide two services that are not dependent on one another. This term is used to describe the degree and intent of interconnected but non-dependent components within an information system.
Loose coupling is also known as low or weak coupling.
——引用自 techopedia
- 除了
Contaner
这个核心功能类的代码行数达到了 200 多行,其余每个类的行数都控制在 100 行以内。 Container
类只需知道直线的表示方法是 (ax+by+c=0),它也只能使用一定的接口获得 (a),(b),(c) 三个参数。对于其他的实现细节,我们可以忽略。IOHandler
类同理,只对外显示必要的接口。- 但是对于异常,我们将异常全部暴露出来,并没有将其封装成一个单独的“异常”类。如果某个异常的定义或者功能发生了变化,我们需要修改所有使用到该异常的地方。这是我们在设计时考虑不周的一个地方。
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 图显示计算模块部分各个实体之间的关系
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. Design by Contract,Code Contract
契约式编程
Bertrand Meyer 等人于 20 世纪 80 年代后期发明了 Eiffel 语言。
在这种语言里,他首次提出了 Design by Contract——契约式设计的思想,这也是软件工程界对“无bug程序”最激进的尝试。
契约式设计要求程序给出并遵守三种契约:
- 先决条件
- 先决条件或先验条件指在执行一段代码前必须成立的条件。
- 对函数参数和调用函数时的上下文的约束。
- 后置条件
- 后置条件指在执行一段代码后必须成立的条件或谓词。
- 函数的返回值和退出函数时的上下文的约束。
- 不变条件
- 在程序执行过程或部分过程中,可始终被假定成立的条件。
优点
- 能够避免自然语言的二义性,它更加严谨,功能描述更加确切。它既帮助自己实现要求的功能,同时帮助他人理解该段代码的作用。
- 对团队协作、大型软件开发、严格功能定义项目帮助很大。
- 契约的重要性并不在于如何达到最好的设计效果,最好的性能要求,而是在于最正确的写法,最明确的功能定义。
缺点
- 契约的撰写实际上是比编写代码更加艰难的过程。
- 撰写者需要确切理解需求,覆盖所有情况。
- 当功能非常复杂时,规格也会非常复杂,这使得契约反而难以理解,这时候反而应该以自然语言为主,契约为辅。
- 自动化测试软件的部署非常困难。
项目实现
在我们的项目中,由于本次作业的内容较为统一,每个函数的输入输出都相对种类单一,加上时间紧迫,我们讨论后没有采用契约式设计。
但是相关代码中包含了一定契约式设计的思想,这一部分主要是由我的队友实现的。对于每个暴露出来的接口,他都写了接口文档,说明了每个模块的作用、参数、返回值和期望的异常,也许在一个侧面反映了契约式设计的一些特点。
/*
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();
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))的射线,在(xin (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 所使用。
在 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(' ').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
)读取,末尾添加一个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. 结对的过程
由于现在不具备面对面实施结对编程的条件,加上两人的时间安排不好统一,我们更多是采用微信交流、pull request 和 issue 等方式,Live Share 或者屏幕共享比较少。
就这次结对的过程而言,不知道是我的网络问题还是 Live Share 本身的问题,其连接并不稳定,有一定延迟。
微信交流是最频繁的方式,毕竟最熟悉也最方便。
pull request 和 issue 也能准确地标出需要修改的地方。
13. 说明结对编程的优点和缺点,同时描述结对的每一个人的优点和缺点在哪里
从这次实际体验来说,我更多担任驾驶员的角色,我的队友更多是一个领航员。我们属于交替分工,交替复审。我主要负责核心功能的设计、封装和测试,队友主要负责 IO 处理、异常和 UI。在项目规范的设计上,主要是由他用“断言”的方式来制定规范。这次结对的过程并没有出现冲突、争吵等问题,双方的相处还是很和谐的。
结对编程
优点
- 代码复审
- 保障了代码质量,规范代码标准,同时帮助双方理解代码,为自己的设计提供便利。
- 保障了测试质量,及时检查出 Bug,不造成更重的修改负担。
- 责任感
- 不希望对对方产生影响,更想把自己负责的部分写好。
- 问题交流
- 出现问题时不再是一人挣扎解决,有队友帮助。
缺点
- 沟通成本
- 由于不是单打独斗,双方需要及时的交流与沟通,规范设计、定义接口、规划进度,这都造成了一定的负担。
- 系统问题
- 双方的编程环境存在差异,可能造成一定的问题复现障碍和运行障碍。
- 时间问题
- 不具备面对面实施结对编程的条件,两人的时间安排不好统一。
结对伙伴
优点
- 我
- 善于沟通;
- 代码简洁思路清晰;
- 做事效率较高。
- 伙伴
- 结对伙伴复审非常仔细,对测试质量把控很高,这帮助我检查出很多潜在的问题。
- 交流中的领航作用很明显,为项目定义了很多设计要求。
- 学习能力和编码能力强,解决了很多问题。
缺点
-
我
- 在测试时对一些特殊情况考虑不充分。
-
伙伴
- 前期沟通不是非常充分。
-
共同
- 在预先规划上做得不好,导致在项目进行中经常返工和修修补补。
14. 实现完程序之后,在PSP表格记录下你在程序的各个模块上实际花费的时间
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
· Estimate | · 估计这个任务需要多少时间 | 10 | 10 |
Development | 开发 | ||
· Analysis | · 需求分析 (包括学习新技术) | 360 | 300 |
· Design Spec | · 生成设计文档 | 30 | 20 |
· Design Review | · 设计复审 (和同事审核设计文档) | 20 | 10 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 10 |
· Design | · 具体设计 | 20 | 20 |
· Coding | · 具体编码 | 240 | 300 |
· Code Review | · 代码复审 | 30 | 30 |
· Test | · 测试(自我测试,修改代码,提交修改) | 120 | 240 |
Reporting | 报告 | ||
· Test Report | · 测试报告 | 30 | 30 |
· Size Measurement | · 计算工作量 | 5 | 5 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 15 | 15 |
合计 | 880 | 985 |
分析
- 总时间相差了 2 个小时。
- 需求分析 (包括学习新技术)
- 我原本对于学习
UI
的占时估计很大,这里主要是结对伙伴的帮助,他对UI
的贡献非常大,所以缩短了这一项的时间
- 我原本对于学习
- 具体编码
- 这里主要是有吃了设计上的亏,伙伴间没有很好地设计整个项目,预先设定好所有的接口,导致一些地方的不断返工。
- 测试(自我测试,修改代码,提交修改)
- 这里我认为是结对伙伴的功劳,他给我提了很多测试意见,让我反复修改单元测试。
- 除此之外,我还遇到了一些 VS 的玄学 Bug,导致 dll 的封装和调用一直不成功,白白浪费了很多时间。
这次的总结就是——设计先行。在团队协作中,严格的项目设计能够为开发指明清晰的工作方向,节省大量的时间。