作业信息
教学班级:周五班
项目地址:https://github.com/buaddd/buaa_SE_2022_Pair_Program
PSP表格
Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|
计划 | 60 | 120 |
估计这个任务需要多少时间 | 30 | 30 |
开发 | 1200 | 1500 |
需求分析 (包括学习新技术) | 180 | 120 |
生成设计文档 | 180 | 120 |
设计复审 (和同事审核设计文档) | 30 | 60 |
代码规范 (为目前的开发制定合适的规范) | 30 | 60 |
具体设计 | 180 | 180 |
具体编码 | 600 | 600 |
代码复审 | 180 | 120 |
测试(自我测试,修改代码,提交修改) | 300 | 300 |
报告 | 180 | 180 |
测试报告 | 120 | 120 |
计算工作量 | 30 | 60 |
事后总结, 并提出过程改进计划 | 60 | 60 |
合计 | 56 * 60 | 60.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的问题,我们没有提前去考察这个问题,在测试完程序之后才开始学习,导致最终时间不够没有实现。
项目的失败和项目中的每一个人或多或少都有关系,通过这次作业,我感觉我在合作项目中主要有以下几个可以改进的地方:
- 需要更加坚定自己的想法并积极与队友沟通进行改进,不能总把自己放在被动实现目标的位置
- 需要对项目的技术难度有更清楚的认识,不能在最后关头再去学习新的知识
- 设立更加明确的时间表,按照阶段去开展任务,同时代码实现后最好可以和合作者交换进行审查,互相督促完成目标。
(软工团队项目要加油)