软工结对作业

项目内容
本作业所属课程2022年北航敏捷软件工程教学实践
本作业要求结对编程项目-最长英语单词链
个人课程目标掌握结对协作能力、VIsual studio等开发工具使用技能、开发高质量软件能力、使用分析与测试工具对代码进行分析和测试
本作业在哪个具体方面帮助我实现目标学习使用工程化方法对软件实例有初步分析和认知,进行结对项目实际体验

软件工程结对编程作业

  • 教学班级:周五班
  • 项目地址:https://github.com/ZerglingChen3/LongestEnglishWordChain

PSP表格

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

接口设计

设计理念

  • 信息隐藏(Infromation Hiding):信息隐藏是将敏感的或外部无需访问的信息封装在自身内部,使得外部不可见此类信息。在我们的设计过程中,我们将图进行封装,外部只能通过给出的方法对图中的边和点进行添加和查询操作。图中的边又进一步封装,外部只能通过给定的方法访问指定的权值。
  • 接口设计(Interface Design):接口设计决定了模块之间沟通的效率和效果。良好的接口设计应该可以让外部通过接口,简明快速地了解到接收的请求和返回的结果,而不关心实现的细节。在我们的设计中,每个接口中的参数可以划分成两部分,分别是传入的请求相关参数和返回的结果相关参数。相关性强的参数往往放在一起,比如连通块个数和连通子图,后者的数目即是前者的值,这样的信息往往前后连续放置。返回值通常表示函数运行成功或异常。
  • 松耦合(loose coupling):松耦合指模块之间的联系是很小的,对于一个模块的修改不必担心破坏其他关联模块的结构。我认为,相比于上述两点,松耦合是一种更宽泛的状态,在我们的设计中,由于做到了良好的信息隐藏和接口设计,模块之间的耦合自然地下降,互相之间影响很少。

计算模块接口的设计和实现

存储
  • 单词类(Word)中储存的是单词原本的字符串信息、首尾字母以及长度,外界可以通过公共方法读取这些信息,但不能修改。
  • 边类(Edge)中存储了一条边的端点、权值、邻接边以及对应的单词。外界可以通过公共方法读取这些信息,但不能修改。
  • 图类(Graph)中存储了一张图中的边、点、点的权重、自环索引、常规边索引、各点自环数目。外界可以通过公共方法读取这些信息,可以调用创建常规边和自环的方法进行增添。
建图

我们将每个字母抽象成一个点,单词则是从首字母所在点到尾字母所在点的一条边。单词的长度作为对应边的边权。同时,我们需要特殊记录每个点的自环。

产生所有单词链

gen_all_chain接口的实现如下图所示,首先处理原始单词列表,建立图,枚举所有出现过的字母作为起点,然后进行dfs搜索得到所有路径,即可得到所有单词链。

在这里插入图片描述

产生首字母不重复的单词链

gen_chain_word_unique接口的实现如下图所示。由于首字母不重复,因而在答案中每个点仅使用一次,并且题目中要求合法的输入中不包含环。

我们利用原始单词列表建图,所得的图必然是DAG,然后求出图的拓扑序,并按照拓扑序进行动态规划,求最长路径。转移方程为 d p [ i ] = d p [ j ] + 1 dp[i] = dp[j]+1 dp[i]=dp[j]+1,其中需要满足 e ( j , i ) ∈ G e(j, i) \isin G e(j,i)G t o p o [ i ] > t o p o [ j ] topo[i] > topo[j] topo[i]>topo[j]。并且在转移的过程中,我们需要记录 p r e E d g e [ i ] preEdge[i] preEdge[i]表示当前 d p [ i ] dp[i] dp[i]是由哪条边转移而来的,以便于后面逆推产生单词链。

特殊的,只有末尾的字母是允许有自环的,因而在dp结束后,需要特判每一个字母是否有自环,如果有,则dp值需要加1。

逆推答案时,首先判断是否包含自环,然后根据 p r e E d g e [ i ] preEdge[i] preEdge[i]不断寻找此前的边和点,直到不存在 p r e E d g e [ i ] preEdge[i] preEdge[i](值为-1)为止。
在这里插入图片描述

首、尾字母限制

在阐述剩下两个接口前,我们先阐述对-h-t的实现方法。由于求最长的接口均使用动态规划(DP)实现,我们只需特殊处理DP的初始化和最终的答案更新。

  • 对于首字母限制(-h),我们在DP前只对合法的字母对应的点的DP值赋予初始权值,其他点初始化为-1;当没有指定首字母时,所有点在DP前初始化为其初始权值。
  • 对于尾字母限制(-t),我们在DP结束后,只判断合法字母的对应点的DP值是否符合要求;当没有指定尾字母时,我们取所有点中DP值最大的点。
产生单词最多的链

gen_chain_word接口的实现如下图所示,此时,我们对于有环和无环的处理时不同的。

对于无环的情况,首先建图,然后求出拓扑序,然后按照拓扑序进行动态规划。有效出发点被初始化为该点的自环数目( s e l f _ l o o p _ c n t ( x ) self\_loop\_cnt(x) self_loop_cnt(x)),转移方程为 d p [ i ] = d p [ j ] + 1 + s e l f _ l o o p _ c n t ( j ) dp[i] = dp[j]+1+self\_loop\_cnt(j) dp[i]=dp[j]+1+self_loop_cnt(j),其中需要满足 e ( j , i ) ∈ G e(j, i) \isin G e(j,i)G t o p o [ i ] > t o p o [ j ] topo[i] > topo[j] topo[i]>topo[j]。找到有效的最大终止位置,然后逆推得到答案序列,需要注意考虑中间每个点的自环的情况。

对于有环的情况,在建图后需要进行缩点操作,我们使用Trajan算法求出连通块并进行缩点。然后利用dfs搜索预处理出每个连通块内,节点两两之间的最长路径。接下来,对于缩点后的图求出拓扑序,并进行DP。DP的过程与无环的情况略有不同,对于每一个连通块,需要先考虑通过环内路径更新所有环内节点的值,然后再考虑由环内的点取更新后续连通块。找出合法最大值的点后,逆推出答案序列,需要特殊考虑环内的点之间的转移,使用dfs搜索指定起点和终点之间的路径,由于起点、终点、总长度是确定的,即可在dfs的过程中判断,以找到合法路径。

在这里插入图片描述

生成字母最多的链

gen_chain_char接口的实现与上述gen_chain_word大致相同,不再赘述重复部分,主要差异有以下两点:

  1. DP表达式改为 d p [ i ] = d p [ j ] + e d g e _ w e i g h t ( e ) + s e l f _ l o o p _ c n t ( j ) dp[i] = dp[j]+edge\_weight(e)+self\_loop\_cnt(j) dp[i]=dp[j]+edge_weight(e)+self_loop_cnt(j)
  2. 在建图后,需要将一些可能干扰答案的无用边删掉:
    1. 孤立自环边:某个点只有一个自环,且该点入度、出度为 0
    2. 孤立树边:边的起点、终点均没有自环,起点的入度为0,终点的出度为0

UML设计

主要信息存储在GraphEdgeWord三个类中,三者逐层包含,将信息层层封装。主要功能封装在getwordsoutputgens三个接口中,getwords负责读入,output负责输出,gens负责核心运算。

在这里插入图片描述

计算模块接口的性能改进

改进思路
利用拓扑性质

在生成最长不重复、最长单词数、最长字母数中,拓扑的性质尤为重要。首先,我们假设图中无环,那么我们可以观察到一个重要的性质:最优解应当是以没有入度为起始点,以没有出度的点为终点,经过的点的顺序是严格按照拓扑序的。因而我们按照拓扑序进行DP即可得到最优解。对于有环的图,我们进行缩点,然后对缩点后的图按照拓扑序求解。

按照求解拓扑序的时间复杂度为 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(V+E),其中V为点集,E为边集。

引入动态规划

我们按照拓扑序进行DP,3类求解问题的DP的表达式如上文所述。其中无环的情况下DP的时间复杂度为 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(V+E),其中V为点集,E为边集。有环的情况下,DP的时间复杂度为 O ( m a x ( ∣ V i ∣ 2 ) + ∣ E i ∣ ) ∗ ∣ S ∣ ) O(max(|V_i|^2)+|E_i|)*|S|) O(max(Vi2)+Ei)S),其中 V i V_i Vi为第 i i i个连通块的点数, E i E_i Ei为第 i i i个连通块的边数, S S S为所有连通块的集合。

性能分析图

在这里插入图片描述
可以看出运算模块仍然是主要的性能瓶颈,IO模块占用不高。

关于Design by Contract / Code Contract的思考

契约式设计(Design by Contract) /代码契约(Code Contrast) :

契约式设计强调三个基本概念:前置条件后置条件不变式。契约式设计要求模块在运行(调用)前满足前置条件,在运行之后结果满足后置条件,并且运行的结果中满足不变式所要求某些变量的不变。

契约式编程的优势是思路清晰,对模块之间耦合的规定和要求更加明确,同时可以消除一些模块之间的兼容性问题。缺点是其撰写、检查和实现的过程中往往需要结合语言本身的特性。

单元测试

单元测试代码

设计思路

计算模块的单元测试部分包含文件读、写和用例测试两部分代码。

  • 文件读入:将测试用例从文件中读入,并进行简单的去重和整理,然后将单词列表传递给计算模块。
  • 文件输出:将计算模块的结果输出到文件中,方便人工调试
  • 暴力对拍:采用dfs方法搜索图中所有可能的路径,找到合法的最优路径,与计算模块所得的路径相比较。
  • 用例测试:调用文件读入、输出,以及计算模块,并对计算模块的结果进行验证,验证主要思路如下:
    • 验证链的长度或链的总数是否是期望的(与暴力方法得到结果相吻合)
    • 验证链是否合法
      • 链中的单词来自源单词列表
      • 单词不重复
      • 收尾相接
部分测试代码
文件读入
int handleInput(char* fileName, char* word[], int* len) {
			FILE* file;
			int r = fopen_s(&file, fileName, "r");
			if (file == NULL) {
				return -1;
			}
			else {
				std::string s = "";
				char c;
				int wordCount = 0;
				wordSet.clear();
				while ((c = fgetc(file)) != EOF) {
					if (c >= 'A' && c <= 'Z')
						s += char(c - 'A' + 'a');
					else if (c >= 'a' && c <= 'z')
						s += c;
					else {
						if ((int)s.size() > 1) {
							if (wordSet.find(s) == wordSet.end()) {
								char* tmp = (char*)malloc(s.length() + 1);
								if (tmp != NULL) {
									char* str = tmp;
									for (int i = 0; i < s.length(); i++) {
										(*str++) = s[i];
									}
									(*str) = '\0';
									word[++wordCount] = tmp;
									wordSet.insert(s);
								}
							}
						}
						s = "";
					}
				}
				if ((int)s.size() > 1 && wordSet.find(s) == wordSet.end()) {
					char* tmp = (char*)malloc(s.length() + 1);
					if (tmp != NULL) {
						char* str = tmp;
						for (int i = 0; i < s.length(); i++) {
							(*str++) = s[i];
						}
						(*str) = '\0';
						word[++wordCount] = tmp;
					}
				}
				(*len) = wordCount;
			}
			return 0;
		}
文件输出
void output(char* path, int ans, char* result[], int len) {
			FILE* file;
			fopen_s(&file, path, "w");

			fprintf(file, "%d\n", len);
			for (int i = 1; i <= len; ++i) {
				fprintf(file, "%s\n", result[i]);
			}
			fclose(file);
		}
测试模块

core部分针对不同测试点的测试模块共有64个,这里只给出一个例子。

TEST_METHOD(TestGenWordNNT)
{
			
		char filename[100] = "../test/input.txt";
			
		int len = 0;
		int r = handleInput(filename, words, &len);

		char path[100] = "../test/output.txt";
			
		int ans = gen_chain_word(words, len, result, 0, 0, true);

		output(path, ans, result, (ans > 0));

		Assert::AreEqual(ans, 14);
		
		r = judge(ans, result);
		
		Assert::AreEqual(r, 0);

}
暴力对拍
 void dfs_find_chain_max(int pt, int len, bool first_diff, int END) {
            if (len > ansLen && (END == -1 || END == pt) && chainLen > 1) {
                ansLen = len;
                outputLen = chainLen;
                for (int i = 1; i <= chainLen; ++i) {
                    ans[i] = chain[i];
                }
            }
            visp[pt]++;
            for (int e = first[pt]; e; e = edges[e].next) {
                int to = edges[e].to;
                if (first_diff) {
                    if (!vist[e] && !visp[to]) {
                        vist[e] = true;
                        chain[++chainLen] = word[e];
                        dfs_find_chain_max(to, len + edges[e].len, first_diff, END);
                        chainLen--;
                        vist[e] = false;
                    }
                }
                else {
                    if (!vist[e]) {
                        vist[e] = true;
                        chain[++chainLen] = word[e];
                        dfs_find_chain_max(to, len + edges[e].len, first_diff, END);
                        chainLen--;
                        vist[e] = false;
                    }
                }
            }
            visp[pt]--;
        }

        void dfs_find_chain_go(int pt, int len) {
            if (len > 1) {
                chainCount++;
                for (int i = 1; i <= chainLen; ++i) {
                    std::cout << chain[i] << " ";
                }
                std::cout << std::endl;
            }
            for (int e = first[pt]; e; e = edges[e].next) {
                int to = edges[e].to;
                if (!vist[e]) {
                    vist[e] = true;
                    chain[++chainLen] = word[e];
                    dfs_find_chain_go(to, len + 1);
                    chainLen--;
                    vist[e] = false;
                }
            }
        }

        void link(int S, int T, int id, bool tot_character) {
            ++te;
            edges[te].from = S;
            edges[te].to = T;
            edges[te].next = first[S];
            edges[te].id = id;
            if (tot_character) {
                edges[te].len = word[id].size();
            }
            else {
                edges[te].len = 1;
            }
            first[S] = te;
        }

        int checker(char START, char END, bool find_all, bool tot_character, bool first_diff) {
            ansLen = 0;

            te = 0;
            for (int i = 0; i < 26; i++) {
                first[i] = 0;
            }

            for (int i = 1; i <= totWord; i++) {
                link(word[i][0] - 'a', word[i][word[i].size() - 1] - 'a', i, tot_character);
            }

            if (find_all) {
                for (int i = 0; i < 26; i++) {
                    dfs_find_chain_go(i, 0);
                }
                return chainCount;
            }
            else {
                if (START == 0) {
                    for (int i = 0; i < 26; i++) {
                        if (END == 0) {
                            dfs_find_chain_max(i, 0, first_diff, -1);
                        }
                        else {
                            dfs_find_chain_max(i, 0, first_diff, END - 'a');
                        }
                    }
                }
                else {
                    if (END == 0) {
                        dfs_find_chain_max(START - 'a', 0, first_diff, -1);
                    }
                    else {
                        dfs_find_chain_max(START - 'a', 0, first_diff, END - 'a');
                    }
                }
                return ansLen;
            }
            return 0;
        }
测试数据构造
数据点自环重复单词混淆字符
0-1
2-3、7
4
5-6、10
8-9、11
12
数据点测试内容
0有环,有自环,有重复,图复杂,检查缩点是否正确,针对所有
1有环,有自环,有重复,检查去重和缩点,针对所有
2有环,有自环,针对所有,判断自环和环的叠加状态是否正常
3有自环,针对gen_char
4有环,针对所有
5有自环,针对所有
6针对gen_char,判断是否能跳过孤立长边
7有环,有自环,针对所有,判断自环和环的叠加状态是否正常
8有自环,针对gen_char,判断是否能正常计入首部的自环
9有自环,针对gen_char,判断是否能正常计入末尾的自环
10有自环,针对gen_char,判断是否能跳过孤立的长自环
11存在单个字母单词、有自环
12存在乱码、单个字母单词
覆盖率截图

在这里插入图片描述

异常处理

我们总共支持了十四种异常,每一种异常都做了单元测试。

参数中包含多个输入文件

当参数中出现多个文件(判断文件是以 txt 结尾),会反馈"指定了多个文件路径,请仅指定单一路径!"。

单元测试代码如下:

TEST_METHOD(MULT_PATH_FILE_ERROR)
{
	char* argv[] = {"Wordlist.exe", "-n", "../test/input1.txt", "../test/input2.txt" };
	int problemType, start, end;
	bool loop_enable;
	char* name;
	int r = parameterExtract(argv, 4, problemType, loop_enable, start, end, &name);
	Assert::AreEqual(r, (int)-Error::MULTI_FILE_PATH);
}

不存在的参数

当参数中出现非要求的参数时,会反馈"参数不存在,请重新输入!"。

单元测试代码如下:

TEST_METHOD(PARAMETER_NOT_EXISTS_ERROR)
{
	char* argv[] = { "Wordlist.exe", "-n", "../test/input1.txt", "-s" };
	int problemType, start, end;
	bool loop_enable;
	char* name;
	int r = parameterExtract(argv, 4, problemType, loop_enable, start, end, &name);
	Assert::AreEqual(r, (int)-Error::PARAMETER_NOT_EXISTS);

	argv[3] = "s";
	r = parameterExtract(argv, 4, problemType, loop_enable, start, end, &name);
	Assert::AreEqual(r, (int)-Error::PARAMETER_NOT_EXISTS);

	argv[3] = "测试异常参数";
	r = parameterExtract(argv, 4, problemType, loop_enable, start, end, &name);
	Assert::AreEqual(r, (int)-Error::PARAMETER_NOT_EXISTS);
}

参数中不存在文件路径

当参数中不含有文件路径时,会反馈"参数中不存在文件路径!"。

单元测试代码如下:

TEST_METHOD(PATH_NOT_EXISTS_ERROR)
{
	char* argv[] = { "Wordlist.exe", "-r" };
	int problemType, start, end;
	bool loop_enable;
	char* name;
	int r = parameterExtract(argv, 2, problemType, loop_enable, start, end, &name);
	Assert::AreEqual(r, (int)-Error::NO_FILE_PATH);
}

指定首尾字母时忘记指定字符

当指定 -h 和 -t 参数时,如果后面没有立即接大小写字符,会反馈"指定首尾字母时忘记字母参数!"。

单元测试代码如下:

TEST_METHOD(NO_CHAR)
{
	char* argv[] = { "Wordlist.exe", "-h" };
	int problemType, start, end;
	bool loop_enable;
	char* name;
	int r = parameterExtract(argv, 2, problemType, loop_enable, start, end, &name);
	Assert::AreEqual(r, (int)-Error::NO_CHAR_ERROR);

	argv[1] = "-t";
	r = parameterExtract(argv, 2, problemType, loop_enable, start, end, &name);
	Assert::AreEqual(r, (int)-Error::NO_CHAR_ERROR);
}

指定首尾字母时字符不合法

当指定 -h 和 -t 参数时,如果后面的字符并不是大小写字符,会反馈"指定字母时格式不正确!只允许指定大小写字母!"。

单元测试代码如下:

TEST_METHOD(WRONG_CHAR_FORM)
{
	char* argv[] = { "Wordlist.exe", "-h", "%" };
	int problemType, start, end;
	bool loop_enable;
	char* name;
	int r = parameterExtract(argv, 3, problemType, loop_enable, start, end, &name);
	Assert::AreEqual(r, (int)-Error::CHAR_FORM_ERROR);

	argv[2] = "-t";
	r = parameterExtract(argv, 3, problemType, loop_enable, start, end, &name);
	Assert::AreEqual(r, (int)-Error::CHAR_FORM_ERROR);
}

参数指定了多个任务

参数 -n 、-w 、-m 、-c 分别代表需要执行的四个任务。在一个参数序列中不允许指定超过两个任务,违反则会反馈"指定了多个任务,请仅指定一个任务!"。

单元测试代码如下:

TEST_METHOD(MULTI_WORK)
{
	char* argv[] = { "Wordlist.exe", "-n", "-w" };
	int problemType, start, end;
	bool loop_enable;
	char* name;
	int r = parameterExtract(argv, 3, problemType, loop_enable, start, end, &name);
	Assert::AreEqual(r, (int)-Error::MULTI_WORK_ERROR);
}

参数中未指定任务

在一个参数序列中没有指定任务,会反馈"没有指定任务,请至少指定一个任务!"。

单元测试代码如下:

TEST_METHOD(MULTI_WORK)
{
	char* argv[] = { "Wordlist.exe", "-n", "-w" };
	int problemType, start, end;
	bool loop_enable;
	char* name;
	int r = parameterExtract(argv, 3, problemType, loop_enable, start, end, &name);
	Assert::AreEqual(r, (int)-Error::MULTI_WORK_ERROR);
}

重复指定首字母

当出现多次使用 -h 参数时,会反馈"重复指定首字母!"。

单元测试代码如下:

TEST_METHOD(FIRST_CHAR_DUP)
{
	char* argv[] = { "Wordlist.exe", "-h", "a", "-h", "b", "-n" };
	int problemType, start, end;
	bool loop_enable;
	char* name;
	int r = parameterExtract(argv, 6, problemType, loop_enable, start, end, &name);
	Assert::AreEqual(r, (int)-Error::FIRST_CHAR_DUPLICATE);
}

重复指定尾字母

当出现多次使用 -t 参数时,会反馈"重复指定尾字母!"。

单元测试代码如下:

TEST_METHOD(FINAL_CHAR_DUP)
{
	char* argv[] = { "Wordlist.exe", "-t", "a", "-t", "b", "-n" };
	int problemType, start, end;
	bool loop_enable;
	char* name;
	int r = parameterExtract(argv, 6, problemType, loop_enable, start, end, &name);
	Assert::AreEqual(r, (int)-Error::FINAL_CHAR_DUPLICATE);
}

重复指定允许有环参数

当出现多次使用 -r 参数时,会反馈"重复指定有环参数!"。

单元测试代码如下:

TEST_METHOD(ENABLE_LOOP_DUP)
{
	char* argv[] = { "Wordlist.exe", "-r", "-r" };
	int problemType, start, end;
	bool loop_enable;
	char* name;
	int r = parameterExtract(argv, 3, problemType, loop_enable, start, end, &name);
	Assert::AreEqual(r, (int)-Error::ENABLE_LOOP_DUPLICATE);
}

参数之间存在冲突

对于参数 -n 和 -m,并不支持和其他参数共同使用,此时会反馈"-n参数不支持和其他参数共同使用!“或者”-m参数不支持和其他参数共同使用!"。

单元测试代码如下:

TEST_METHOD(PARAMETER_CONFLICT)
{
	char* argv[] = { "Wordlist.exe", "-n", "-r", "../test/input1.txt" };
	int problemType, start, end;
	bool loop_enable;
	char* name;
	int r = parameterExtract(argv, 4, problemType, loop_enable, start, end, &name);
	Assert::AreEqual(r, (int)-Error::N_WORK_WITH_OTHER_PARAMETER);

	argv[1] = "-m";
	r = parameterExtract(argv, 4, problemType, loop_enable, start, end, &name);
	Assert::AreEqual(r, (int)-Error::M_WORK_WITH_OTHER_PARAMETER);
}

文件不存在

当输入参数指定的文件无法打开或者不存在时,会反馈"单词表所在文件不存在!"。

单元测试代码如下:

TEST_METHOD(NO_FILE_ERROR)
{
	char fileName[100] = "../test/input0.txt";
	char* word[500];
	int len = 0;
	int r = handleInput(fileName, word, &len);
	Assert::AreEqual(r, (int)-Error::FILE_NOT_FIND);
}

传入单词有误

当传入的单词表(已经分割好的单词)中存在单词为空指针时,会反馈"传入接口的单词表有误,请检查单词合法性"。

单元测试代码如下:

TEST_METHOD(WORD_NOT_AVAIL)
{
	char* words[4] = { "abc", "edfg", NULL, NULL};

	char* result[10];
	int len = 4;
	int r = gen_chain_word_unique(words, len, result);
	Assert::AreEqual(r, (int)-Error::WORD_NOT_AVAILABLE);

	r = gen_chains_all(words, len, result);
	Assert::AreEqual(r, (int)-Error::WORD_NOT_AVAILABLE);

	r = gen_chain_char(words, len, result, 'a', 'z', false);
	Assert::AreEqual(r, (int)-Error::WORD_NOT_AVAILABLE);

	r = gen_chain_word(words, len, result, 'a', 'z', false);
	Assert::AreEqual(r, (int)-Error::WORD_NOT_AVAILABLE);
}

单词中存在隐藏环

当传入的单词表中存在隐藏环且未指定 -r 参数时,会反馈"单词表中包含隐藏环"。

单元测试代码如下:

TEST_METHOD(LOOP_CHECK)
{
	char* words[4] = { "gg", "abc", "cde", "ea"};
	char* result[10];
	int r = gen_chain_word_unique(words, 3, result);
	Assert::AreEqual(r, (int)-Error::HAVE_LOOP);

	r = gen_chains_all(words, 3, result);
	Assert::AreEqual(r, (int)-Error::HAVE_LOOP);

	r = gen_chain_char(words, 3, result, 'a', 'z', false);
	Assert::AreEqual(r, (int)-Error::HAVE_LOOP);

	r = gen_chain_word(words, 3, result, 'a', 'z', false);
	Assert::AreEqual(r, (int)-Error::HAVE_LOOP);
}

界面模块

设计过程

界面模块采用的Python的PyQt5设计的。

单词输入界面

在这里插入图片描述

输入界面允许用户从文件中导入文本和从文本框中输入文本,文本导入后将显示到中间的文本框中。

点击重置按钮允许将文本框中的文本清空。

点击确定按钮将进入参数选择页面。

参数选择界面

在这里插入图片描述

参数选择界面中上方将选择问题参数,这里四个问题只能选择一个,且如果未指定则会提示未指定任务参数。

下方其他参数的设定通过下拉框选择,可以不指定,也可以指定。

返回按钮将回到单词输入界面。

确定按钮将开始进行计算。

结果反馈页面

在这里插入图片描述

计算结束后答案将反馈到文本框中,用户可自行下载使用,或者选择右上方的导出到文件进行导出。

下面的返回按钮将回到单词输入界面。

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

界面模块与计算模块的对接主要通过下面的函数:

char* call_by_cmd(int len, char* cmd)

该函数会执行一个命令,并将异常信息通过返回值的方式进行反馈。

如果不存在异常则反馈空串。

该函数的实现在control这个模块中,通过空格将任务分割,调用myControll模块进行之后的计算工作。

异常信息在计算过程中会保存在error.log文件中,在返回之前会读取这个文件将异常结果返回。

计算的答案保存在solution.txt中。

界面模块中将通过python导入dll的方式导入control.dll模块,调用其中的call_by_cmd接口,读取异常信息并最终显示结果。

接口互换

信息

与我们进行接口互换的组是:

  • 19373442 郭衍培
  • 19373682 牛易明

我们两组均使用 C++ 做为项目语言,前端均为 python 实现。

互换情况
我们的计算模块与他们的GUI

在这里插入图片描述

他们的计算模块与我们的GUI

在这里插入图片描述

交换时的主要问题

我们两组均采用一个交接函数

char* call_by_cmd(int len, char* cmd)

来进行前后端的交接,不同的是我们的输出将答案输出和异常结果分开输出。答案输出到文件 solution.txt 中,异常输出到该函数的返回值中。而另外一组全部输出到返回值中。因此他们跑我们的 dll 模块时修改比较容易,而我们需要适配他们的输出,需要对输出内容进行格式解析,进行的比较复杂。

结对纪实

结对过程记录

在这里插入图片描述

总结

缺点

需要协商二人的时间,尤其在课程时间安排不一致、课余时间紧俏的情况下,需要牺牲很多休息时间来进行结对编程中需要一同完成的环节。

优点
  1. 算法思路可以经过复审验证。两个人经过讨论后确定思路,可以让整个方案更加清晰,考虑的方面更加全面。
  2. 互相学习。可以在结对编程中学习到对方在完成任务中的良好的工作习惯,修正自己的不足。
  3. 锻炼表达能力。结对编程期间可以锻炼自己对方案的概况和描述能力,沟通的过程中学习和掌握沟通方法。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值