个人第三次作业——结对编程
一、项目信息
项目 | 内容 |
---|---|
这个作业属于哪个课程 | 2023 年北航软件工程 |
这个作业的要求在哪里 | 结对项目-最长英语单词链 |
教学班级 | 周四下午班 |
项目地址 | 项目地址 |
我在这个课程的目标是 | 了解软件工程的方法论、获得软件项目开发的实践经验 |
这个作业在哪个具体方面帮助我实现目标 | 对于结对编程有了初步认识,对于团队合作的开发有一些深入的了解 |
二、PSP 估计
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | |
Planning | 计划 | 60 | |
· Estimate | · 估计这个任务需要多少时间 | 30 | |
Development | 开发 | 1470 | |
· Analysis | · 需求分析 (包括学习新技术) | 240 | |
· Design Spec | · 生成设计文档 | 30 | |
· Design Review | · 设计复审 (和同事审核设计文档) | 30 | |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | |
· Design | · 具体设计 | 60 | |
· Coding | · 具体编码 | 720 | |
· Code Review | · 代码复审 | 60 | |
· Test | · 测试(自我测试,修改代码,提交修改) | 300 | |
Reporting | 报告 | 120 | |
· Test Report | · 测试报告 | 30 | |
· Size Measurement | · 计算工作量 | 30 | |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 60 |
三、接口实现
指导书给出的接口为
int gen_chains_all(char* words[], int len, char* result[]); int gen_chain_word(char* words[], int len, char* result[], char head, char tail, char reject, bool enable_loop); int gen_chain_char(char* words[], int len, char* result[], char head, char tail, char reject, bool enable_loop);
在设计接口的时候我们主要考虑的问题有两点
-
和gui,cli,单元测试的对接
单元测试采用的是google的gtest工具(cpp单元测试工具,在clion中有集成),cli和我们计算核心的源代码相同都是cpp,而gui采用的是Qt框架编写的,Qt是一种c++框架,能够正常调用cpp生成的dll文件
-
和交互小组的对接
合作小组采用的技术和我们小组相同,均为gui(Qt框架)+core(cpp),所以我们只需要约定好接口的名字和参数就可以正常进行松藕合测试
Loose coupling的概念是相对于紧耦合而言的,要求程序之间之间的联系尽量松散可变,如果要素之间的交互存在较低的交互频率,那么就容易出现松散耦合,而Information Hiding注重信息隐藏,考虑到我们的两点需求与这两个原理,以及小组间命名的习惯,我们最终采用的接口为
int countChains(char *words[], int wordsLen, char *result[]); /** * 以单词形式统计最长的单词链 * @param words 单词数组 * @param wordsLen 单词数组大小 * @param result 结果数组 * @param head 指定首字母,如果指定,则范围为 'a' ~ 'z',若没有指定则为 0 * @param tail 指定尾字母,如果指定,则范围为 'a' ~ 'z',若没有指定则为 0 * @param ban 指定禁用的首字母,如果指定,则范围为 'a' ~ 'z',若没有指定则为 0 * @param allowLoop 是否允许环 * @return 最长的单词链的长度 */ int getLongestWordChain(char *words[], int wordsLen, char *result[], char head, char tail, char ban, bool allowLoop); /** * 以字符形式统计最长的单词链 * @param words 单词数组 * @param wordsLen 单词数组大小 * @param result 结果数组 * @param head 指定首字母,如果指定,则范围为 'a' ~ 'z',若没有指定则为 0 * @param tail 指定尾字母,如果指定,则范围为 'a' ~ 'z',若没有指定则为 0 * @param ban 指定禁用的首字母,如果指定,则范围为 'a' ~ 'z',若没有指定则为 0 * @param allowLoop 是否允许环 * @return 最长的单词链的长度 */ int getLongestCharChain(char *words[], int wordsLen, char *result[], char head, char tail, char ban, bool allowLoop);
四、计算模块设计实现
4.1 算法模型
对于这道题,我们采用的模型是图模型,我们建立了一个具有 26 个节点的图,分别用 int
表示,0 ~ 25
节点分别对应字母 a ~ z
。单词是图上的边,单词的首字母和尾字母分别指示了这个边的起点和终点,比如说 banana
这个单词的起点就是 b
,终点是 a
,单词的长度是这条边的权,也就是如下图的模型(局部)
那么寻找“单词链”的过程就类似于寻找路径的问题。
需要注意的是,这是一个复杂图,即可能存在两个或者两个以上的边的起点和终点相同,比如说 banana
和 ba
就会造成这种现象,同样的,这个图中有可能存在自环的,比如说 aba
。
4.2 问题分析
文档中提出的多种需求,可以被分为两类,一个是求解所有的路径,一个是求解最长的路径。
对于求解所有的路径,唯一的方法是进行 DFS,因为求解所有的路径是一种枚举,DFS 也是一种枚举策略。
在进行 DFS 的时候,需要利用一个 visit
数组,这个数组用来避免重复路径的出现,考虑到这个图是一个复杂图,所以没有办法像比较常见的做法,记录一个对于点的 visit
数组(因为可以重复遍历一个点),根据题目要求,我们应当记录一个对于边的 visit
数组。按照这种逻辑,此时的复杂度与边的个数相关,经过不严谨的推断,如果边的个数为 n ,那么此时的算法复杂度大致是 O(n!) 。
这种 DFS 的思路,无论是对于成环的还是不成环的,都是适用的,因为 visit
记录的对象是以边为单位的。
对于求解最长的路径,最简单的思路是首先枚举所有的路径,然后挑出最长的一条路径来,此时的算法复杂度是与第一个问题保持一致的。但是考虑到第二个问题的输入数据的规模是要大于第一个问题的,所以用这种无脑 DFS 的方式是没有办法满足所有的要求的。所以可以对这个算法进行一定程度的优化。这个问题可以被描述为“在图上寻找最长路径”的问题。
首先对于无环的情况,是可以先对图上的点进行拓扑排序,然后按照拓扑序进行动态规划,计算最长路径,这种方法是建立在“最长路径的子路径同样是最长子路径”的基础上的,是符合动态规划特征的。为了输出答案,我们只需要再维护一个一维的数组用于还原方案即可。此时的时间复杂度是 O(n) 。
对于有环的情况,会发现没有办法使用“拓扑排序 + 动态规划”的思路,这是因为拓扑排序要求图是无环的。针对这种情况,我们可以考虑首先利用 tarjan
算法求解出 scc
,然后对于“顶层图”,也就是每个节点都是一个 scc
的图,进行“拓扑 + 动态规划”的思路,而对于 scc
内部,只能再次使用 DFS 算法。
可是当一个 scc
过大的时候,采用 DFS 依然会导致时间复杂度飙升,这里可以考虑记忆化搜索,利用一个 int128
去记录路径中已经出现的边,并且再记录一个搜索的起点,这两项会确定唯一一个“最长路径”,然后将其记录下来,即可完成记忆化搜索,如下图所示:
map<pair<int, pair<long, long>>, int> remember;
但是在我们实现的时候,并没有实现完整的这个算法,这是因为这种有环图中的路径还原我们之前采用的同样是 DFS,我们的数据结构在进行这种记忆化搜索的还原时,支持性不太强,最终由于时间原因,没有能够实现。
对于题目中对于“指定头结点,尾节点”的限制行为,可以通过在正常的算法开始之前限定起始节点或者在算法运行结束之后挑选终止节点的方法解决。对于禁止某个头结点的行为
4.3 具体实现
由上面的分析可以看出,计算核心的数据结构应当采用图结构,所以我们建立了一个类 Graph
去作为数据结构的主体,其内部主要维护了一个邻接链表来记录图结构,之所以采用这种形式,是因为在其上实现的算法经常有查看某个特定定点的所有出边的操作。
Graph
上实现了 tarjan(), topoSort(), deleteEdge(), compressGraph()
等多种与图联系比较紧密的算法。
关于边的结构,实现了一个 Edge
类,用于记录边的起点和终点,权。同时为了方便 C 风格的算法实现(我个人感觉很多算法描述起来,确实是用 C 风格而不是 python 风格要更加自然一些),我们引入了 index
这个 int
量,表示对于 Edge
的唯一标识。
在介绍了面向对象的部分后,介绍我们面向过程的部分,我们的算法主体是面向过程的,一共分为三个 API,表示三种需求
/** * 统计所有的单词链 * @param words 单词数组 * @param wordsLen 单词数组大小 * @param result 结果数组,每一个元素是一个单词链 * @return 单词链的个数 */ int countChains(char *words[], int wordsLen, char *result[]); /** * 以单词形式统计最长的单词链 * @param words 单词数组 * @param wordsLen 单词数组大小 * @param result 结果数组 * @param head 指定首字母,如果指定,则范围为 'a' ~ 'z',若没有指定则为 0 * @param tail 指定尾字母,如果指定,则范围为 'a' ~ 'z',若没有指定则为 0 * @param ban 指定禁用的首字母,如果指定,则范围为 'a' ~ 'z',若没有指定则为 0 * @param allowLoop 是否允许环 * @return 最长的单词链的长度 */ int getLongestWordChain(char *words[], int wordsLen, char *result[], char head, char tail, char ban, bool allowLoop); /** * 以字符形式统计最长的单词链 * @param words 单词数组 * @param wordsLen 单词数组大小 * @param result 结果数组 * @param head 指定首字母,如果指定,则范围为 'a' ~ 'z',若没有指定则为 0 * @param tail 指定尾字母,如果指定,则范围为 'a' ~ 'z',若没有指定则为 0 * @param ban 指定禁用的首字母,如果指定,则范围为 'a' ~ 'z',若没有指定则为 0 * @param allowLoop 是否允许环 * @return 最长的单词链的长度 */ int getLongestCharChain(char *words[], int wordsLen, char *result[], char head, char tail, char ban, bool allowLoop);
函数调用关系如下:
对于 countChains
:
对于 getLongestWordChain()
对于 getLongestCharChain()
可以看到 getLongestWordChain()
和 getLongestCharChain()
的调用关系基本一致,这是因为在设计的时候为了优化进行的代码冗余性提升。
五、无警告编译
为了在 clion
中显示警告信息,我们需要在 CMakeList.txt
中这样调整信息:
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra")
然后运行时就会将警告信息显示在 clion
的控制台中,最终完全没有警告的截图如下:
六、UML 图
core通过调用Graph、Edge类的构造器建图,然后根据用户要求进行拓扑排序,求强连通分量以及动态规划求最长链等操作
七、性能改进
我们利用 gperf
进行性能分析,并利用 pprof
进行可视化转换。
7.1 存储单词链
在对 -n
功能进行性能分析的时候,我们发现了一个现象:
明明只是简单的保存发现的链,居然会占如此多的性能,所以我们研究了这个函数,注意到他实际上进行了两遍复制(在生成一条单词链的时候进行了一遍,在将生成单词链复制到 result
中又进行了一遍),所以我们修改了这个函数,将其改为只需要复制一遍,如下所示:
void saveChain(int &chainCount, char *result[], vector<char *> &chain) { int resultLen = 0; for (const auto &word: chain) { resultLen += (int) strlen(word) + 1; } char *resultChain = (char *) malloc(resultLen); int i = 0; for (const auto &word: chain) { int len = (int) strlen(word); for (int j = 0; j < len; j++) { resultChain[i++] = word[j]; } resultChain[i++] = ' '; } resultChain[resultLen - 1] = '\0'; result[chainCount++] = resultChain; }
修改后的性能图,发现此方法性能有一个明显提高:
7.2 OpenMp 并行
性能消耗最大的函数是这个
void WordChain::Loop::dfsSccDistance(Graph *scc, int start, int cur, int step, bool edgeVisit[], int vertexVisit[], int sccDistance[][MAX_VERTEX]) { vertexVisit[cur]++; // 遍历当前节点的每一条非自环边 for (auto &edge: scc->getVertexEdges()[cur]) { int target = edge->getTarget(); if (!edgeVisit[edge->getIndex()]) { edgeVisit[edge->getIndex()] = true; // 这里记录自环情况,如果已经发生过一次自环计算了,那么就不能发生第二次了 int targetWeight = (vertexVisit[target] > 0) ? 0 : scc->getVertexWeight(target); sccDistance[start][target] = std::max(sccDistance[start][target], step + 1 + targetWeight); dfsSccDistance(scc, start, target, step + 1 + targetWeight, edgeVisit, vertexVisit, sccDistance); edgeVisit[edge->getIndex()] = false; } } vertexVisit[cur]--; }
考虑到 scc 内的 DFS 是不相关的,所以考虑对于这个部分进行并行,openMp
是一个多线程工具,我们如果想要使用它,需要首先修改 CMakeList.txt
文件
find_package(OpenMP REQUIRED) # 在链接之前先判断是否已经搜索到openmp if(OpenMP_FOUND) target_link_libraries(${PROJECT_NAME} OpenMP::OpenMP_CXX) else() message(FATAL_ERROR "openmp not found!") endif()
然后在需要并行的代码里,利用编译指令来开启并行,如下所示:
#pragma omp parallel for schedule(dynamic) for (int j = 0; j < ALPHA_SIZE; j++) { // 如果该节点属于某个颜色 if (rawGraph->getSccIndex()[j] == i) { bool *edgeVisit = new bool[MAX_EDGE]; memset(edgeVisit, 0, sizeof(bool) * MAX_EDGE); int vertexVisit[MAX_VERTEX] = {0}; // 进行 dfs dfsSccDistance(rawGraph->getSccs()[i], j, j, 0, edgeVisit, vertexVisit, sccDistance); delete[] edgeVisit; } }
对于边界区域,也同样可以利用 critical
指出,来保证正确性:
#pragma omp critical { sccDistance[start][target] = std::max(sccDistance[start][target], step + edgeWeight + targetWeight); }
最终效果确实实现了并行,我们可以在算法进行过程中打印输出语句来检验,如下所示,可以发现语句呈现乱序状态,正是由于线程切换造成的:
同时从桌面的性能检测工具可以看出,并行化的程序确实极致压榨了 CPU 的性能(成功让我的电脑在 30 分钟内电量归零):
我们进行这个优化是希望可以处理一个 scc 中有多条边的时候,可以更快的进行处理,在理论上可以优化 12 倍(因为我的电脑是 12 核的),但是我们发现,即使是这样,我们也没有办法跑通一个较为复杂的 scc 图,这是因为这种优化其实是一种 O(1) 形式的优化,并没有办法大幅度提高效率。所以非常遗憾。!
八、契约编程
契约式设计(Design by Contract)主要是指软件设计者提供精确、可验证的接口。优点在于可以保证调用接口的模块有一定的进入条件,以及退出时有特定的属性,可以使程序设计更加清晰,以及保证设计的准确性。缺点是增加了程序实现的复杂度,也增加了程序本身的复杂度,降低性能。我们在结对编程当中主要在gui和cli调用core中的接口时采用了该方法
九、单元测试
单元测试
单元测试采用了google的gtest测试框架,主要由手工样例测试构成,力求测试到所有参数和所有异常情况,保证代码覆盖率,以getLongestCharChain
接口为例
void testgetLongestCharChain(char *words[], int len, char *ans[], int ans_len, char head, char tail, char ban, bool allowLoop) { char **result = (char **) malloc(10000); int out_len = 0; ASSERT_TRUE(ans_len == out_len); std::sort(result, result + out_len, [](char * p, char *q) { return strcmp(p, q) < 0; }); std::sort(ans, ans + ans_len, [](char * p, char *q) { return strcmp(p, q) < 0; }); // for (int i = 0; i < ans_len; i++) { if (result != nullptr) ASSERT_TRUE(strcmp(ans[i], result[i]) == 0); } }
对应的测试样例如下
TEST(getLongestCharChain, singleWordNoLoop) { char *words[] = {"aaa"}; char *ans[] = {}; testgetLongestCharChain(words, 1, ans, 0, 'a', 'a', 0, false); } TEST(getLongestCharChain, singleWord) { char *words[] = {"aaa"}; char *ans[] = {}; testgetLongestCharChain(words, 1, ans, 0, 'a', 'a', 0, true); } TEST(getLongestCharChain, rparam) { char *words[] = {"element", "heaven", "table", "teach", "talk"}; char *ans[] = {"table", "element", "teach", "heaven"}; testgetLongestCharChain(words, 5, ans, 4, 0, 0, 0, true); } TEST(getLongestCharChain, cparam) { char *words[] = {"element", "heaven", "teach", "talk"}; char *ans[] = {"element", "teach", "heaven"}; testgetLongestCharChain(words, 4, ans, 3, 0, 0, 0, false); } TEST(getLongestChariChain, hparam) { char *words[] = {"algebra", "apple", "zoo", "elephant", "under", "fox", "dog", "moon", "leaf", "trick", "pseudopseudohypoparathyroidism"}; char *ans[] = {"pseudopseudohypoparathyroidism", "moon"}; testgetLongestCharChain(words, 11, ans, 2, 'p', 'n', 0, false); } TEST(getLongestCharChain, hparamtParamLoop) { char *words[] = {"algebra", "apple", "zoo", "elephant", "under", "fox", "dog", "moon", "leaf", "trick", "pseudopseudohypoparathyroidism"}; char *ans[] = {"pseudopseudohypoparathyroidism", "moon"}; testgetLongestCharChain(words, 11, ans, 2, 'p', 'n', 0, true); } TEST(getLongestCharChain, hpramTparamLoopNew) { char *words[] = {"algebra", "apple", "zoo", "elephant", "under", "fox", "dog", "moon", "leaf", "trick", "pseudopseudohypoparathyroidism"}; char *ans[] = {"pseudopseudohypoparathyroidism", "moon"}; testgetLongestCharChain(words, 11, ans, 2, 'p', 'n', 0, false); }
单元测试覆盖率:
core.cpp和graph.cpp两个计算核心的文件代码行数覆盖率和分支覆盖率都在90%以上
十、异常处理
10.1 异常处理框架
我们采用了 CPP 的异常处理机制进行异常处理。我们定义了自己的异常,这个异常继承自 std::runtime_error
同时实现了自己的构造器方法,并且重写了 what()
方法用于打印异常信息,其实现如下
class MyException : std::runtime_error { public: ErrorType errorType; explicit MyException(ErrorType errorType); const char *what() const noexcept override; }; MyException::MyException(ErrorType errorType) : runtime_error(errorMap.find(errorType)->second), errorType(errorType) { } const char *MyException::what() const noexcept { return errorMap.find(errorType)->second.c_str(); }
我们定义了枚举变量 ErrorType
来实现异常的多样性,具体如下所示
enum ErrorType { FILE_NOT_FIND = 1, MULTI_FILE_PATH, PARAMETER_NOT_EXISTS, NO_FILE_PATH, NO_CHAR_ERROR, CHAR_FORM_ERROR, ALLOC_MEMORY_ERROR, MULTI_WORK_ERROR, NO_WORK_ERROR, FIRST_CHAR_DUPLICATE, ENABLE_LOOP_DUPLICATE, N_WORK_WITH_OTHER_PARAMETER, WORD_NOT_AVAILABLE, HAVE_LOOP, TOO_MANY_CHAINS }; const static std::unordered_map<ErrorType, std::string> errorMap = { {MULTI_FILE_PATH, "指定了多个文件路径,请仅指定单一路径!"}, {PARAMETER_NOT_EXISTS, "参数不存在,请重新输入!"}, {MULTI_WORK_ERROR, "指定了多个任务,请仅指定一个任务!"}, {NO_CHAR_ERROR, "指定首尾字母时忘记字母参数!"}, {CHAR_FORM_ERROR, "指定字母时格式不正确!只允许指定大小写字母!"}, {FIRST_CHAR_DUPLICATE, "重复指定首字母!"}, {ENABLE_LOOP_DUPLICATE, "重复指定有环参数!"}, {NO_FILE_PATH, "参数中不存在文件路径!"}, {N_WORK_WITH_OTHER_PARAMETER, "-n 参数不支持和其他参数共同使用!"}, {NO_WORK_ERROR, "未指定任务,请指定一个任务!"}, {WORD_NOT_AVAILABLE, "不存在符合要求的单词链"}, {FILE_NOT_FIND, "输入文件没有找到"}, {HAVE_LOOP, "无环图中有环"}, {TOO_MANY_CHAINS, "单词链过多"} };
最后我们在 control
方法中对于抛出的异常进行捕获,并将 what()
中的信息打印到标准错误流中:
try { //.... } catch (MyException &e) { cerr << e.what(); }
10.2 异常种类
10.2.1 MULTI_FILE_PATH
指在输入命令行命令的时候,输入了多个输入文件,就会抛出这个异常,如
./wordList input1.txt input2.txt
10.2.2 PARAMETER_NOT_EXISTS
输入的参数不是题目给定的,就会抛出这个异常,比如说
./wordList input.txt -f
10.2.3 MULTI_WORK_ERROR
尽管这个程序有多个参数,但是有些参数是冲突的,比如说同时指定求解单词链的个数和最长单词链,这时就会抛出这个异常,比如说:
./wordList input.txt -n -w
10.2.4 NO_CHAR_ERROR
在指定 -h, -t, -j
参数的时候,没有指定对应的字符,这时就会抛出这个异常,比如说:
./wordList input.txt -n -j
10.2.5 CHAR_FORM_ERROR
在指定 -h, -t, -j
参数的时候,指定的字符不是字母,这时就会抛出这个异常,比如说:
./wordList input.txt -n -j 0
10.2.6 FIRST_CHAR_DUPLICATE
在指定 -h, -t, -j
参数的时候,指定的字符存在多个,这时就会抛出这个异常,比如说:
./wordList input.txt -n -j a -j a
10.2.7 ENABLE_LOOP_DUPLICATE
在重复指定 -r
参数后,就会抛出这个异常
./wordList -r input.txt -r
10.2.8 NO_FILE_PATH
当没有指定输入文件的时候,会抛出这个异常
./wordList -r
10.2.9 N_WORK_WITH_OTHER_PARAMETER
-n
参数不支持和其他参数共同使用,如果同时使用,就会抛出这个异常:
./wordList -r input.txt -n
10.2.10 NO_WORK_ERROR
没有指定任务的时候,会抛出这个异常
./wordList input.txt
10.2.11 WORD_NOT_AVAILABLE
不存在符合要求的单词链时,会抛出这个异常
输入命令:
./wordList input.txt -j a -n
input.txt
中的内容为
ab bc cd
10.2.12 FILE_NOT_FIND
输入文件没有找到,即不存在指定的输入文件
./wordList notExisitInput.txt -w
10.2.13 HAVE_LOOP
无环图中有环。
输入命令:
./wordList input.txt -n
input.txt
中的内容为
ab ba
十一、界面模块设计
GUI采用Qt编写,Qt是一种cpp框架,所以没有接口转换的问题,使用的IDE是Qt Creator,非常方便,界面设计只需要用鼠标点,查询事件可以通过跳转槽完成。
十二、界面模块与计算模块对接
GUI会在用户界面收集所有参数,然后通过core.h引入函数签名调用core中的接口完成查询任务,通过在跳转槽中捕获异常可以在gui得到关于异常的提示
查询最长单词链:
查询最长字母链:
查询所有单词链:
文件导出功能:
异常提示:
时间提示:
十三、结对过程
13.1 时间地点
我们选择在新主楼进行结对,结对的时间是 3 月的 6,7,8,9,10,11,12,15 日。基本上只要上完课了就会去进行结对。以下是我们结对的图片:
13.2 结对形式
我们基本上就按照《构建之法》中提到的结对编程的方法,一个人敲代码,一个人领航,然后基本上过两个小时交换一次,大致一天可以交换两到三次。
驾驶员负责具体的代码书写,驾驶员负责查阅相关资料和指导实现。
13.3 结对过程
这个项目被我们分割成了 6 个部分:
-
指令解析,
-
文件读入,
-
计算核心
-
输出
-
GUI
-
测试
指令解析是我们进行的第一个模块,因为第一次结对很尴尬,所以我们立刻就开始了。就一个人写,一个人看着,所幸这个地方并不太难。领航员时刻盯着说明文档,避免出现没有考虑到的错误,然后驾驶员专注于按照领航员的说法进行正确的解析和对于异常情况的处理。
在文件读入模块时,领航员考虑利用休息时间整理出一套完整的实现算法,这是因为单词的读入要实现“分词”和“去重”,同时要考虑文件读入的性能。所以领航员实现了一套基于状态机的算法,可以简洁优雅地完成“分词”功能,但是发现驾驶员在心中也已经有了自己的打算,更加侧重于文件读写优化,加上敲代码的过程不是听领航员讲课的过程,所以驾驶员在没有完全弄清楚领航员的设计前,就基本已经完成了代码。这个现象我们会在下一节讨论的。
在计算核心部分,我们明白任务的算法成分很高,就约定休息一天进行调研,然后在进行结对编程
在 CLI 输出部分,因为在计算模块调试的时候,基本上也需要利用输出功能进行调试,所以我们只稍微将模块解耦了一下,就完成了这个部分。
对于 GUI 部分,由队友进行代码审核,而我担任驾驶员。
在测试部分,我们一起构造了样例,并且进行了比较流畅的结对编程,体验比较好。
十四、结对编程分析
结对编程的优点:
-
面对面交流沟通高效
-
一个人写一个人看的编程方式发现错误的速度很快
-
能够取长补短相互学习,毕竟在学习的过程中有一个直接能解决问题的人比使用搜索引擎寻找答案高效很多
-
结对编程促使我学习了很多新内容,例如gtest、qt设计和覆盖性分析
结对编程的缺点:
-
两个人写同一个模块有时间上的重叠,很难确定是否比分工合作最后再整合的效果好,例如我们第一次结对的时候两个人盯着一个命令行解析写了一下午
-
可能是因为有人监督大大减少了摸鱼的情况,总感觉两个人一起工作的时候十分劳累
-
写同样的程序难免出现意见不统一的情况,但是好在我和我的队友都比较好说话
自己的优点:
-
擅长造数据和测试,特别是针对性构造数据(虽然现在有点摆,不想干这活了)
-
使用c++写过较大型的项目(编译课设),比较熟悉c++和stl等相关内容
-
学习新东西比较快,例如clion中的单元测试,qt框架等都是新学的内容
自己的缺点:
-
不爱写注释,和队友形成了鲜明的对比
-
自己常用的编程环境都是macos下的,导致在交换模块和生成dll时有许多局限性
-
大学才开始学习计算机相关内容,不太懂算法,尤其害怕数学相关的内容
队友的优点:
-
善于沟通,工作认真负责
-
比较细致,善于发现代码中的问题,并且有写注释的好习惯
-
文字功底好,写文档的能力强
-
算法和数学强于我,并且学习能力强
队友的缺点:
-
对代码风格的要求和我不太一样,并且对代码风格要求比较高,可能导致编程的时候有一些体验的问题
十五、PSP 实际
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 60 |
· Estimate | · 估计这个任务需要多少时间 | 30 | 30 |
Development | 开发 | 1470 | 1480 |
· Analysis | · 需求分析 (包括学习新技术) | 240 | 300 |
· Design Spec | · 生成设计文档 | 30 | 30 |
· Design Review | · 设计复审 (和同事审核设计文档) | 30 | 30 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | 10 |
· Design | · 具体设计 | 60 | 120 |
· Coding | · 具体编码 | 720 | 480 |
· Code Review | · 代码复审 | 60 | 30 |
· Test | · 测试(自我测试,修改代码,提交修改) | 300 | 480 |
Reporting | 报告 | 120 | 180 |
· Test Report | · 测试报告 | 30 | 90 |
· Size Measurement | · 计算工作量 | 30 | 30 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 60 | 60 |
合计 | 1660 | 1670 |
十六、模块互换
与我们交换模块的小组为 cjj 和 wxz,他们的学号分别是 20373021 和 20373020。
因为在做设计的时候,我们就和互换小组商量过 core
接口问题和讨论过异常的形式,所以说我们的接口基本上完全一致,在互换过程中没有出现太大的纰漏,只有在我们的 core
加上他们的 GUI 测试异常的时候,发现异常无法捕获,没有办法确定到底是哪里的 bug,最后我们用
catch(...)
的形式完成了捕获。
以下是效果图:
通过单元测试
我们的 GUI 加他们的 core
他们的 GUI 加我们的 core