结对项目作业
项目 | 内容 |
---|---|
这个作业属于哪个课程 | 2020计算机学院软件工程(罗杰 任健) |
这个作业的要求在哪里 | 结对项目作业 |
教学班级 | 005 |
GitHub地址 | SE_pair_work |
1.在文章开头给出教学班级和可克隆的 Github 项目地址。(1')
见上表
2.PSP表格
在开始实现程序之前,在下述 PSP 表格记录下你估计将在程序的各个模块的开发上耗费的时间。(0.5')
在你实现完程序之后,在附录提供的PSP表格记录下你在程序的各个模块上实际花费的时间。(0.5')
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
· Estimate | · 估计这个任务需要多少时间 | 395 | 450 |
Development | 开发 | ||
· Analysis | · 需求分析 (包括学习新技术) | 120 | 120 |
· Design Spec | · 生成设计文档 | 30 | 30 |
· Design Review | · 设计复审 (和同事审核设计文档) | 30 | 30 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | 30 |
· Design | · 具体设计 | 120 | 150 |
· Coding | · 具体编码 | 480 | 540 |
· Code Review | · 代码复审 | 120 | 120 |
· Test | · 测试(自我测试,修改代码,提交修改) | 240 | 270 |
Reporting | 报告 | ||
· Test Report | · 测试报告 | 60 | 60 |
· Size Measurement | · 计算工作量 | 30 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 20 |
合计 | 1290 | 1380 |
估计时间与实际花费时间相差不大,主要时间消耗在于UI 的设计和编码。
由于此前未涉及过有关工作,因此在学习相关知识上消耗了许多时间。
在UI的具体编码时,又由于不熟练出现了很多意想不到的情况,花费很多额外时间。
3. 看教科书和其它资料中关于 Information Hiding,Interface Design,Loose Coupling 的章节,说明你们在结对编程中是如何利用这些方法对接口进行设计的。(5')
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).
-- 引用自维基百科
我们在编程时,将函数具体实现的细节隐藏起来,只留下接口与其他模块(例如UI模块)进行联系,外界模块在调用时只能使用特定的功能。
Interface Desig和Loose Coupling
在设计接口时,我们从UI模块的需求和保证松耦合两方面考虑,例如在UI模块中存在添加几何对象的功能,据此设计了添加几何对象的接口。
在我们的设计中,计算模块各个接口的功能非常单一,与其他接口没有过多的联系,例如添加几何对象的接口就只实现添加的功能,不存在一个接口同时实现多种功能的情况。
在模块的总体设计上,计算模块只考虑如何实现添加,删除,计算交点等计算方面功能,UI模块只考虑如何实现绘制几何对象及交点等绘制方面功能,分工明确。
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),因此进行了一定的修改,把去重的条件限制的更多一点,只有交点个数超过5000000才进行去重。
修改后的效果如图:
主要时间花费在了IO,而去重的时间几乎很少,都没有进入热点函数!
上面的数据是点数接近5000000的随机数据,对于一个一直计算的数据,主要的热点函数还是计算函数line2line:如下图:
7. 契约式设计
看 Design by Contract,Code Contract 的内容,描述这些做法的优缺点,说明你是如何把它们融入结对作业中的。(5')
-
契约式设计:要求软件设计者为软件组件定义正式的,精确的并且可验证的接口。核心思想是对软件系统中的元素之间相互合作以及“责任”与“义务”的比喻。
即如果在面向对象程序设计中一个类的函数提供了某种功能,那么它要:- 期望所有调用它的客户模块都保证一定的进入条件:这就是函数的先验条件。
- 保证退出时给出特定的属性:这就是函数的后验条件。
- 在进入时假定,并在退出时保持一些特定的属性:不变式。
-
优点:
- 如果接口都可以满足先验条件,将会省去很多测试部分的工作。
- 先验条件,后验条件和不变式的有助于使用者了解接口。
- 组件服务的提供方和使用方各自的义务被表述得更清晰,从而使设计更加系统化、更清楚、更简单。
-
缺点:
- 主要缺点在于学习契约式设计和实际使用它都要花费大量的时间,因此在某些情况下(例如小项目的开发)使用契约式设计可能付出与回报不成正比。
在本次结对作业中,我们没有这么详细地去书写先验条件等,但也对函数接口的部分限制做了书面说明。
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')
结对编程优点:
- 两个人互相监督可以提高工作的效率,防止怠惰。
- 遇到问题时两人一起解决会比一个人单独解决时更快,并且两个人一起考虑问题会考虑得更加全面。
- 每个人都不是全能的,因此两个人可以形成一种互补。
结对编程缺点:
- 结对编程的开展会受限于双方时间的限制。
- 二者由于性格原因可能无法合作。
优点 | 缺点 | |
---|---|---|
伙伴 | 编码能力强,态度认真,工作态度积极 | 编码风格更偏向于面向过程 |
我 | 乐于学习新知识,听从安排认真完成每一件事,有耐心 | 容易偷懒 |
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风格描述的文档系统,下次团队项目准备尝试!