2020BUAA 软工-结对作业
结对作业
项目 | 内容 |
---|---|
北航2020软工 | 班级博客 |
作业要求 | 具体要求 |
1.在文章开头给出教学班级和可克隆的 GitHub 项目地址(例子如下)。(1')
教学班级 | 005 |
---|---|
项目地址 | GitHub |
2.在开始实现程序之前,在下述 PSP 表格记录下你估计将在程序的各个模块的开发上耗费的时间。(0.5')
见part14
3.看教科书和其它资料中关于 Information Hiding,Interface Design,Loose Coupling 的章节,说明你们在结对编程中是如何利用这些方法对接口进行设计的。(5')
Information Hiding
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.
Interface Design
六大原则:单一职责原则(SRP)、里氏替换原则(LSP)、依赖倒置原则(DIP)、接口隔离原则、迪米特法则(LOD)、开闭原则
Loose Couple
松耦合是两个对象通过接口来交互,彼此没有其他的联系,而对象内往往是高内聚。
-
我们一开始的设计是:计算模块只有计算的接口,而且是单次计算(直线和直线、直线和圆、圆和圆),而把数据都保存在命令行或者UI这边,但是在处理异常时,发现这样做导致数据分离,异常几乎得处理两遍。这样做之前的模块就相当于被拆分开了,数据和计算方法分别在不同的模块。
接口只提供计算,把需要计算的几何图形传入,得到计算结果。
-
经过与交换团队的商量,我们更改了设计,把数据放到了计算模块,提供了数据交互的接口:
- 添加和删除直线(L,S,R)
- 添加和删除圆
- 重置几何图形数据
- 获得交点个数
- 获得交点坐标
命令行程序和UI只需要把数据通过接口传递给计算模块,再通过接口控制计算模块的行为。
这样设计的目的是:
- 数据的信息全部隐藏在计算模块中,UI和命令行程序只进行数据输入,不处理具体的维护逻辑。
- 通过接口来控制计算模块的行为,而不是仅仅调用计算模块来进行计算
- 通过API来控制计算模块,和交换小组之间更容易进行对接
4.计算模块接口的设计与实现过程。设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图?说明你的算法的关键(不必列出源代码),以及独到之处。(7')
计算模块设计了三个类:Point,Line,Circle,沿袭了上一次的设计,只是添加了比较符号运算;对于直线、射线、线段的区分,我们是采用一个char标志,而没有使用继承。基本的求交点逻辑沿袭上一次的个人作业。
函数的设计,全部设计为全局函数,主要进行交互,类内部的函数主要是数据交换。
对应的计算函数都是和类分开的,传入需要计算的几何图形获得结果。
三个DLL内部全局变量:
- lines:保存直线(L,R,S)
- circles:保存圆
- Result:保存交点
对外的接口函数:(参数信息省略)
- addLine() :添加直线(L,R,S)
- delLine() :删除直线(L,R,S)
- addCircle() :添加源
- delCircle() :删除圆
- resetRes() :重置,将直线、圆、交点信息全部重置
- getResultOfIntersect() :启动计算,计算结束后返回交点个数
- getPoint() :返回所有的交点
下面是给交换小组增加的接口:
- getNumofLines() :获取存储的直线数量
- getLines() :获取存储的直线
- getNumOfCircles() :获取存储的圆的数量
- getCircles() :获取存储的圆
主要的模块逻辑:
- 通过添加函数来添加几何图形数据
- 调用getResultOfIntersect()函数来进行计算并且返回结果
- 通过getPoint()来获取交点(供UI使用)
- 重置数据
整体的模块就四种基本操作,内部逻辑对外不公开,实现信息隐藏和封装!
关键算法流程图:
直线和直线:(这里所有的输入对象都不存在重合的情况,已经处理完毕)
- 首先按照直线判断是否相交,如果相交,则计算交点坐标,然后判断交点是否在给定的两个几何图形上(直线、射线、线段)
- 如果平行,那么考虑特殊情况
- 射线和射线平行,但是可能会有交点,即:起点相同,方向反向
- 射线和线段平行,可能会有交点,射线的起点和线段的某个交点重合
- 线段和线段平行,有交点:A的某个端点和B的某个端点重合
直线和圆:
- 按照直线和圆进行判断和计算
- 圆心距大于半径无交点
- 圆心距等于半径相切
- 圆心距小于半径有两个交点
- 然后判断交点是否在给定的直线、射线、线段上
圆和圆:
- 和第一次的处理一样,不需要添加额外的东西,按照公式计算。
5.阅读有关 UML 的内容:UML。画出 UML 图显示计算模块部分各个实体之间的关系(画一个图即可)(2’)。
6.计算模块接口部分的性能改进。记录在改进计算模块性能上所花费的时间,描述你改进的思路,并展示一张性能分析图(由VS 2015/2017的性能分析工具自动生成),并展示你程序中消耗最大的函数。(3')
计算模块分为三个部分:
- 直线和直线
- 直线和圆
- 圆和圆
三个部分的和是总的结果
因为我们使用vector来存储交点,采取的策略是最后进行去重。但实际在写代码的时候三个计算部分结束时都进行了去重:(其中直线和直线计算如下图)
可以看到主要的时间都在去重(clearRes),因此进行了一定的修改,把去重的条件限制的更多一点,在最后进行一次出去重。
修改后的效果如图:
主要时间花费在了IO,而去重的时间几乎很少,都没有进入热点函数!
上面的数据是点数接近5000000的随机数据,对于一个一直计算的数据,主要的热点函数还是计算函数line2line:如下图:
7.看 Design by Contract,Code Contract 的内容:描述这些做法的优缺点,说明你是如何把它们融入结对作业中的。(5')
Design by Contract
契约式设计或者Design by Contract (DbC)是一种设计计算机软件的方法。这种方法要求软件设计者为软件组件定义正式的,精确的并且可验证的接口,这样,为传统的抽象数据类型又增加了先验条件、后验条件和不变式。这种方法的名字里用到的“契约”或者说“契约”是一种比喻,因为它和商业契约的情况有点类似。
契约式设计强调三个概念:前置条件,后置条件和不变式。
-
post-conditions:是指操作执行完之后的情况,发生在每个操作的最后
-
pre-condition:是指在执行操作之前,期望具备的环境,发生在每个操作(方法,或者函数)的最开始。
-
invariants:是指使关于类(class)的断言,实际上是前置条件和后置条件的交集。
优点:
- 获得更优秀的设计
- 提高可靠性
- 更出色的文档
- 简化调试
- 支持复用
缺点:
- 开发这需要时间学习撰写良好契约的思想和技术。
- 契约的撰写成本
- 需要大量的实践
- 最好用于顺序式程序(sequential program) 契约可以用在并发程序和分布式程序中。
我们在设计中没有明确的使用契约式设计,不过一部分工作类似于这个,比如说直线和直线重合,可以设置为一种契约,但是我们是把这个提取出来,放在了外面单独设置了一个函数来解决这个问题。
接口的设计中,我使用c++,为了传递交点,使用指针,而对方使用C#,为了简单,直接使用了数组来对接。
8.计算模块部分单元测试展示。展示出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路。并将单元测试得到的测试覆盖率截图,发表在博客中。要求总体覆盖率到 90% 以上,否则单元测试部分视作无效。(6')
考虑本次新增的功能,主要是射线和线段,因此,测试的重点是直线与直线之间的计算:
//L and L
TEST_METHOD(L_L_true)
{
Line l1('L', -2, 4, 3, 5);
Line l2('L', 9, -2, 6, 4);
Point p(5.272727272727273, 5.45454545454545455);
double* x = new double();
double* y = new double();
int num = lWithl('L', -2, 4, 3, 5, 'L', 9, -2, 6, 4, x, y);
Assert::IsTrue(num == 1);
Point res(*x, *y);
Assert::IsTrue(p == res);
}
//L and R
TEST_METHOD(L_R_true) {
Line l1('L', -2, 4, 3, 5);
Line l2('R', -4, 0, 0, 2);
Point p(8, 6);
Point res;
double* x = new double();
double* y = new double();
int num = lWithl('L', -2, 4, 3, 5, 'R', -4, 0, 0, 2, x, y);
Assert::IsTrue(num == 1);
//revert
num = lWithl('R', -4, 0, 0, 2, 'L', -2, 4, 3, 5, x, y);
Assert::IsTrue(num == 1);
}
//接口测试
TEST_METHOD(line_with_line2) {
resetRes();
addLine('L', 91598, 51695, -292, 76595);
addLine('L', 91040, -72215, -66266, 41547);
addLine('L', 79726, 64976, -47232, -59439);
addLine('L', -60802, -86271, 15560, 68129);
addLine('L', -18061, 96085, 98631, 76823);
int num = getResultOfIntersect();
Assert::IsTrue(num == 10);
}
以上为举例:
测试的函数主要是计算交点的函数,同时测试了接口函数。接口函数通过样例以及随机数据进行了测试。
针对于直线和直线分为:
- 直线和直线(主要是通过回归测试)
- 直线和射线
- 相交的特殊情况:交点是射线起点
- 不相交:交点不在射线上
- 射线和射线
- 相交的特殊情况:
- 交点是起点
- 平行且相交(在一条直线上)
- 不相交:交点不在两个射线上
- 相交的特殊情况:
- 射线和线段
- 相交的特殊情况
- 交点在射线的起点
- 交点在线段的端点之一
- 平行且相交:射线起点和线段端点重合
- 相交的特殊情况
- 线段和线段:
- 相交:
- 交点是端点
- 平行且相交
- 不相交:和上面类似
- 相交:
直线和圆的单元测试主要是增加了射线和线段,考虑特殊情况:
- 射线的起点在圆上
- 线段在原内部,无交点
- 相切时切点是射线的起点或者线段的端点
单元测试:
代码覆盖率:
9.计算模块部分异常处理说明。
- 在博客中详细介绍每种异常的设计目标。每种异常都要选择一个单元测试样例发布在博客中,并指明错误对应的场景。(5')
异常类型 | 样例 | 具体说明 | 输出信息 |
---|---|---|---|
命令行参数中有错误参数 | intersect.exe -a input.txt -o output.txt | 命令行参数中使用了除-i和-o之外的参数 | Wrong Arguement!! |
命令行参数中缺少参数 | intersect.exe -o output.txt | 命令行参数缺少-i或-o中的一个或多个 | Too Few Arguements!! |
缺少对几何对象数目的描述 | L 2 0 3 4 C 2 3 4 |
输入文件第一行不是自然数N | Please Input N!! |
使用小写字母描述几何对象类型 | 1 l 0 2 3 4 |
将L,R,S,C错写为l,r,s,c | Please Use Capiter Letter!! |
缺少几何对象类型描述 | 1 0 2 3 4 |
缺少类型描述L,R,S,C | Please Input Type!! |
几何对象参数缺少 | 1 L 2 3 2 |
L,R,S类型对象输入少于四个的参数 C类型对象输入少于三个的参数 |
Too Few Messages!! |
几何对象参数为非数字 | 1 L L2 2 3 3 |
几何对象参数为非数字 | Please Input a Interger!! |
几何对象参数过多 | 1 L 23 3 4 5 6 |
L,R,S类型对象输入多于四个的参数 C类型对象输入多于三个的参数 |
Too Many Messages!! |
几何对象参数数值超出范围 | 1 L 100000 -100000 2 3 |
输入的参数不在(-100000,100000)范围内 | Out Of Bound!! |
线形几何对象两点重合 | 1 L 2 3 2 3 |
L,R,S类型对象输入的两点坐标相同 | Two Points Coincide!! |
圆半径小于等于0 | 1 C 3 4 -2 |
输入了小于等于0的圆半径 | Radiu Must Be Greater Than Zero!! |
线形几何对象重合 | 2 L 2 3 4 6 L 0 0 10 15 |
L,R,S类型对象重合导致出现无限多个交点 | line is same with before one |
圆重合 | 2 C 2 3 4 C 2 3 4 |
C类型对象重合导致出现无限多个交点 | Circle is same with before one |
10.界面模块的详细设计过程
在博客中详细介绍界面模块是如何设计的,并写一些必要的代码说明解释实现过程。(5')
本次UI模块利用c# WinForm进行界面设计。
下图为UI界面:
按钮功能如下
1.文件导入:选择文件并从文件导入几何对象的描述
相应代码:
DialogResult dr = openFileDialog1.ShowDialog();
//获取所打开文件的文件名
string filename = openFileDialog1.FileName;
if (dr == DialogResult.OK && !string.IsNullOrEmpty(filename))
{
StreamReader sr = new StreamReader(filename);
string line = sr.ReadLine();
while (!sr.EndOfStream)
{
line = sr.ReadLine();
comboBox1.Items.Add(line);//将信息加入到删除左侧的复选框(comboBox)中
add(line);
}
sr.Close();
}
其中add函数的功能为将line中的信息提取出来并调用计算模块相关函数。
2.添加:添加按钮左侧的文本框内的几何对象描述
相应代码:
if (textBox1.Text != "")//判断文本框中的值是否与组合框中的的值重复
{
if (comboBox1.Items.Contains(textBox1.Text))
{
MessageBox.Show("该几何对象已存在!", "提示");
}
else
{
comboBox1.Items.Add(textBox1.Text);
string[] points = textBox1.Text.Split(' ');
add(textBox1.Text);
MessageBox.Show("添加成功!", "提示");
}
}
else
{
MessageBox.Show("请输入几何对象!", "提示");
}
3.删除:将左侧comboBox内选择的几何对象删除
if (comboBox1.Text != "")
{
if (comboBox1.Items.Contains(comboBox1.Text))
{
comboBox1.Items.Remove(comboBox1.Text);
del(comboBox1.Text);
MessageBox.Show("删除成功!", "提示");
}
else
{
MessageBox.Show("请重新选择!", "提示");
}
}
else
{
MessageBox.Show("请选择要删除的几何对象", "提示");
}
4.绘制: 在按钮下方绘制现有几何对象并标记交点,按钮右侧显示交点数目及查看交点坐标
相应代码:
Bitmap b = new Bitmap(length, length);
Graphics g = Graphics.FromImage(b);
Paint(b, g);//画出坐标系
for (int i = 0; i < comboBox1.Items.Count; i++)
{
string line = (string)comboBox1.Items[i];
string[] points = line.Split(' ');
Draw(points, g);
}//画出每一个几何对象
int size = Class1.getResultOfIntersect();
double[] x = new double[size];
double[] y = new double[size];
int[] size1 = new int[1];
label4.Text = "交点数目:" + size.ToString();
Class1.getPoint(x, y, size1);
comboBox2.Items.Clear();
for(int i = 0; i < size; i++)
{
comboBox2.Items.Add("(" + ((float)x[i]).ToString() + "," + ((float)y[i]).ToString() + ")");//填写交点坐标
MarkPoint(g, (float)x[i], (float)y[i]);//标记交点
}
其中Paint函数的功能为画出坐标系,坐标系的绘画即利用两根直线作为x轴y轴,利用短线段作为刻度线,根据现有几何对象的数据决定坐标系的比例尺,即两条刻度线之间相差的坐标数(例如比例尺为2,则x轴上两刻度线之间坐标相差2).
Draw函数的功能为画出一个几何对象,由于使用的函数只能根据两点坐标画线,因此对于直线的绘画采用计算直线与左右边界的交点坐标并根据这两点坐标画线的方法。对于圆则直接利用函数即可。
MarkPoint函数的功能是标记交点,采用的方法是画一个实心小圆作为标记。
11.界面模块与计算模块的对接
详细地描述 UI 模块的设计与两个模块的对接,并在博客中截图实现的功能。
UI模块的设计及功能截图在10.界面模块的详细设计过程中已经介绍过。
在C#中,直接使用DLLimport语句就可以导入计算模块的dll提供的接口,在UI模块中直接调用即可。
导入语句如下
[DllImport("core.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void getPoint(double[] x, double[] y, int[] size);
[DllImport("core.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void addLine(char l, int x1, int y1, int x2, int y2);
[DllImport("core.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void delLine(char l, int x1, int y1, int x2, int y2);
[DllImport("core.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void addCircle(int x, int y, int r);
[DllImport("core.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void delCircle(int x, int y, int r);
[DllImport("core.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int getResultOfIntersect();
12.描述结对的过程,提供两人在讨论的结对图像资料(比如 Live Share 的截图)。关于如何远程进行结对参见作业最后的注意事项。(1')
我们使用的是腾讯会议来进行远程交流:
- 讨论了接口的设计,隐藏数据,进行了重构的设计
- 讨论UI的设计以及和API的对接处理
然后我们是双线,我负责计算核心,他负责UI,一起推进工程!
13.看教科书和其它参考书,网站中关于结对编程的章节,例如,说明结对编程的优点和缺点。同时描述结对的每一个人的优点和缺点在哪里(要列出至少三个优点和一个缺点)。(5')
结对编程:
优点 | 缺点 |
---|---|
能够提供更好的设计质量和代码质量,合作有更强的解决问题的能力 | 双方差距较大时,容易变成一方主导的工作形式 |
双方互相监督,促进工作的推进 | 习惯和性格不同可能会产生争议和矛盾 |
结对能更有效地交流,相互学习和传递经验,降低学习成本 | 双方不协调,经验丰富者自负,新手自卑,最终起到反作用 |
对于企业来说,能够更好的处理人员流动,因为两人都对同一项目有较深的了解 | 两个人如果都不互相监督,形成集体摸鱼的行为! |
结对者-wzb
优点 | 缺点 |
---|---|
编码能力强,可以独立解决问题! | 常常想着摸鱼 |
善于沟通,比较亚萨西! | |
14.在你实现完程序之后,在附录提供的PSP表格记录下你在程序的各个模块上实际花费的时间。(0.5')
PSP2.1 | Personal Software Process Stage | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 45 | 60 |
· Estimate | · 估计这个任务需要的时间 | 45 | 60 |
Development | 开发 | 780 | 960 |
· Analysis | · 需求分析 | 60 | 120 |
· Design Spec | · 生成设计文档 | 30 | 30 |
· Design Review | · 设计复审 | 15 | 15 |
· Coding Standard | · 代码规范 | 15 | 15 |
· Design | · 具体设计 | 120 | 120 |
· Coding | · 具体编码 | 240 | 360 |
· Code Review | · 代码复审 | 60 | 60 |
· Test | · 测试 | 240 | 240 |
Reporting | 报告 | 120 | 120 |
· Test Report | · 测试报告 | 60 | 60 |
· Size Measurement | · 计算工作量 | 30 | 30 |
· Postmorten & Process Improvement Plan | · 事后总结,并提出过程改进计算 | 30 | 30 |
合计 | 600 | 1140+ |
Appendix
附加题(松耦合)
交换小组 | 学号 |
---|---|
zyc | 16191051 |
shx | 17373072 |
前面提到了,我们因为设计原因进行了重构,并且提前和对方小组商量了接口。尽管如此,还是遇到了对接的问题。
- 命令行程序调用对方小组的计算核心:下面用A代表我们自己的计算核心,B代表交换小组的!
#include "core.h"//A的核心头文件
int main(int argc, char** argv) {
//B的dll的调用声明!
typedef void(*ADD)(int x1, int y1, int x2, int y2);//B的直线(L,R,S)添加函数
typedef int(*GetNum)(); //B的计算函数
typedef void(*ADDCIRCLE)(int x, int y, int r); //B的圆的添加函数
HMODULE hMod = LoadLibrary(L"intersect_core.dll");
ADD addAline = (ADD)GetProcAddress(hMod, "AddLine");
ADD addAradial = (ADD)GetProcAddress(hMod, "AddRay");
ADD addAsegment = (ADD)GetProcAddress(hMod, "AddSection");
ADDCIRCLE addAcircle = (ADDCIRCLE)GetProcAddress(hMod, "AddCircle");
GetNum getInSize = (GetNum)GetProcAddress(hMod, "GetIntersectionsSize");
//...打开文件,读数据以及处理异常:
for (int i = 0; i < lineNum; i++) {
//...逻辑省略
if (mark == 'L' || mark == 'R' || mark == 'S') {
//...异常处理
addLine(mark, x1, y1, x2, y2);//A
if (mark == 'L') {
addAline(x1, y1, x2, y2);//B
}
else if (mark == 'R') {
addAradial(x1, y1, x2, y2);//B
}
else if (mark == 'S') {
addAsegment(x1, y1, x2, y2);//B
}
}
else if (mark == 'C') {
//...异常处理
addCircle(x1, y1, r); //A
addAcircle(x1, y1, r); //B
}
}
fileIn.close();
int res = getResultOfIntersect();//A
int res1 = getInSize(); //B
if (fileOut) {
fileOut << res;
}
cout << res << " " << res1 << endl;
return 0;
}
经过验证,两个核心A和B的结果保持一致(随机数据测试+单元测试)
一个结果如图:(小尺度数据)
问题:
- 这里因为我们把数据全部保存在了后端,而对方需要获得直线和圆的数据,所以给对方新增了四个数据交互的API。(商量后我们这里修改更方便)
- UI的对接:
- 下图是我们的UI使用对方的核心计算的结果
问题:
-
因为对方修改了接口的参数类型,导致我们一度无法正常使用,最后在沟通下发现这一问题,给我们的启示是,接口的设计应该更好的维护,尤其是更新后需要及时告知接口使用方。或者维护一个接口的说明,实时更新,这里想到了老师课上提到的:
对于API文档的维护,我看到的比较好的做法是,在注释中加入API文档,修改代码的同时必须修改对应的文档,然后用一些工具可以生成专门的文档,这样可以较好的确保API文档与实现的一致性。
然后找到了Doxygen,一种开源跨平台的,以类似JavaDoc风格描述的文档系统,下次团队项目准备尝试!