项目 | 内容 |
---|---|
这个作业属于哪个课程 | 2020计算机学院软件工程(罗杰 任健) |
这个作业的要求在哪里 | 结对项目作业 |
教学班级 | 006 |
项目地址 | https://github.com/CrapbagMo/PairProgramIntersect |
1. PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 30 |
· Estimate | · 估计这个任务需要多少时间 | 30 | 30 |
Development | 开发 | 930 | 1020 |
· Analysis | · 需求分析 (包括学习新技术) | 90 | 120 |
· Design Spec | · 生成设计文档 | 30 | 30 |
· Design Review | · 设计复审 (和同事审核设计文档) | 60 | 60 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | 30 |
· Design | · 具体设计 | 150 | 180 |
· Coding | · 具体编码 | 360 | 360 |
· Code Review | · 代码复审 | 60 | 90 |
· Test | · 测试(自我测试,修改代码,提交修改) | 240 | 240 |
Reporting | 报告 | 60 | 70 |
· Test Report | · 测试报告 | 30 | 40 |
· Size Measurement | · 计算工作量 | 10 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 20 | 20 |
合计 | 1110 | 1210 |
2. Information Hiding,Interface Design,Loose Coupling
Information Hiding
是指信息隐藏原则,在1972年被 David Parnas 提出,他指出:代码模块应该采用定义良好的接口来封装,这些模块的内部结构应该是程序员的私有财产,外部是不可见的。在结对编程中,我们对于信息隐藏的应用是:- 在多层设计中的层与层之间加入接口层
- 所有类与类之间都通过接口类访问
- 类的所有数据成员都是private,所有访问都是通过访问函数实现的
Interface Design
是指接口设计,接口被定义为职责(或角色、能力),接口决定了一种类能够做什么,赋予了他在某种情况下的权力和义务。在结对编程中,我们专注于定义行为,把某个具体功能提取出来,先把这个功能弄成策略,然后各自具体用类去继承这个接口。Loose Coupling
是指松耦合,是指减少一个代码单元与其他代码单元的配合关系。最理想、最松散的耦合,是一个单元无需其他代码单元的配合可以单独完成它的功能。松耦合的目标是最小化依赖。松耦合这个概念主要用来处理可伸缩性、灵活性和容错这些需求。在结对编程中,我们通过加入中间层,来减少各个代码单元之间的耦合度。
3. 计算模块接口的设计与实现
-
函数及类:
-
五个主要类
点类:
class Point
图形类:
class Figure
线条类:
class Line: Figure
圆形类:
class Circle: Figure
平面容器类:
class PlaneContainer
; -
五个主要函数:
添加图形:
int add_Figure(std::string buf)
初始化容器:
void initial_PlaneContainer()
释放容器:
void dispose_PlaneContainer()
获取交点序列:
double* get_IntersectionPoints()
获取交点数目:
int get_NumOfIntersectionPoints()
-
-
主要作用:
Circle
、Line
类继承自Figure
类,实现std::set
方法。该方法返回两个图形的交点集合。intersect(Figure*) - 五个函数都是对外提供的接口函数。
init_PlaneContainer、dispose_PlaneContainer
采用单例模式用于维护全局静态变量PlaneContainer* pc
;add_Figure
、get_NumOfIntersectionPoints
、get_IntersectionPoints
用于提供添加图形、获取交点序列、交点数目功能。 - 异常由add_Figure函数捕获并处理,处理完后已错误代码形式返回,具体处理方式本节不做介绍。
-
算法关键及独到之处:
-
首先考虑直线和圆的情况(先不考虑射线和线段):
按照直线和直线, 直线和圆, 圆和圆在平面上的关系分为下面三种情况考虑:
直线和直线:
-
判断直线是否相交: \(A_1*B_2-A_2*B_1!=0\)则相交.
-
若相交则求交点: \((\frac{B_1*C_2-B_2*C_1}{A_1*B_2-A_2*B_1},\frac{A_2*C_1-A_1*C_2}{A_1*B_2-A_2*B_1})\)
直线和圆:
-
联立直线和圆方程(为了起见简便, 若\(B!=0\), 化为斜截式再联立), 求得系数\(tA\), \(tB\), \(tC\).
-
根据\(Delta=tB^2-4*tA*tC\)判断交点个数
-
若\(Delta\ge0\) , 根据求根公式求得交点横坐标, 进而求出交点.
圆和圆:
-
计算圆心距\(dis=\sqrt{(x_1-x_2)^2+(y_1-y_2)^2}\).
-
比较圆心距\(dis\) 和半径和\(r_1+r_2\), 半径差\(|r_1-r_2|\) .
-
若有交点则两圆相减求出相交弦方程, 进而转化为直线和圆得交点.
-
-
注意到直线、射线、线段的不同点仅在于其坐标范围不同。因此为Line类引入范围属性(\(R \leq x \leq S\))即可同时表示三种线,其中射线、直线的无穷端以宏INF表示。求交点时,将
Line
与Figure
求交点,求得的交点再判断是否在Line
所在范围内即可,若不在范围内则剔除。这就将直线、射线、线段统一了。
-
4. UML实体关系图
5. 计算模块接口部分的性能改进
- 改进之前,一分钟之内只能运算两百张图不到,运行一分钟(处理了大约150个图形)后性能探测截图:
可以看到,主要时间都花在了,PlaneContainer.insert
方法的set_union
函数上。
经查阅资料发现,在set_union函数中,存在了多次set复制,效率极低。于是修改了此处逻辑如下:
- 修改之后,可以在20秒左右完成2000个图形的计算,性能大大提高,与改前可谓“天壤之别”再次执行了性能探查:
此时PlaneContainer.insert
方法已经不耗费太多时间了。
6. Design by Contract & Code Contract
Design by Contract
和 Code Contract
是指契约式设计和代码遵守的契约,是按照某种规定对一些数据等做出约定,如果超出约定,程序将不再运行,例如要求输入的参数必须满足某种条件,否则不会运行。我们在声明一个函数/方法的时候,对函数的输入和输出所具备的性质是有所期望和规定的。有时候这种性质会被我们明确的写出来,有时候会被我们忽略掉。这些期望和规定就是Contract。
契约编程在面向对象设计课程中有所涉及,第一次接触 JML,我的反应是:这也太傻了,不就是帮程序员把程序用伪代码实现了吗!其实这并不对,契约式编程的重要原则就是推迟对于过程的思考。JML是在代码中增加了一些符号,这些符号只表述一个方法要干什么,并不关心它的实现过程。
这样的契约编程在当时看来是非常耗时的,我们需要首先在设计好接口的条件下,先对所有的函数仔细思考其“输入” 及 “输出”,再进行具体的编码。
我们在结对编程中由于时间关系,并没有使用契约式编程的方法。经过个人项目的磨练,core
中的核心代码涉及的函数在我们看来,其输入输出参数的条件都已经非常明了,特意地为了完成契约式编程的任务而改变相对更熟悉的工作方式,在我们看来是不明智的。
7. 计算模块部分单元测试展示
代码覆盖率情况:(使用VS2017 Enterprise代码覆盖率工具生成)
对项目中的类的单元测试覆盖率为 \(93.07\%\) ,对类 Circle 的覆盖率为 \(100\%\), 对类 Line 的覆盖率为 \(98.5\%\) 。
我们设置了针对功能的测试和针对异常的测试。
针对功能性测试,我们测试了圆、直线、线段和射线的一些函数在正常情况下的功能性,同时也构造了一些特殊情况下的测试用例,比如
// 射线 圆 内部相交
PlaneContainer pc;
pc.insert(new Circle(0, 0, 2));
pc.insert(new Line(1, 0, 2, 2, RL));
int count = pc.countIntersectionPoints();
Assert::AreEqual(count, 1);
// 射线 射线 一个交点
PlaneContainer pc;
pc.insert(new Line(0, 0, 1, 1, RL));
pc.insert(new Line(0, 0, -1, -1, RL));
int count = pc.countIntersectionPoints();
Assert::AreEqual(count, 1);
// 精度测试
PlaneContainer pc;
pc.insert(new Line(0, -100000, 1, 100000, SL));
pc.insert(new Line(0, 0, 0, 1, SL));
pc.insert(new Line(0, -99999, 1, -99999, SL));
int count = pc.countIntersectionPoints();
Assert::AreEqual(count, 3);
8. 计算模块部分异常处理说明
本次结对项目中,我们共设计了五类异常,
-
输入不满足标准格式
我们使用正则表达式来识别输入是否满足标准格式,正则表达式如下:
std::regex segREGEX("S\\s+-?\\d+\\s+-?\\d+\\s+-?\\d+\\s+-?\\d+\\s*\\n?"); std::regex lineREGEX("L\\s+-?\\d+\\s+-?\\d+\\s+-?\\d+\\s+-?\\d+\\s*\\n?"); std::regex rayREGEX("R\\s+-?\\d+\\s+-?\\d+\\s+-?\\d+\\s+-?\\d+\\s*\\n?"); std::regex circleREGEX("C\\s+-?\\d+\\s+-?\\d+\\s+-?\\d+\\s*\\n?");
-
参数不在标准范围 (-100000, 100000) 内
-
定义直线的两点重合
-
圆的半径不大于零
-
计算中出现无穷交点
-
直线、线段或射线与直线、线段或射线出现部分或完全重合
-
两圆完全重合
-
对于单元测试,我们对每一类异常场景及正常场景设计了3-4个测试样例,测试代码如下,
TEST_METHOD(TestMethod1)
{ // 格式错误
int res = add_Figure("acsd");
Assert::AreEqual(res, -1);
delete(pc);
}
TEST_METHOD(TestMethod2)
{ // 格式错误
int res = add_Figure("C 5 3 -2 1");
Assert::AreEqual(res, -1);
delete(pc);
}
TEST_METHOD(TestMethod3)
{ // 格式错误
int res = add_Figure("L -5 3 -2 0 4");
Assert::AreEqual(res, -1);
delete(pc);
}
TEST_METHOD(TestMethod4)
{ // 格式错误
int res = add_Figure("R 5 -3 2");
Assert::AreEqual(res, -1);
delete(pc);
}
TEST_METHOD(TestMethod5)
{ // 格式错误
int res = add_Figure("c 5 -3 2");
Assert::AreEqual(res, -1);
delete(pc);
}
TEST_METHOD(TestMethod6)
{ // 格式错误
int res = add_Figure("S 5 -3 2 0-1\n");
Assert::AreEqual(res, -1);
delete(pc);
}
TEST_METHOD(TestMethod7)
{ // 正常
int res = add_Figure("R 5 -3 2 3");
Assert::AreEqual(res, 0);
delete(pc);
}
TEST_METHOD(TestMethod8)
{ // 正常
int res = add_Figure("C 5 -3 3\n");
Assert::AreEqual(res, 0);
delete(pc);
}
TEST_METHOD(TestMethod10)
{ // 半径小于0
int res = add_Figure("C 5 -3 -2 \n");
Assert::AreEqual(res, 3);
}
TEST_METHOD(TestMethod11)
{ // 半径等于0
int res = add_Figure("C -5 -3 0\n");
Assert::AreEqual(res, 3);
}
TEST_METHOD(TestMethod12)
{ // 点超出坐标轴范围
int res = add_Figure("L 963214 -3 2 3\n");
Assert::AreEqual(res, 1);
}
TEST_METHOD(TestMethod13)
{ // 点超出坐标轴范围
int res = add_Figure("R 5 -3 526151 3\n");
Assert::AreEqual(res, 1);
}
TEST_METHOD(TestMethod14)
{ // 点超出坐标轴范围
int res = add_Figure("C -3 526151 3\n");
Assert::AreEqual(res, 1);
}
TEST_METHOD(TestMethod15)
{ // 两点重合
int res = add_Figure("R 2 -3 2 -3 ");
Assert::AreEqual(res, 2);
}
TEST_METHOD(TestMethod16)
{ // 两点重合
int res = add_Figure("L 99999 88888 99999 88888 \n");
Assert::AreEqual(res, 2);
}
TEST_METHOD(TestMethod17)
{ // 两点重合
int res = add_Figure("S 0 2 0 2 \n");
Assert::AreEqual(res, 2);
}
TEST_METHOD(TestMethod18)
{ // 无穷交点
add_Figure("L 2 2 3 3 ");
int res = add_Figure("L 0 0 -1 -1 \n");
Assert::AreEqual(res, 4);
}
TEST_METHOD(TestMethod19)
{ // 无穷交点
add_Figure("S 2 2 4 4 ");
int res = add_Figure("S 3 3 0 0 \n");
Assert::AreEqual(res, 4);
}
TEST_METHOD(TestMethod20)
{ // 无穷交点
add_Figure("R 1 1 4 4 ");
int res = add_Figure("S 2 2 0 0 \n");
Assert::AreEqual(res, 4);
}
TEST_METHOD(TestMethod21)
{ // 无穷交点
add_Figure("R 1 1 1 0 ");
int res = add_Figure("S 1 0 1 5 \n");
Assert::AreEqual(res, 4);
}
TEST_METHOD(TestMethod22)
{ // 无穷交点
add_Figure("C 1 1 1 ");
int res = add_Figure("C 1 1 1 \n");
Assert::AreEqual(res, 4);
}
9. 界面模块的详细设计过程 & 10. 界面模与计算模块的对接
-
界面模块是基于.NET 4.7.2 的窗体应用,采用C#语言编写。
-
除
Program
用于定义Main
函数外,整个模块一共只有类,下面结合图形、代码介绍该类的组成
namespace GUI
{
unsafe public partial class MainForm : Form
{
//下面为控件类实例,由VS自动生成
//Panel是主要的控件,用于绘图
private System.Windows.Forms.Panel panel1;
//打开输入文件对话框
private System.Windows.Forms.OpenFileDialog openFileDialog1;
//文件输入按钮和手工输入按钮
private System.Windows.Forms.Button inputButton;
private System.Windows.Forms.Label pathInput;//保存并显示输入文件路径
private System.Windows.Forms.Button button2;
//1-6用于获取手工输入图形的数据,7用于显示多行提示信息
private System.Windows.Forms.TextBox textBox1;
private System.Windows.Forms.TextBox textBox2;
private System.Windows.Forms.TextBox textBox3;
private System.Windows.Forms.TextBox textBox4;
private System.Windows.Forms.TextBox textBox5;
private System.Windows.Forms.TextBox textBox6;
private System.Windows.Forms.TextBox textBox7;
//以下label用于显示单行信息
private System.Windows.Forms.Label num;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.Label label3;
private System.Windows.Forms.Label label4;
private System.Windows.Forms.Label label5;
private System.Windows.Forms.Label label6;
private System.Windows.Forms.Label label7;
private System.Windows.Forms.Label label8;
private System.Windows.Forms.Label label9;
//下面为用户变量
//绘图画笔
private Pen pen;
//绘图类
private Graphics g;
//直线容器,每四个数字代表一条直线
private LinkedList StraightLines;
//射线容器,每四个数字代表一条射线
private LinkedList RayLines;
//线段容器,每四个数字代表一条线段
private LinkedList LineSegments;
//圆形容器,每三个数字代表一个圆形
private LinkedList Circle;
//下面的五个函数皆为从core.dll中导入的接口函数
//添加图形并返回添加结果
[DllImport("core.dll")]
private static extern int add_Figure(StringBuilder buf);
//初始化容器
[DllImport("core.dll")]
private static extern void initial_PlaneContainer();
//摧毁容器
[DllImport("core.dll")]
private static extern void dispose_PlaneContainer();
//获取交点序列
[DllImport("core.dll")]
private static extern double* get_IntersectionPoints();
//获取交点数目
[DllImport("core.dll")]
private static extern int get_NumOfIntersectionPoints();
//下面为成员方法(包括回调函数)
//构造方法,主要为变量分配。
public MainForm();
//窗体打开和关闭的回调,用于显示欢迎和再见提示
private void MainForm_Load(object sender, EventArgs e);
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
//输入按钮的回调函数,用于处理文件输入
private void inputButton_Click(object sender, EventArgs e);//文件输入
private void button2_Click(object sender, EventArgs e)//手工输入
//画板的重绘回调函数,用于绘制图形
private void panel1_Paint(object sender, PaintEventArgs e);
//处理add_Figure函数返回来的错误代码
private bool processRetVal(int retCode, string line);
//存储图形,以供绘图函数使用
private void store(string buf);
//四个小的绘图函数
private void drawLine();
private void drawCircle();
private void drawRay();
private void drawLineSegment();
//坐标系变换函数
private void change(ref Point p);
private void change(ref PointF p)
}
}
-
重点介绍以下几个函数的运行逻辑
-
private void inputButton_Click(object sender, EventArgs e);//文件输入
- 若打开不成功则不进行处理并给出提示。
-
否则,按行循环读取文件送
string line
,调用add_Figure
处理,并获取处理结果送int retCode
。 -
将
line
和retCode
交给processRetVal
处理。 -
通过
get_NumOfIntersectionPoints
更新交点数目并显示
-
private void button2_Click(object sender, EventArgs e);//手工输入
- 通过6个
TextBox
输入框获取输入数据。 - 根据LRSC输入框输入的类型代码,将LRSC和后面的相应输入框中内容组成
string line
,调用add_Figure处理,并获取处理结果送int retCode
。 - 将
line
和retCode
交给processRetVal
处理。 - 通过
get_NumOfIntersectionPoints
更新交点数目并显示
- 通过6个
-
private bool processRetVal(int retCode, string line);
-
若
retCode==0
,未发生异常,调用store
将line
存储。 -
否则,不进行存储并提示相应的错误,提示用户,例如。
private void panel1_Paint(object sender, PaintEventArgs e);
- 绘制坐标系、刻度等。
- 调用
drawLine
、drawCircle
、drawRay
、drawSegment
绘制相应图形。
-
-
模块对接
- core模块不提供内存管理,由接口函数
init_PlaneContainer
和dispose_PlaneContainer
管理。 - 核心接口add_Figure供界面模块调用。
- 正则匹配,如果格式不符合要求,不进入下一步并返回相应的错误代码。否则予以解析进入下一步。
- 根据解析结果,构造相应的
Figure
并捕获可能的异常,如果构造过程中出现两点重合、半径非正等异常,不进入下一步,并反回相应的错误代码。 - 将构造的
Figure
假如容器pc
,并捕获可能的异常,如果出现无穷交点等异常,不进入下一步,并返回相应错误代码。 - 正常返回。代码代码0;
- core模块不提供内存管理,由接口函数
-
使用图例
11. 结对过程描述
我们在结对过程中使用了 Visual Studio 中的 Live Share 和微信进行交流,截图如下。
12. 结对编程的优缺点
优点:
- 结对编程中,两个人都必须对代码熟悉,所以两个人都处于对代码不断地处于 “复审‘ 的过程,不断地审核、提高的过程。这样可以提高代码质量。
- 结对编程可以让程序员更专注于工作,更注重代码质量、代码风格等。
结对编程的过程也是一个互相督促的过程,每个人的一举一动都在别人的视线之内,所有的想法都要受到对方的评价。由于这种督促的压力,使得程序员更认真地工作。
- 结对编程使工作更容易、简单,因为一个人工作时遇到困难很容易陷入颓废无力的境况。而结对编程中,往往比较少产生两个人都无力解决的问题,即使遇到了,双方也会互相鼓励,而不是陷入恶性循环。
缺点:
- 结对编程中如果双方的水平差不多,就能具有更高的效率。但如果双方的水平差距较大,则会出现强的一方拉着弱的一方走的情形。对于强的一方,可能承包了大部分工作,既累又没办法保障代码的质量;;而对于弱的一方,没有参与感,对项目一知半解,更产生了挫败感。在这种情况下,双方的交流也不在同一个层次上,结对编程不如独自编程。
结对伙伴的优点:
- 有责任心
- 自学能力强
- 思路清晰
结对伙伴的缺点:
- 有点贪玩,使项目完成时间略微滞后
我的优点:
- 有责任心
- 认真
- 经常主动交流
我的缺点:
- 没有脑子,做不了设计,只能跟着队友走