结对项目作业
写在前面:
项目 | 内容 |
---|---|
这个作业属于哪个课程 | 2020计算机学院软件工程(罗杰 任健) |
这个作业的要求在哪里 | 结对项目作业 |
教学班级 | 006 |
项目地址 | https://github.com/CrapbagMo/PairProgramIntersect |
在开始实现程序之前,在下述 PSP 表格记录下你估计将在程序的各个模块的开发上耗费的时间。
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 30 |
· Estimate | · 估计这个任务需要多少时间 | 30 | 30 |
Development | 开发 | 1020 | 1050 |
· Analysis | · 需求分析 (包括学习新技术) | 90 | 120 |
· Design Spec | · 生成设计文档 | 60 | 60 |
· Design Review | · 设计复审 (和同事审核设计文档) | 30 | 30 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 40 | 50 |
· Design | · 具体设计 | 120 | 120 |
· Coding | · 具体编码 | 300 | 280 |
· Code Review | · 代码复审 | 60 | 120 |
· Test | · 测试(自我测试,修改代码,提交修改) | 300 | 420 |
Reporting | 报告 | 50 | 90 |
· Test Report | · 测试报告 | 20 | 50 |
· Size Measurement | · 计算工作量 | 10 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 20 | 30 |
合计 | 1050 | 1350 |
看教科书和其它资料中关于 Information Hiding,Interface Design,Loose Coupling 的章节,说明你们在结对编程中是如何利用这些方法对接口进行设计的。
-
在设计的所有类中,都将属性设置为了
private
,通过setX
,getX
等方法访问。 -
全局静态变量
PlaneContainer* pc
通过init_PlaneContainer
和dispose_PlaneContainer
方法访问,对外隐藏了指针。 -
在接口向外传递异常时,考虑到
C#
的异常类和C++
的异常类并不兼容。我们也没有什么好办法解决这个问题,于是决定采用传统C
用返回值传递错误状态的思想。使用返回值来传递异常。 -
在core.dll中,我们并不依赖任何外部项,完全依靠自身即可完成对外接口提供的功能。
计算模块接口的设计与实现过程。 设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图?说明你的算法的关键(不必列出源代码),以及独到之处。
-
类及函数
-
五个主要类:
点类:
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
所在范围内即可,若不在范围内则剔除。这就将直线、射线、线段统一了。
-
阅读有关 UML 的内容:https://en.wikipedia.org/wiki/Unified_Modeling_Language。画出 UML 图显示计算模块部分各个实体之间的关系(画一个图即可)。
-
UML实体图:
计算模块接口部分的性能改进。记录在改进计算模块性能上所花费的时间,描述你改进的思路,并展示一张性能分析图(由VS 2015/2017的性能分析工具自动生成),并展示你程序中消耗最大的函数。
-
改进之前,一分钟之内只能运算两百张图不到,运行一分钟(处理了大约150个图形)后性能探测截图:
可以看到,主要时间都花在了,
PlaneContainer.insert
方法的set_union
函数上。经查阅资料发现,在set_union函数中,存在了多次set复制,效率极低。于是修改了此处逻辑如下:
修改之后,可以在20秒左右完成2000个图形的计算,性能大大提高,与改前可谓“天壤之别”再次执行了性能探查:
此时
PlaneContainer.insert
方法已经不耗费太多时间了。
看 Design by Contract,Code Contract 的内容:
http://en.wikipedia.org/wiki/Design_by_contract
http://msdn.microsoft.com/en-us/devlabs/dd491992.aspx
描述这些做法的优缺点,说明你是如何把它们融入结对作业中的。
-
关于 Design by Contract,Code Contract
-
优点
-
获得更优秀的设计
可以启发程序员思考程序的先验条件,有助于了理清思路
契约双方的权力和义务得到了共享,同时获得了清晰的描述
-
提高可靠性
普遍采用断言机制,可靠性高
帮助开发者更好地理解代码
方便测试,随调随用
-
是优秀的文档
好的契约设计,代码本身即是注释,即是文档
契约设计是精确的规范
-
复用性高
-
-
缺点
-
需要花费时间学习契约思想和技术
-
契约代码的开发会更加耗时(但可以节省测试,编写文档时间)
-
需要大量时间
-
只适用于顺序程序,不适用于并行程序。
-
-
-
在本项目中的应用
-
在
add_Figure
函数调用其他函数的设计中,我们使用了契约方式,约定,add_Figure
只负责传递格式正确的格式给其他的函数,其他的函数不再做格式检查。 -
PlaneContainer.insert方法中也采用了契约方式,约定,
PlaneContainer.insert
方法只负责插入一个合法的Figure
,但不保证是否会和已有的Figure
冲突,这需要由他所调用的方法检查。
-
计算模块部分单元测试展示。展示出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路。并将单元测试得到的测试覆盖率截图,发表在博客中。要求总体覆盖率到 90% 以上,否则单元测试部分视作无效。
-
代码覆盖率情况:(使用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);
-
针对异常的测试用例见下一部分。
-
计算模块部分异常处理说明。在博客中详细介绍每种异常的设计目标。每种异常都要选择一个单元测试样例发布在博客中,并指明错误对应的场景。
-
本次结对项目中,我们共设计了七类异常
-
输入不满足标准格式
我们使用正则表达式来识别输入是否满足标准格式,正则表达式如下:
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); }
界面模块的详细设计过程。在博客中详细介绍界面模块是如何设计的,并写一些必要的代码说明解释实现过程。
界面模块与计算模块的对接。详细地描述 UI 模块的设计与两个模块的对接,并在博客中截图实现的功能。
-
这两个问题内容有相关联的地方故,合起来说。
-
界面模块是基于.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);//文件输入
-
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);
-
private void panel1_Paint(object sender, PaintEventArgs e);
- 绘制坐标系、刻度等。
- 调用
drawLine
、drawCircle
、drawRay
、drawSegment
绘制相应图形。
-
-
模块对接
-
core模块不提供内存管理,由接口函数
init_PlaneContainer
和dispose_PlaneContainer
管理。 -
核心接口add_Figure供界面模块调用。
-
正则匹配,如果格式不符合要求,不进入下一步并返回相应的错误代码。否则予以解析进入下一步。
-
根据解析结果,构造相应的
Figure
并捕获可能的异常,如果构造过程中出现两点重合、半径非正等异常,不进入下一步,并反回相应的错误代码。 -
将构造的
Figure
假如容器pc
,并捕获可能的异常,如果出现无穷交点等异常,不进入下一步,并返回相应错误代码。 -
正常返回。代码代码0;
-
-
使用图例
描述结对的过程,提供两人在讨论的结对图像资料(比如 Live Share 的截图)。关于如何远程进行结对参见作业最后的注意事项。
-
在项目一开始,我们采用了“领航与-驾驶员”模式,但是在实验一下午之后,由于网络等原因,我们发现“领航员-驾驶员”模式工作效率不太高,于是我们采用了独立写然后互审,互测的方式,工作效率有所提高,并一直沿用到结束。
-
我们在结对过程中使用了 Visual Studio 中的 Live Share 和微信进行交流,截图如下:
看教科书和其它参考书,网站中关于结对编程的章节,例如:http://www.cnblogs.com/xinz/archive/2011/08/07/2130332.html ,说明结对编程的优点和缺点。同时描述结对的每一个人的优点和缺点在哪里(要列出至少三个优点和一个缺点)。
-
关于结对编程
-
优点
-
互相鼓励,不容易沮丧。
-
互相监督,不容易偷懒。
-
互相学习,双方都能在编程中学到东西。
-
两个人随时复审,降低 bug产出率。
-
-
缺点
-
与合不来的人容易发生冲突,导致团队矛盾。
-
双方技术实力差距过大,可能会使得另一方产生不满情绪。
-
观点出现分歧容易发生争吵。
-
开发时可能会闲谈、走神,降低效率。
-
-
-
关于结对双方
优点 缺点 我 有耐心
代码风格良好
善于交流粗心 结对伙伴 细心
时间观念好
逻辑清晰不善交流
在你实现完程序之后,在附录提供的PSP表格记录下你在程序的各个模块上实际花费的时间。
见上文PSP表格。