结对——软工第一次结对项目

1.在文章开头给出教学班级和可克隆的 Github 项目地址

项目 内容
这个作业属于哪个课程 2020春季计算机学院软件工程(罗杰 任健)
这个作业的要求在哪里 结对项目作业
教学班级 005
github项目地址 https://github.com/Lebway/IntersectPairWork.git

2.在开始实现程序之前,在下述 PSP 表格记录下你估计将在程序的各个模块的开发上耗费的时间 14.你实现完程序之后,在附录提供的PSP表格记录下你在程序的各个模块上实际花费的时间

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划
· Estimate · 估计这个任务需要多少时间 10 20
Development 开发
· Analysis · 需求分析 (包括学习新技术) 400 500
· Design Spec · 生成设计文档 100 100
· Design Review · 设计复审 (和同事审核设计文档) 50 60
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 30 30
· Design · 具体设计 100 120
· Coding · 具体编码 360 500
· Code Review · 代码复审 50 100
· Test · 测试(自我测试,修改代码,提交修改) 300 450+50=500
Reporting 报告
· Test Report · 测试报告 20 30
· Size Measurement · 计算工作量 5 5
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 20 30
合计 1445 1995

3.看教科书和其它资料中关于 Information Hiding,Interface Design,Loose Coupling 的章节,说明你们在结对编程中是如何利用这些方法对接口进行设计的

Information Hiding

其它资料定义:在设计和确定模块时,使得一个模块内包含的特定信息(过程或数据),对于不需要这些信息的其他模块来说,是不可访问的。

具体实施:

  • 类中所有数据成员都要是private,所有的访问都需要通过接口访问,使外部类无法操作内部属性。比如在编写计算模块中的类的时候,图形与图形之间,图形与计算交点之间,都是不可见对方的内部属性的,如果想访问某个内部变量,必须通过.get来进行获取。
  • 将所使用的数字定义为num等常量,则可以隐藏数字的具体数值,且如果需要改变常量值,仅需要在定义处进行改动。在判断浮点误差的时候,我们需要很多特别小的值,所以为了方便,我们将其设定为eps,从而在改动时,或者是在数字不可见上有了优化。

Interface Design

书中步骤包含:通过读取用户的输入,以及创造和改进交互的媒介(输入输出设备上的文字、图像、声音、振动等)帮助用户进行交互。对数据的展现方式进行设计,确定图表、行列的大小;行列边界的颜色;各种参数的呈现方式;对于关键数据,是否采用特殊方式显示等。相当于生命周期的具体实现阶段。

  • 接口设计要简洁,而不应该在接口中包含过多的方法。比如在设计GUI和计算模块接口的时候,我们最初将类的所有函数全部开放在接口中,但后来发现这样做是不安全的,因为很容易在运行中篡改成某些错误的值。
  • 接口封装合理,而不是是想到什么加什么。对于计算模块本身,我们可以将各个图形类的public部分视为对外开放的接口,而计算模块通过这些接口来获取所存储图形的相关信息。而过多的冗余函数会导致使用者很困惑,比如可以通过一个vector得到所有的数据而不需要分别存放的时候,可以较好的减少代码维护困难程度。

Loose Coupling

其它资料定义:藕合度是度量一个代码单元在使用时与其他单元的关系。最理想,最松散的耦合,是一个单元无需其他代码单元特别的配合而可以使用。松藕合一般与高内聚相关。具备高内聚性的代码单元,一般都比较松藕合。

  • 耦合与否需要设计合理,但如果为了耦合性放弃代码的可读性,这样也是无必要的。比如结对编程中,我们将不同的几何图形设计为不同的类,从而避免了混杂的单独一个图形类,即保证了可读性,也保证了耦合度低,修改一个类时,与其它类无关。
  • 类只做自己范围内的职责,比如设计计算交点的模块,则仅需要计算交点,而设计用来GUI的模块,仅需要对图形进行处理,两者所需要交互的部分只有数据流,从而导致某一个模块需要修改,则只需要改动自己模块且保证接受数据接口不变即可。

对于这三种的设计,在不同问题的环境下并不是一成不变的,比如对于隐藏信息一点来说,我们将需要使用的类变量全部封装成数据流进行传输,这样就导致了原本设计好的类被拆开,在可读性上变差,或者说对于某些需要约束的需求来说,过低的耦合度有时会带来需求改动较大时,两者之间交互更加复杂。

4.计算模块接口的设计与实现过程。设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图?说明你的算法的关键(不必列出源代码),以及独到之处

计算模块接口的设计与实现过程 设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图?说明你的算法的关键(不必列出源代码),以及独到之处

在结对的开始,结对的两位同学根据各自的个人项目作业对本项目的计算模块设计进行了统一:核心部分为4个类,包含圆和线两个部分,其中射线、线段等均是线的子类。各个类具有和其他对象的交点计算函数。

实现流程

  • 排除平行线或重合线:不同于上次作业,我们不能直接排除对于两条平行的线(射线、线段),因为两条共线的射线也可能存在交点。因此对于共线的线,我们需要进行额外的判断。 (画图,两条射线相交)
  • 排除平行线后,我们面对的问题便是一般问题,为了简化逻辑,本次项目的实现逻辑基本参照上次作业,即射线、线段间的交点也使用直线的交点计算公式。在算出交点后,再判断该交点是否能够是可行的,并舍弃非法点。在设计的过程中,我们也考虑过对于不能相交的直线和线段进行额外的判断来减少交点计算的运算量。但是考虑到判断的逻辑比较复杂、容易出错,我们最终还是采用了先算出所有交点,再舍弃非法点的方式。

独到之处

  • 在计算圆和圆的交点的过程中,我们将圆的一般公式输入MATLAB,并实现了MATLAB的计算流程。其好处在于能够保证程序的正确性和效率。
  • 为了方便计算,我们储存了射线的方向。
  • 为了降低计算量,我们存储了很多中间变量。
    • 如在圆相关的计算中,常常会用到圆半径的平方,我们便选择把半径的平方存为圆的参数;
    • 又如在判断点是否可行的过程中,涉及到很多比较操作,这些操作需要进行+-eps的操作,我们选择直接将参数+-eps存为参数,减少了运算耗费。
    • 又如圆的计算较为复杂,在观察了计算式后我们发现:运算过程中有很多重复的计算步骤,我们同样采取了存储中间计算结果的方式来减少这些运算步骤。

5.画出 UML 图显示计算模块部分各个实体之间的关系(画一个图即可)

6.计算模块接口部分的性能改进。记录在改进计算模块性能上所花费的时间,描述你改进的思路,并展示一张性能分析图(由VS 2015/2017的性能分析工具自动生成),并展示你程序中消耗最大的函数

从性能分析图中,可以看出,主要性能消耗在于使用vector存储交点之后,通过sort进行去重来得到交点个数。同时,经过多次测试,发现对于数据量的增加,这部分的性能消耗一直占据主要部分,那么我们应该从这部分入手进行优化测试,同时进行思考其他部分是否可以继续减少时间。

hash映射

考虑到主要占用时间是vector存储交点后去重的问题,那么一个很简单的想法就是不集体去重,而是每次插入直接判断是否重复,若重复则不插入,这里的查找复杂度为常数级,所以我们认为可能会减少这部分运行的时间。

但经过对比实验之后,发现如果数据量较大时,hash中的冲突维护需要更多的时间,那么就导致了速度越来越慢,复杂度退化为log级乃至线性级。经过统计,发现在大量数据时两者差距并不是很大。

Bentley-Ottmann算法

本次作业中,比上次增加了线段部分的计算,那么很自然的想到扫描线算法。

而在上次作业的博客中已经分析过本算法,本次将其搬运过来。

这个算法大致的思路是:

如果一个线段和另外一个线段有交点,那么用一条直线从上向下扫,可知有交点的线段一定是相邻的,如图所示:

可以看出,bc交点之上,bc线段相邻。而有bd线段相交但不相邻的情况,这种情况下,当直线扫过bc交点,达到bd交点上时,可以知道bd是相邻的,所以通过这样的算法,就可以维护两个数据结构,得到交点个数:一条链表,用来记录所有线段的端点和已经找到的交点,每个点按y的递减顺序存储,若y相同,则按x递增顺序存储;一棵二叉树,负责记录与扫描线相交的线段,每条线段按照上端点的x坐标递增顺序存储。

如此,可以通过扫描线得到交点个数,算法时间复杂度为(O((n+k)logn)),n为线段数量,k为焦点数量。

分组去重

由于使用sort并去重,其时间复杂度是(O(nlogn)),那么经过思考,可以通过线线分组,线圆分组等,进行局部去重之后再整体去重。

后来发现,这种做法在分组内部交点重复较多的时候会比较好用,但事实上数据不会像设定的这样,而且书写起来不美观,所以暂时放弃了这种做法。

结构体和类

本次代码使用了类进行几何图形的存储,实现了比较好的封装,同样的,使用结构体也是可以进行比较好的封装的。

但这两者的一个问题就是,封装的比较好,必然带来了速度的下降,所以如果使用数组分散存储,会带来速度的提升这点是必然的,但是因为会导致数据维护难度下降、模块耦合度增高等负面影响,所以暂时放弃了这种做法。

7.看 Design by Contract,Code Contract 的内容,描述这些做法的优缺点,说明你是如何把它们融入结对作业中的

Design by Contract

定义为:它规定了软件设计人员应该为软件组件定义正式的、精确的和可验证的接口规范,这些规范扩展了带有前置条件、后置条件和不变量的抽象数据类型的普通定义。

Code Contract

定义为:契约的形式有先决条件、后置条件和对象不变量。契约充当外部和内部api的检查文档。契约用于通过运行时检查、启用静态契约验证和文档生成来改进测试。

优点:

  • 更优秀的设计
  • 更高的可靠性
  • 更好的文档
  • 简化调试
  • 支持复用

缺点:

  • 书写代码可能较臃肿
  • 需要大量的实践才能很好的掌握
  • 契约的撰写成本较高
  • 并发程序和分布式程序不太适用

在本次作业中,我个人认为契约编程的强制约束思想在错误处理部分体现的很好。对于命令行,需要约定指令是固定的格式,从而可以进行自动化测试,对于输入的几何图形,需要进行格式的约束,比如直线需要两点,圆需要圆心和半径,而同时也要约束数据的大小与个数等,来保证这种约定的执行。

错误处理代表的是一种直接在代码中体现出的契约,是显式的,而其实还有一部分是在设计思路上体现出的契约编程,这部分编程就需要更加谨慎,将其在代码编写之前就约定好。比如在编写计算模块和GUI之间交互的部分时,我和队友就将输入输出数据全部以数据流的格式传输,而非使用使两者更加耦合的类模块。同时我们在设计代码初期也约定了固定的接口函数,从而在不暴露信息的环境下最大限度实现所要完成的任务。

而在编写代码时,由于 提前设计好了相关功能函数,所以在最后进行对接模块的时候十分的方便。

8.计算模块部分单元测试展示。展示出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路。并将单元测试得到的测试覆盖率截图,发表在博客中。要求总体覆盖率到 90% 以上,否则单元测试部分视作无效

计算模块部分单元测试展示 展示出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路。并将单元测试得到的测试覆盖率截图,发表在博客中。要求总体覆盖率到 90% 以上,否则单元测试部分视作无效

单元测试的思路:

  • 单元测试的目的是进行代码覆盖性检查,以防止代码功能性错误。
  • 我们在单元测试中主要考虑了以下情况:
    • 不同几何图形之间的交点:四种几何图形,两两相交,共有12种情况;
    • 不同几何图像之间的位置情况:
      • 包括线之间的相交、相离、重合等情况;
      • 圆之间的相交、相切、相离等;
      • 线段、射线中存在的“共线,但有一点相交”的情况;
    • 错误处理:对于重合的圆、有无穷交点的线,我们需要返回错误类型;
    • 精度问题:由于我们使用的是double类型,在等于等比较判断时,需要考虑eps的影响。精度问题也是我们需要在单元测试中考虑到的。

消除 Code Quality Analysis 中的所有警告:

9.计算模块部分异常处理说明。在博客中详细介绍每种异常的设计目标。每种异常都要选择一个单元测试样例发布在博客中,并指明错误对应的场景

按照程序设计的思路,我们将需要处理的错误分为如下几个:

enum class ErrorType {
	NoError, CommandNum, CommandFormat, FileNotExist,
	EmptyFile, NumOutOfRange, AssertNum,
	FigureOutOfRange, LinePointOverlap, FigureFormat, SameLine, SameCircle,
};

命令行输入参数过少

这一点是需要对输入的命令行指令进行检测,所以基于定义的枚举,设计了如下单元测试:

int argc = 4;
char* argv[5];
argv[1] = "-i";
argv[2] = "input.txt";
argv[3] = "-o";
argv[4] = "output.txt";
ErrorType errortype = CommandError(argc, argv);
Assert::AreEqual(int(errortype), int(ErrorType::CommandNum));

命令行参数格式不对

同样的,这时命令行的数量正确,但是所输入的指令可能不符合本次作业的要求,设计了如下单元测试:

int argc = 5;
char* argv[5];
argv[1] = "";
argv[2] = "input.txt";
argv[3] = "";
argv[4] = "output.txt";
ErrorType errortype = CommandError(argc, argv);
Assert::AreEqual(int(errortype), int(ErrorType::CommandFormat));

输入文件不存在

虽然命令行的参数可能是对的,但是所需要输入的文件如果在当前路径下不存在的话,同样会导致程序错误,设计了如下单元测试:

int argc = 5;
char* argv[5];
argv[1] = "-i";
argv[2] = "notexistfile.txt";
argv[3] = "-o";
argv[4] = "output.txt";
ErrorType errortype = CommandError(argc, argv);
Assert::AreEqual(int(errortype), int(ErrorType::FileNotExist));

输入文件为空

如果输入文件存在,但是文件为空,那么有可能导致程序无法接收到任何信息而无限卡在输入阶段,设计了如下单元测试:

std::ifstream file;
file.open("empty.txt");
ErrorType errortype = EmptyFileError(file);
Assert::AreEqual(int(errortype), int(ErrorType::EmptyFile));

输入n不为数字

对于输入部分的检测全部结束,那么接下来该对其输入的内容进行检测了,这里需要判断读入的n是否是一个数字,设计了如下单元测试:

std::string n = "s";
ErrorType errortype = NumError(n);
Assert::AreEqual(int(errortype), int(ErrorType::AssertNum));

输入n超过范围

根据题目中定义可知,n的范围为[1, 500000],这样,就需要检测n超过边界的情况了,设计了如下单元测试:

std::string n = "-1";
ErrorType errortype = NumError(n);
Assert::AreEqual(int(errortype), int(ErrorType::NumOutOfRange));

输入图形格式错误

对于本题目来说,线类的图形需要L/S/R x1 y1 x2 y2这样的格式进行输入,圆类的图形需要C x y r这样的格式进行输入,所以需要对图形输入的格式进行相关的规范化,设计了如下单元测试:

std::string figure = "";
CorrectFigure correctfigure = FigureError(figure);
Assert::AreEqual(int(correctfigure.errortype), int(ErrorType::FigureFormat));

输入图形参数超过范围

对于本题目来说,除了圆半径之外的参数范围为[-100000, 100000],圆半径的范围为[1, 100000],那么就势必要进行数字是否超过范围的判断,设计了如下单元测试:

long long n = 5;
int low = 1;
int high = 2;
ErrorType errortype = BondError(n, low, high);
Assert::AreEqual(int(errortype), int(ErrorType::FigureOutOfRange));

输入的直线两点重合

对于本题目来说,直线需要两个不同的点去构造,所以两个端点不可以重合,设计了如下单元测试:

long long a = 1;
long long b = 1;
long long c = 1;
long long d = 1;
ErrorType errortype = OverlapError(a, b, c, d);
Assert::AreEqual(int(errortype), int(ErrorType::LinePointOverlap));

输入直线重合

vector<struct Position> res;
Line line1(1, 1, 1, 0);
Line line2(1, 2, 1, 3);
ErrorType err = line2.lineIntersect(line1, res);
Assert::IsTrue(err == ErrorType::SameLine);

输入圆重合

vector<struct Position> res;
Circle c1(0, 0, 5);
Circle c2(0, 0, 5);
ErrorType err = c1.circleIntersect(c2, res);
Assert::IsTrue(err == ErrorType::SameCircle);

对于如上所有单元测试部分,均进行了反向测试,即对其命令的正确形式也进行了相关测试,以保证代码覆盖率。

10.界面模块的详细设计过程。在博客中详细介绍界面模块是如何设计的,并写一些必要的代码说明解释实现过程

使用了qt creator进行界面的编写,界面如下:

其中包含了一个Widget,两个ListWidget,两个Linetext,四个Push Button。

Widget

这个是画图界面,我们采用的是QCustomerPlot这个库,所以需要在工程的ui界面拖动处将其“提升”:

这样原本使用QCustomerPlot库的时候,replot之后不需要再show,就可以出现所画图形,也不会另外弹出画图窗口。

图形绘制

我们实现了draw_line和draw_cycle两个函数,用来画出线类和圆类图形。

对于draw_line,由于QCustomerPlot并不提供直线的绘制功能,而只提供线段绘制,故我们采用将给出两点坐标延拓到画布边界的方式来表示直线。

对于draw_cycle,由于QCustomerPlot并不提供圆形的绘制功能,故我们采用上下两圆弧拼接为一个圆的方式作为圆的绘制,需要注意的是,在绘制曲线时,要设置好点与点之间的间隔距离,太大会导致生成直线,太小会导致点过于多画的很慢。

同时,为了便于查看,将不同图形设置为不同的颜色。

界面移动、缩放

对于widget类,可以对其机型相关参数设置,以达到画图界面的移动、缩放等功能,部分代码如下:

ui->widget->setInteractions(QCP::iRangeDrag|QCP::iRangeZoom| QCP::iSelectAxes |
                              QCP::iSelectLegend | QCP::iSelectPlottables);

交点绘制

为了便于查看,我们将交点用半径为0.02的圆形来绘制,部分代码如下:

for (int i = 0; i < int(positions.size()); i += 2) {
	line = QString("%1 %2").arg(positions[i]).arg(positions[i + 1]);
	draw_cycle(positions[i], positions[i + 1], 0.02, 100);
	ui->listwidget2->addItem(line);
}

其中draw_cycle参数的100表示曲线两点间包含圆直径*100个点,从而可以画出比较圆滑的曲线。

坐标显示

考虑到如果对于大量图形,保持坐标显示在屏幕上不是一个好思路,会遮挡几何图形,所以采用鼠标点击触发点坐标显示的函数,部分代码如下:

if (event->button() != Qt::LeftButton){
    return;
}
QPointF ChickedPoint = event->pos();
if(!ui->widget->viewport().contains(event->pos())){
    return;
}
double currentx = ui->widget->xAxis->pixelToCoord(ChickedPoint.x());
double currenty = ui->widget->yAxis->pixelToCoord(ChickedPoint.y());

对于每一次点击,进行绘图界面的刷新工作,显示出以两条相互垂直直线形成的标记,且在标记旁边出现此点坐标。

ListWidget

这部分需要使用两个模板,一个用来显示几何图形的相关参数,另一个用来显示交点数量以及坐标。

这里需要注意的就是,在每次删除或者增加图形的时候,要随之维护两者所承载的文本。

Linetext

这部分同样需要两个模板,一个用来显示所选用文件的路径,另一个 用来显示输入几何图形的参数。

Push Button

这部分需要使用四个模板,包括路径,添加文件,添加图形和删除图形。

需要说明的一点是,需要将这四个按钮全部固定为slot,才能在函数中进行调用:

路径

这部分代码需要负责,点击之后弹出路径选择模块,从本地中选择适合的文件路径进行输入,这一步是之后文件输入的前置条件,部分代码如下:

QString file_path = QFileDialog::getOpenFileName(NULL,"标题",".","*.txt");
if(file_path.isEmpty())
{
	return;
}
else {
	ui->text1->setText(file_path);
}

添加文件

添加文件一个按钮所需要做的事情包括,打开路径文本中的文件,对其中的图形参数进行处理,画图几个部分。最初设计时考虑添加一个画图按钮,在添加文件或者图形之后点击即可画图,后来经过思考,发现这样做的话可能会因为遗忘等原因忘记自己添加到哪个图形这样的窘境,所以将其与添加模块合并,下同。

部分代码如下:

QString path = ui->text1->text();
QFile file(path);
file.open(QIODevice::ReadOnly | QIODevice::Text);
QTextStream in(&file);
QString line = in.readLine();
int n = line.toInt();
for (int i = 0; i < n; i++) {
	line = in.readLine();
	ui->listwidget1->addItem(line);
	QStringList strlist = line.split(" ");
	create_array(strlist);
}
updatelistwidget2();
draw();

添加图形

这部分需要获取在文本框中输入的图形参数,进而画出相关图像,操作顺序与添加文件相似,部分代码如下:

QString line = ui->text2->text();
ui->listwidget1->addItem(line);
QStringList strlist = line.split(" ");
create_array(strlist);
updatelistwidget2();
draw();
ui->widget->replot();

删除图形

这里我提供了多选删除的操作,所以在想删除大量图形的时候,只需要选择自己想删除的图形,就可以一次性全部删除,而不用一个一个删。部分代码如下:

ui->listwidget1->setSelectionMode(QAbstractItemView::MultiSelection);

删除之后需要对交点重复计算,也需要对剩余图形进行重新绘图,部分代码如下:

while(ui->listwidget1->selectedItems().size()>0)
{
	QListWidgetItem* sel = ui->listwidget1->selectedItems().at(0);
	if (sel)
	{
		int r = ui->listwidget1->row(sel);
		sel = ui->listwidget1->takeItem(r);
	}
}

11.界面模块与计算模块的对接。详细地描述 UI 模块的设计与两个模块的对接,并在博客中截图实现的功能

为了实现书中所提到的松耦合原则,我们约定GUI模块和计算模块的交互只包含数据流。

通过如下接口实现两个模块的对接:

class DLL_EXPORT DLL
{
public:
    DLL();
    void setFigures(std::vector<int>);
    void setMapRange();
    void reset();
    void update();
    std::vector<double> getFigures();
    std::vector<double> getIntersects();
};

使得GUI界面只需要关注自己的画图功能即可。

我们约定直线为1,射线为2,线段为3,圆为4,将其存入连表中,五个元素为一个图形,格式为,图形形状,x1,y1,x2,y2,由于圆只包含三个参数,所以将其最后一位补0来统一格式进行数据流的传递。

对于交点,只需要传递交点坐标即可。

如此便实现了GUI和计算模块的交互过程,并且完全不需要依赖两者内部所调用的模块。

下面进行功能展示:

添加文件

添加图形

删除图形

交点及坐标

12.描述结对的过程,提供两人在讨论的结对图像资料(比如 Live Share 的截图)。关于如何远程进行结对参见作业最后的注意事项

我们尝试使用了LiveShare来进行远程代码开发。LiveShare能提供类似Overleaf的共同开发环境,有效提高编码效率。但是可能由于网络原因,延迟较高,在我们本次开发中使用较少。

在设计、编码过程中,我们主要使用了腾讯会议来进行相互交流。腾讯会议的投屏功能,能够方便地展现一方的开发环境,方便结对编程。

同时我们使用Github进行进度管理和版本控制。如我们使用issue功能来设置和分配待完成事项等。

13.说明结对编程的优点和缺点。同时描述结对的每一个人的优点和缺点在哪里(要列出至少三个优点和一个缺点)

结对编程

定义为:结对编程是一种敏捷软件开发的方法,两个程序员在一个计算机上共同工作。一个人输入代码,而另一个人审查他输入的每一行代码。输入代码的人称作驾驶员,审查代码的人称作观察员(或导航员)。两个程序员经常互换角色。在结对编程中,观察员同时考虑工作的战略性方向,提出改进的意见,或将来可能出现的问题以便处理。这样使得驾驶者可以集中全部注意力在完成当前任务的“战术”方面。观察员当作安全网和指南。结对编程对开发程序有很多好处。比如增加纪律性,写出更好的代码等。

优点:

  • 编程的效率会因此提升,bug数量会减少,因为一个人负责编写代码,另一个人负责代码的检查。同样是写代码,两个人一起会更加能够保证程序的质量,而第二个人的检查也减少了误触、马虎等导致bug的机会。
  • 可以促进两者中较弱的那一方水平的增长,水平较低的那一方会潜移默化地受到水平较高的那一方的影响,学到新的东西;而水平较高的那一方在给水平较低那一方进行教学的过程中也温故知新,让自己的思路更加清晰。
  • 定期打乱配对,可以让参与项目的人员更换位置,使维护文档变的不必要,因为大家对于本项目都了然于胸,较好的掌握项目的基本框架。
  • 起到互相激励的作用,一般情况下,单个人进行工作容易懈怠,导致拖延症的产生,而两个人可以相互讨论,让两个人的工作效率提升。同时由于两个人的角色可以互换,不至于因为重复干同样的工作而感到十分疲惫。

缺点:

  • 合作的水准比较大的取决于双方的道德约束,即责任心。若两者中有一方对项目不上心的话,很容易导致合作无法继续进行下去
  • 由于两者水平参差不齐,可能导致更强的一方对更弱的一方不满,或者是导致更强的一方全权包揽,最终变成了个人项目。
  • 两者水平若同样强,可能就某些问题产生不可调和的分歧,造成效率低下的窘境,可能会出现两个完全不同的版本,或者甚至完全无法动工。
  • 两个人可能会聊天而导致分散注意力,效率低下。

优缺点

姓名 优点 缺点
ljy 编程能力强、交流能力强、有责任心 有时有点摸鱼
dxy 善于学习、交流能力强、有责任心 有时比较急躁

14.松耦合

我们和@17373325的同学进行了核心模块交换松耦合测试。在核心模块部分我们使用的是基本数据类型的数据流,而他们使用的是自定结构体,其他行为基本一致。因此在进行少量的改动之后,我们的GUI便可以配合他们的核心模块一起使用了。

交换后的展示结果如下:

图为使用其他组同学的核心Dll模块的运行结果。(我们的GUI+他们的Dll)

同时,由于接口定义不同,所以我们稍作修改,除了以上的GUI模块实现了对接,我们也对本来的exe进行了测试,测试结果如下:

在交换时发现的问题:
问题最大的部分就是接口的不一致,同样问题,不同的人有不同的实现方法,而由于我们是在实现功能之后再进行的松耦合交换,不可避免的导致接口无法直接使用,所以只能通过重新封装接口来实现。而且在这里面,我们认为在接口中将数据封装为数据流,而非是依赖于其它结构体的形式更加合理,使耦合度进一步降低。

原文地址:https://www.cnblogs.com/dxy1999/p/12558893.html