软工结对项目

作业信息

教学班级:周五班
项目地址:https://github.com/buaddd/buaa_SE_2022_Pair_Program

PSP表格

Personal Software Process Stages预估耗时(分钟)实际耗时(分钟)
计划60120
估计这个任务需要多少时间3030
开发12001500
需求分析 (包括学习新技术)180120
生成设计文档180120
设计复审 (和同事审核设计文档)3060
代码规范 (为目前的开发制定合适的规范)3060
具体设计180180
具体编码600600
代码复审180120
测试(自我测试,修改代码,提交修改)300300
报告180180
测试报告120120
计算工作量3060
事后总结, 并提出过程改进计划6060
合计56 * 6060.5 * 60

项目设计与实现

项目结构与UML图

输入输出,建图,异常检查
本项目采用面对对象的思路进行设计,将不同的部分封装成不同类,结构如下所示

  • MyIO: 负责文件的读取与计算结果的输出,保存读入的所有字符串
  • Node, Word_vertex: 分别针对不允许环路与允许环路的情况,保存单词相关的信息,例如首尾的字母,后继节点等内容
  • Generator:根据字符串与参数情况生成Node与Word_vertex对象,建图
  • Checker: 进行环路检测,对于非-r的情况,如果检测到了环路,需要退出程序并报错
    在这里插入图片描述

Core相关计算类
在这里插入图片描述

  • Core: 封装本次作业的计算接口
  • Node_chain_builder与Word_chain_builder: 负责构建单词链,使用graph中的信息,构建单词链
  • Node_graph与Word_graph:保存所有节点,存储相关的信息,供生成者调用,辅助生成单词链
  • Node_chain与Word_chain:存储计算得到的单词链结果
  • Word_tarjan_vertex:代表计算过程中某一类Word_vertex的特殊情况

UML图
在这里插入图片描述

接口设计与实现

Information Hiding,Interface Design,Loose Coupling

  • Information Hiding:信息隐藏在面对对象的设计中十分重要,要求每个类保管其内部的数据,并对外提供相应的接口,这样数据更加安全,同时解耦的更加完全,在本次作业中,我们将数据封装起来,构造出例如Node类来保管字符串相关的信息,满足Information Hiding的设计要求。
  • Interface Design:接口设计对于日常开发的规范十分重要,好的接口设计可以言简意赅地表述清楚其作用,并实现相关内容。在涉及到多人合作的部分时,接口设计对于不同部分的合并也起着至关重要的作用,本次作业中在线上合作时,有些部分是分开完成的,例如输入输出与构造单词链的部分,就可以在提前沟通好接口的情况下,同时进行实现。
  • Loose Coupling:松耦合可以使一个类与另外一个类隔开,它们之间只是通过消息来联系,所以设计一类时,可以不用担心破坏另外一个类,我们在设计中进行了多次解耦,最后划分出例如IO类进行输入输出,Node类报存信息,Generator类建图等,模块之间耦合度都较低。

计算模块接口的设计与实现过程
本次作业要实现的计算模块接口如下:

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

我们将其分为无环与有环两种情况去处理:

  • 对于无环的情况,由于没有环路出现,因此可以将所有类似a…b结构的单词归纳为一类,并存储在Node节点中,同时建图过程中存储每个Node的后继Node。
  • 对于有环的情况,上文所述的同结构单词可以重复利用,因此不能再压缩所有单词,需要对每一个单词生成一个Word_vertex类,同时建图过程中存储每个Word_vertex的后继Word_vertex。

针对Node类和Word_vertex类,我们分别设计Node_chain类和Word_chain类来封装两种数据类的链信息,并设计Node_chain_builder类和Word_chain_builder类来实现两种链的构造。

对于接口实现的4种功能,gen_chain_unique 和 gen_chains_all 仅涉及无环情况,只需要调用 Node_chain_builder 类;而 gen_chain_word 和 gen_chain_char 既设计无环情况,又涉及有环情况,需要根据需求选择调用Node_chain_builder类和Word_chain_builder类。

两种builder类的实现结构基本类似,包括init()、build()、get_result()和get_num()这4个基本功能,分别用于初始化、构造对应chain、返回result结果和返回result数组大小

其中,build()内调用一个私有成员函数travelsal_build()来递归遍历每一个节点(Node节点或者Word_vertex节点),每当遇到一个正确的可结束的结尾时,调用相应的update函数来更新目标结果

计算模块接口部分的性能改进
针对无环和有环两种情况,分别给出优化策略

  • 无环的情况:我们使用Node代替Word_vertex来保存单词信息,通过单词的首尾字母在一个26*26的DAG图(类似a…a的情况单独处理)上找到入度为0的点,然后向下遍历。对于gen_chain_word、gen_chain_char和gen_chain_word_unique这种查找最大单词(或字母)数的问题,我们只需要在遍历到出度为0的点时进行更新。经过优化,每个节点只会计算一次。而对于gen_chains_all保存所有单词链的问题,需要进行进一步的优化。
  • gen_chains_all的优化:同样在26*26的DAG图上找到入度0的点向下遍历,每当chain加入一个新的Node时,我们根据cur_chain_all的内容和新节点Node更新cur_chain_all。如果新节点Node的出度等于1,继续向下遍历;如果新节点Node的出度大于1,我们将cur_chain_all压栈,压入cur_chain_all_list,并把cur_chain_all清空,继续向下遍历;如果新节点Node出度等于0,则根据cur_chain_all和cur_chain_all_list栈,生成从开始节点到当前节点的所有单词链。经过优化,每种单词链只会被计算一次。
  • 有环的情况:我们使用Word_vertex来保存单词信息,此时问题转变为”在一个有正环的有向图上求最长路“,是一个典型的NP-Hard问题,不能在多项式时间内被求解。但是我们可以根据无环的思路来优化有环的情况,首先利用tarjan算法进行多点,将Word_vertex节点划分为若干个强连通分量,我们用Word_tarjan_vertex类进行保存,这些由强连通分量(Word_tarjan_vertex)构成的新图是一个DAG图。我们求新DAG图的逆拓扑序列,并按照逆拓扑序列遍历每个强连通分量内的最长路径,并保存每个节点开始的最长路径。因为我们是按照逆拓扑序列遍历每个强连通分量,所以一个强连通分量内如果遍历到其他强连通分量的节点,那个节点的最长路径信息已知,避免了重复计算,从而实现了优化。当然最坏情况和直接遍历所有节点一致——只存在一个强连通分量。

VS性能分析图如下所示,选用数据为-r参数下的随机数据,可以发现占用CPU时间最长的方法为Word_chain_builder::travelsal_build(Word_vertex* wv, Word_tarjan_vertex* wtv),此方法为递归调用,递归查询后继节点并更新单词链。
在这里插入图片描述

Design by Contract,Code Contract
契约式设计以面对对象构造为例:

  • 期望所有调用它的客户模块都保证一定的进入条件:这就是函数的先验条件—客户的义务和供应 商的权利,这样它就不用去处理不满足先验条件的情况。
  • 保证退出时给出特定的属性:这就是函数的后验条件—供应商的义务,显然也是客户的权利。
  • 在进入时假定,并在退出时保持一些特定的属性:不变条件。

即在调用某方法的前后都需要进行检查,且检查的任务分别归属于调用者与被调用者,在调用结束后还需要检验不变条件。以本次作业为例,即在调用核心的生成单词链的函数前,需要由调用者预先处理单词,例如检查是否有环,去除重复单词,将单词转换为小写等;而在调用结束后,需要返回计算结果的长度,并提供结果对应的单词链以便进行输出;此处的不变条件则为传入的字符串,字符串是不可变的,单词链中使用的必须是未修改,未拆分的单词。

测试

计算模块部分单元测试展示
测试数据构造:测试数据构造的思路主要是考虑到覆盖不同的情况,没有追求复杂及较为考验时间复杂的的数据,按照不同参数的分类依次进行测试,例如-n,-m, -w, -w -h a, -w -r等等。同时每一个分类下准备类型不同的测试数据,例如-w -h a这个参数组合准备了如下数据

input: {"bbc", "bbd", "beg", "ccf", "cccccf", "grs", "deg"}
output: null

input: {"abc", "bbd", "ccf", "deg", "grrrrx", "xtq", "qbs"}
output: {"abc", "ccf"}

无环测试数据
有环测试数据

异常处理

项目中遇到异常时,采用输出异常类型至终端并结束程序运行的方式,具体的异常类型如下表所示

异常类型示例异常描述
缺少参数Wordlist.exe -r “input.txt”此时只有-r参数,没有-m, -n -w ,-c中的任一参数,不会有输出结果,因此缺少关键的参数
错误参数组合Wordlist.exe -n -r “input.txt”-n与-r两个参数不能同时存在,此时为错误参数组合
重复参数Wordlist.exe -n -n “input.txt”-n与-n两个参数相同,此时为重复参数
错误参数类型Wordlist.exe -x “input.txt”-x参数不存在,此时为错误参数类型
文件名错误Wordlist.exe -r -w “wrong_file.txt”在尝试读取文件时发生错误,该文件不存在
含单词环输入文本包含abc, cde, eba在参数没有-r的情况下,出现了单词环,此时为错误输入
无有效单词输入文本为 12213,2323123,2144此时无法解析出有效的单词,因此无法构建单词链
无单词链输入文本为 abc, abd此时无法构建出单词链,应当提示文本中无单词链
单词链长度过大-n情况下输出超过20000此时应直接输出长度,并提示超过最大范围,随后返回

环路检测实现
异常的检测大多数通过参数直接判断即可,环路检测采用了拓扑排序去判断

bool judge_circle() {
		int count = 0; //用于计算是否还有节点剩余
		queue<Node*> node_queue;
		//找出所有入度为0的节点
		for (vector<Node*>::iterator it = nodes.begin(); it != nodes.end();it++)
		{
			Node* node = *it;
			if ((node->get_inDegree()) == 0) {
				node_queue.push(node);
				count++;
			}
			//同时判断是否存在多个形如a....a的单词,若存在则必定有环
			if (node->get_start() == node->get_end()) {
				vector<string*> temp_vector;
				node->get_list(temp_vector);
				if (temp_vector.size() > 1) {
					return false;
				}
			}
		}
		//cout << "start" << endl;
		while (!node_queue.empty()) {
			Node* temp = node_queue.front();
			//temp->show_words();
			node_queue.pop();
			char end = temp->get_end();
			//当前节点有后继节点
			if (start_map.count(end) > 0) {
				vector<Node*> list = start_map[end];
				for (vector<Node*>::iterator iter = list.begin(); iter != list.end(); iter++) {
					Node* node = *iter;
					node->sub_inDegree();
					if (node->get_inDegree() == 0) {
						//cout << "next node: " << endl;
						//node->show_words();
						node_queue.push(node);
						count++;
					}
				}
			}
		}
		//cout << count << endl;
		return count == nodes.size();
	}

关于结对编程

结对过程
结对过程其实很简单,我在群里文档找了一下没有组队的人,然后私聊询问就组队成功了。
结对图像资料
在这里插入图片描述

结对编程优点与缺点

  • 优点:两个人讨论问题,项目的整体构建速度会较快,同时遇到一些困难时也可以一起讨论如何去实现
  • 缺点:两个人的思路会出现分歧,需要进行沟通解决,同时因为无法做到一直一起完成项目,一些部分需要分工合作,远程合作的情况下效率会变低

队友的优点与缺点

优点缺点
能力强,代码质量高;思路清晰,讨论的时候能很快构建出解决方案;注重细节有些执着于自己的设计

总结
总的来说这次软工结对编程可以算是一个失败的项目,最终很多部分都没有实现,例如单元测试不够完善,没有封装成dll,没有写GUI等等。但是在整个结对编程的过程中还是有一些经验的收获的,例如如何去与结对伙伴去沟通(在某种意义上还是有成功的地方的)。
在项目开始时,我们很快便讨论出来了大致的思路与结构,随后分工是我负责输入输出,异常处理,数据预处理与建图的部分,核心的搜索功能由队友实现。但是在实现的过程中,还是不断会有冲突的思路(例如我觉得Node和Word_vertex可以压缩到一起,但是队友执着于每种都新建不同的类),但是在不断沟通后基本解决了问题,最后在截止前三天实现了作业的需求。在最后准备进行测试的时候,我构造了每种情况的数据,并发送给了队友以供测试(个人以为他会写好针对接口的单元测试)。结果由于没有及时沟通,导致最后没有进行单元测试,只是每种数据通过命令行执行了一遍。以及封装成dll的问题,我们没有提前去考察这个问题,在测试完程序之后才开始学习,导致最终时间不够没有实现。
项目的失败和项目中的每一个人或多或少都有关系,通过这次作业,我感觉我在合作项目中主要有以下几个可以改进的地方:

  • 需要更加坚定自己的想法并积极与队友沟通进行改进,不能总把自己放在被动实现目标的位置
  • 需要对项目的技术难度有更清楚的认识,不能在最后关头再去学习新的知识
  • 设立更加明确的时间表,按照阶段去开展任务,同时代码实现后最好可以和合作者交换进行审查,互相督促完成目标。

(软工团队项目要加油)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值