软件工程结对编程作业

项目 内容
本次作业所属课程 2019BUAA软件工程
本次作业要求 结对编程作业
我在本课程的目标 熟悉结对编程流程
本次作业的帮助 实践了结对编程的流程,对结对编程的优缺点有了更深的体会
本次作业项目github地址 项目地址

1.本次作业项目github地址

项目地址

2.开发前PSP表

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

3.接口设计方法

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

我认为这三个方法是相似的,首先无论是面向过程编程中的函数的概念还是面向对象中的封装的思想都是信息隐藏的原则的体现,这个原则强调代码模块应该采用定义良好的接口来封装(模块的内部结构仅由负责开发的程序员关注),也体现了接口抽象的原则。松耦合原则的也被人称作“高内聚,低耦合”原则,第一次接触这个概念是高老师在计组课上提出CPU这种工程设计模块的时候一定要遵循高内聚低耦合的原则,强调一个代码单元都需要是独立的。在我们的结对编程中,我们采用了类将实现过程封装,类的大部分方法和成员变量对外部均不可见的,符合了信息隐藏的原则。同时将整个项目按照功能划分成一个个模块,不同功能的部分被分成不同的部分。接口的返回值以及输入参数均是基本类型,生成的dll文件也可以适用于其他程序,所以符合低耦合的标准。

4. 计算模块接口的设计与实现过程

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

计算模块是整个项目的核心,发现这个模块计算的内容有很强的聚合性,因此采用面向对象的方式进行封装,最后抽象出一个实体计算类Solver以及接口类Core,共两个类。Core类的接口每次会实例化一个Solver对象,然后调用max_chain_word函数进行计算。画出一个调用的流程图:

算法大致思路:首先拿到问题需要对原问题进行建模,一条单词链相邻两个单词的特征是,前一个单词的末尾等于后一个单词的头,这个可以通过有向图的有向边实现。对于不出现-r参数的情况相当于求图中一些指定点作为起点,一些点作为终点的最大路径问题(采用了一个经典的SPFA算法)。对于出现-r参数的情况归约为求有向有环图中的最大路径问题,属于一个非常经典的NP问题,对于这种情况并没有多项式时间的准确算法,因此采用dfs搜索算法。

算法的独到之处:采用一图多权值的方法,因为原问题有求最长单词数和字符数两种需求,所以在两个节点边的权值记录了两个,求最大单词数目时使用权值为1,求最长单词数使用权值为目的节点的字符长度。这样对于两种问题的求解方法是基本一样的,只是计算不同的边权,很好的做到了代码复用。对于输入的单词列表转化为图之后首先采用拓扑排序的方法判断图是否有环,对于-r参数的情况下不一定调用dfs搜索(当判断出图中无环时仍然采用时间复杂度低的SPFA算法)。

5.UML图

阅读有关UML的内容:https://en.wikipedia.org/wiki/Unified_Modeling_Language。画出UML图显示计算模块部分各个实体之间的关系(画一个图即可)。

VS2017可以下载安装包,支持自动生成类图,参考博客 。最后生成的类图如下:

6.计算模块接口部分的性能改进

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

在完成基本程序的运行功能之后,我对程序的性能进行了一定的改进,在这个过程中是很痛苦的,大概花了两天的时间,其中也走了很多弯路,最后在无环图上优化取得了一定效果,但是有环图改进的幅度很小。

改进的思路:对于无环的情况,尽可能采用时间复杂度比较低的算法,权衡再三最后将最终算法改为SPFA算法。对于有环的情况,因为跟许多同学讨论证实这是一个NP问题,所以能做的改进也是很有限,主要的思路就是在dfs搜索的时候能剪枝,以及在某些特殊的情况下需要进行特殊处理(比如如果判断出无环,即使带-r参数要调用SPFA函数而不是暴力搜索,以及如果有环的情况下找到一个长度为n的链(n为节点总数)也可以停止搜索得到答案)。对于有环的情况,实际上还可以考虑一些启发式算法比如模拟退火算法等,但是本题目需要输出准确解,启发式算法有一定风险,因此没有采用该方法。

这部分主要有两种情况,一个是没有-r参数的时候调用SPFA方法时的性能,

下面展示一个较小数据集下不带-r的性能分析图:

发现其中耗时最多是的生成图的函数,说明计算算法性能基本可以。

一个是带有-r参数且图中有环会频繁递归调用DFS函数的情况,这一部分毫无疑问是dfs函数被多次递归调用。


7.Design by Contract, Code Contract

看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,又称契约式设计,是面向对象程序设计的一大原则。这个设计原则需要我们使用三类断言:前提条件,后继状态和不变量。前提条件是指执行某种操作之前期望具备的环境,后继状态是满足前提条件在方法结束退出时系统所拥有的状态。

优点:契约式设计强调了调用者和被调用者地位的平等性,双方都需要履行一定的义务,在传统的C/S设计模式下,被调用者往往期望处理一切可能的异常,对调用者不做约束,往往造成调用方代码质量很差。契约式设计要求调用者调用前准备好正确的参数,被调用者需要保证正确的结果和不变形,保证了双方的代码质量。

缺点:对于程序设计语言有一定要求,DBC需要断言来验证契约是否成立,但是并不是所有的程序设计语言都有断言机制。

结对编程作业中,对异常的捕捉都由一个函数进行处理,把类的大部分方法都封装成私有方法,调用的时候会满足隐含前置条件,对于接口的前置进行了约束。模块之间以及函数之间采用了契约的思想,保证了双方在一次函数调用的时候都需要承担一定的责任。

8.计算模块部分单元测试展示

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

计算模块单元测试覆盖率达到了98%,截图如下

单元测试分为两大部分,一个是对正常情况下的处理,一个是对异常处理,异常部分的单元测试请参见博客第九部分。正常情况下的测试分为三种,一个是无环无-r参数,一个无环有-r参数,一个是有环有-r参数。对前两种,测试了最大单词数和最大字符数结果相同、结果不同的情况,同时也测试了不同的头尾对结果的限制。对于第三种有环的情况,也测试所有无环的情况,同时测试了图中有多个环的情况。除上面的常规测试之外还增加了边界测试,比如最后的结果只有一个单词(不能构成单词链),以及输入长度为0的情形。展示部分单元测试代码如下:

char *test_list1[] = { "abc","cbd","dbbw","csw","zde","opl","wxx" };
char *test_list2[] = {"room", "mazhenya", "apple", "elephant","mahaoxiang","gxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "zzzzzzzzzzzzzzzzzzzzzzzzzzzzorange","peanut"};
char *test_list3[] = {"abc","cbd","bba","ddb","yz","uv","wx","vw","xy"};
char *test_list4[] = { "xppppy", "fb","ef","bc","de","cd","zpppb","yppppz" };
char *test_list5[] = { "xppppy", "fb","ef","bc","ft","tb","zpppb","yppppz" };
char *test_list6[] = { "xppppy", "ce","ef","bc","de","cd","zpppb","yppppz" };
char *test_list7[] = { "tppppppppz","zppppppppx","ab","ef","bc","de","cd" };
char *test_list8[] = { "ac","bz","cb","cc" };
char *test_list9[] = { "ac","bc","cb","cc" };//有环
char *test_list10[] = { "cx","xy","bc","zd","yz","ab","de","cpppppppppppppppppppd" };
char *test_list11[] = {"abc", "abc", "xyz"};
TEST_METHOD(TestMethod1)
		{
			
			char *answer1[] = { "abc","cbd","dbbw","wxx" };
			int word_num = 7;
			int answer_num = 4;
			char **results1 = new char*[word_num + 1];
			int res = Core::gen_chain_word(test_list1, word_num, results1, 0, 0, false);
			Assert::IsTrue(judge(array2string(results1,res), array2string(answer1,answer_num)));
			for (int i = 0; i < res; i++)
				delete[] results1[i];
			delete[] results1;
		}

		TEST_METHOD(TestMethod2)
		{
			int word_num = 7;
			int answer_num = 2;
			char **results2 = new char*[word_num + 1];
			char *answer2[] = { "dbbw", "wxx" };
			int res = Core::gen_chain_word(test_list1, 7, results2, 'd', 0, false);
			Assert::IsTrue(judge(array2string(results2, res), array2string(answer2, 2)));
		}

		TEST_METHOD(TestMethod3)
		{
			int word_num = 7;
			int answer_num = 3;
			char **results3 = new char*[word_num + 1];
			char *answer3[] = { "abc","cbd","dbbw"};
			int res = Core::gen_chain_word(test_list1, word_num, results3, 0, 'w', false);
			Assert::IsTrue(judge(array2string(results3, res), array2string(answer3, answer_num)));
		}
		TEST_METHOD(TestMethod4) {
			int word_num = 8;
			int answer_num = 4;
			char **results4 = new char*[word_num + 1];
			char *answer4[] = { "room", "mazhenya", "apple", "elephant" };
			int res = Core::gen_chain_word(test_list2, word_num, results4, 0, 0, false);
			Assert::IsTrue(judge(array2string(results4, res), array2string(answer4, answer_num)));
		}
		TEST_METHOD(TestMethod5) { 
			int word_num = 8;
			int answer_num = 3;
			char **results5 = new char*[word_num + 1];
			char *answer5[] = { "mazhenya", "apple", "elephant" };
			int res = Core::gen_chain_char(test_list2, word_num, results5, 'm', 't', false);
			Assert::IsTrue(judge(array2string(results5, res), array2string(answer5, answer_num)));
		}
		TEST_METHOD(TestMethod6) {// -c 以t结尾
			int word_num = 8;
			int answer_num = 2;
			char **results = new char*[word_num + 1];
			char *answer[] = { "zzzzzzzzzzzzzzzzzzzzzzzzzzzzorange", "elephant" };
			int res = Core::gen_chain_char(test_list2, word_num, results, 0, 't', false);
			Assert::IsTrue(judge(array2string(results, res), array2string(answer, answer_num)));
		}
		TEST_METHOD(TestMethod7) {
			int word_num = 8;
			int answer_num = 2;
			char **results = new char*[word_num + 1];
			char *answer[] = { "mahaoxiang","gxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"};
			int res = Core::gen_chain_char(test_list2, word_num, results, 'm', 0, false);
			Assert::IsTrue(judge(array2string(results, res), array2string(answer, answer_num)));
		}
		TEST_METHOD(TestMethod8) {
			int word_num = 9;
			int answer_num = 5;
			char **results = new char*[word_num + 1];
			char *answer[] = { "uv","vw","wx","xy","yz" };
			int res = Core::gen_chain_word(test_list3, word_num, results, 0, 0, true);
			//output(results, res);
			Assert::IsTrue(judge(array2string(results, res), array2string(answer, answer_num)));
		}

		TEST_METHOD(TestMethod9) {
			int word_num = 9;
			int answer_num = 4;
			char **results = new char*[word_num + 1];
			char *answer[] = { "abc","cbd","ddb","bba" };
			int res = Core::gen_chain_word(test_list3, word_num, results, 0, 'a', true);
			//output(results, res);
			Assert::IsTrue(judge(array2string(results, res), array2string(answer, answer_num)));
		}

9.计算模块部分异常处理说明

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

异常1 有环无-r参数 异常发生在输入文件中存在单词环,但是命令设置不支持-r参数,通常发生在忘记设置-r参数的情景下。设计目标是提醒用户检查输入文本或者参数设置是否有误。以下是一个测试循环异常的单元测试用例。

char *test_list3[] = {"abc","cbd","bba","ddb","yz","uv","wx","vw","xy"};
TEST_METHOD(TestMethod10) {
			try {
				int word_num = 9;
				int answer_num = 4;
				char **results = new char*[word_num + 1];
				int res = Core::gen_chain_word(test_list3, word_num, results, 0, 0, false);
				Assert::IsTrue(res == -1);
			}
			catch (const char*  s) {
				Assert::IsTrue(strcmp(s, LOOP_ERROR) == 0);
				cout << s << endl;
			}
	}

异常2 输入的头尾字符不符合题目要求异常发生在用户指定的头尾字符不是0也不是小写字母,目的是提醒用户检查设置的头尾字符限制参数。以下是一个头尾字符异常的单元测试。

char *test_list3[] = {"abc","cbd","bba","ddb","yz","uv","wx","vw","xy"};
try {
				int word_num = 9;
				int answer_num = 4;
				char **results = new char*[word_num + 1];
				int res = Core::gen_chain_char(test_list3, word_num, results, '+', '-', true);
				Assert::IsTrue(res == -1);
			}
			catch (const char*  s) {
				Assert::IsTrue(strcmp(s, TAIL_CHAR_ERROR) == 0);
				//Assert::IsTrue(s == LOOP_ERROR);
				cout << s << endl;
			}

异常3 输入的单词列表中含非法单词 异常发生在用户直接调用接口的时候,没有保证每一个单词都是由小写英文单词组成,因此在计算之前也需要进行单词合法性检查。

TEST_METHOD(TestMethod12) {
			char *test_list[] = {"happ1we2", "yuer", "opui8op"};
			try {
				int word_num = 3;
				int answer_num = 0;
				char **results = new char*[word_num + 1];
				int res = Core::gen_chain_word(test_list, word_num, results, 0, 0, false);
			}
			catch (const char* s) {
				Assert::IsTrue(strcmp(s, WORD_ILLEGAL)==0);
				cout << s << endl;
			}
	}

其余还有一些参数错误等异常均在命令行输入处理模块进行处理。

10.界面模块的详细设计过程

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

对于界面模块的设计,采用了MFC程序来实现。整个UI界面包括输入,输出,参数选择三个部分,具体如下图所示。

下面对部分MFC元素的实现作解释说明。

(1)选择文件输入还是界面输入的CombBox框

comb_choose_input.AddString(_T("选择文件输入(默认)"));
comb_choose_input.AddString(_T("直接输入"));
comb_choose_input.SetCurSel(0);

在初始化时插入选择枝,并设定默认选择为文件输入,在点击运行后的时间通过GetCurSel()方法来获取用户的选择。

(2)参数选择区mode单选框

使用

GetCheckedRadioButton(IDC_max_word, IDC_max_length)

来判断选择枝

(3)文本框(包括输入,输出,head,tail)

采用

GetDlgItem(ID)->GetWindowText(Cstring)

来获取文本框中的文本,需要注意的是,文本内容获取到后类型为Cstring,可用

CT2A(Cstring.GetBuffer())

来转换Cstring为string。

(4)选择(保存)文件的按钮

使用

CString strFile = _T("");
CFileDialog    dlgFile(TRUE, NULL, NULL, OFN_HIDEREADONLY, _T("Describe Files All Files (*.*)|*.*||"), NULL);
if (dlgFile.DoModal())
{
	strFile = dlgFile.GetPathName();
	file_path = CT2A(strFile.GetBuffer());
}

来调用MFC内封装的选择文件UI,如果要保存文件直接打开文件并将内容写入即可。

11.界面模块与计算模块的对接

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

UI模块的运行

在UI模块的运行按钮中绑定事件,点击后,UI模块获取界面上用户输入的信息,转换为相应的参数来调用。

需要注意的是,在运行UI模块时,并不支持通过命令行输入参数进行测试,请直接打开UI模块(MFC_max_word_chain.exe)。如果希望通过命令行打开加参数的方式进行测试,请使用max_word_chain.exe。

对接方式

UI模块在资源文件中引入Core.lib,在MFC_max_word_chainDlg.cpp这个事件响应文件中,需要调用计算模块时,头文件引入Core.cpp。

处理好用户界面输入的信息后,调用

static int gen_chain_word(char* words[], int len, char* result[], char head, char tail, bool enable_loop);
static int gen_chain_char(char* words[], int len, char* result[], char head, char tail, bool enable_loop);

这两个接口进行运算。运算后将result中获取的结果转换为Cstring返回到用户界面。
最终实现的功能如下:

12.结对的过程

描述结对的过程,提供非摆拍的两人在讨论的结对照片。

我们两人的结对编程,经历了顺利的开端,略微坎坷的经过和最后圆满的结束。

起初最开始接触结对编程,我们按照领航员与驾驶员的模式。首先两人一起商讨了关于最长链算法的问题,并在有环时如何处理上进行了一段时间的探讨。随后按照驾驶员写代码,领航员检查设计并监督的方式进行,开始效果良好,避免了不少人为bug的产生。

在几次身份对换之后,我们发现,有时在他人的代码基础之上继续完成自己的代码十分困难。思路不断交替,写代码的效率有了一定的下降。在同伴在自己的代码基础上进行完成时,若完成的结果和自己的思路有些冲突,还会造成一些麻烦。好在项目比较小,我们两方也都比较认真且有团队精神,在不断的磨合之中,适应了对方的步伐,磨合的也越来越好。在最后例如单元测试,错误处理等模块的实现中,我们的结对编程比较顺利。

总体来说这次结对编程是一次不错的体验,与他人一起交流,互相交换编程思想的经历难能可贵。虽然在效率上可能有些问题,但最终项目的完成质量不错就是好的。

13.结对编程反思

看教科书和其它参考书,网站中关于结对编程的章节,例如:
http://www.cnblogs.com/xinz/archive/2011/08/07/2130332.html
说明结对编程的优点和缺点。
结对的每一个人的优点和缺点在哪里 (要列出至少三个优点和一个缺点)。

结对编程的优点:

1> 提供更好的设计质量,一个不合理的设计一般不会得到两个人的支持,比如在这次结对编程的过程中,尤其是在性能优化部分,我提出的一些优化方法总是能很快得到pair的反馈(支持或者举出反例),在设计的时候避免不合理的想法能够极大降低试错所带来的代价。

2> 得到更好的代码质量以及减少bug出现的几率,在这次结对编程中在写代码的时候如果没有遵循规范的时候PAIR会及时询问代码含义,督促每个人遵循良好的代码规范,同时PAIR会指出很多写代码过程中实现细节的bug。

3> 有利于两个结对编程的人互相交流学习和传递经验。

结对编程的缺点:

1> 两个人一起编程的时候会有很多思维上的碰撞,每个人的思考方式不一样,也比较容易出现对彼此的意见产生疑问无法融合的情况。

2> 有可能出现结对编程逐渐演变成单人工作的情形。

队友的优缺点

优点:学习能力很强,对于之前没有接触过的技术能够快速查找资料并实现;工作能力很强,能够基本按时完成相关的计划;很有自己的想法,对于项目的每一步都有思考。

缺点:有时候有点懒散,需要不定时提醒进度和计划。

自己的优缺点

优点:因为有班干部的经历,所以沟通交流能力较强;有时间观念,对计划的执行力较强;能够及时与队友沟通交流自己的想法与问题。

缺点:

算法能力有限,到最后也没有找到300s内跑出100个点的有向有环图的最长链问题。

14.PSP表格回填

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

注:有些部分的计时我也不是很确定究竟算进哪部分比较合理,比如为了想实现300s跑100个有向有环图的点,查找了很多算法,也亲自动手写了一些进行验证,因此这部分一部分放在设计里面,一部分算进测试里面了。

15 模块交换与模块松耦合

因为有两个小组的邀请,所以最后和两个小组进行了模块交换,因为跟第一个小组中出现了问题并进行了修改,所以以下博客展示与第一个小组的交换出现的问题。

合作小组1成员: 16061192 汪慕澜 16061103 赵智源

合作小组2成员: 16021160 庄廓然 15061078 杨帅

问题: 问题主要起源于我对接口的理解有误,在第一版程序的时候将类的两个函数public化作为了一个接口,而且这个类是有状态的。但是发现对方的直接声明了一个没有状态的类,声明了类的static方法。之前自己的写法会产生很多warning(warning提示调用接口需要声明一个类的客户端对象),在对面小组的解释后对自己的接口设计进行了重构,对接口又进行了一次封装。

交换效果:

我们的程序在对方的gui上可以完成运行,效果如下:

对方的程序也可以在我们的gui上完成运行: