项目 | 内容 |
---|---|
本作业所属课程 | 2022年北航敏捷软件工程教学实践 |
本作业要求 | 结对编程项目-最长英语单词链 |
个人课程目标 | 掌握结对协作能力、VIsual studio等开发工具使用技能、开发高质量软件能力、使用分析与测试工具对代码进行分析和测试 |
本作业在哪个具体方面帮助我实现目标 | 学习使用工程化方法对软件实例有初步分析和认知,进行结对项目实际体验 |
- 教学班级:周五班
- 项目地址:https://github.com/ZerglingChen3/LongestEnglishWordChain
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 60 |
· Estimate | · 估计这个任务需要多少时间 | 20 | 15 |
Development | 开发 | 2160 | 3070 |
· Analysis | · 需求分析 (包括学习新技术) | 120 | 150 |
· Design Spec | · 生成设计文档 | 60 | 90 |
· Design Review | · 设计复审 (和同事审核设计文档) | 240 | 120 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 60 | 30 |
· Design | · 具体设计 | 120 | 240 |
· Coding | · 具体编码 | 720 | 1440 |
· Code Review | · 代码复审 | 360 | 400 |
· Test | · 测试(自我测试,修改代码,提交修改) | 480 | 600 |
Reporting | 报告 | 140 | 340 |
· Test Report | · 测试报告 | 60 | 120 |
· Size Measurement | · 计算工作量 | 20 | 20 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 60 | 300 |
合计 | 2360 | 3470 |
信息隐藏是将敏感的或外部无需访问的信息封装在自身内部,使得外部不可见此类信息。
在我们的设计过程中,我们将图进行封装,外部只能通过给出的方法对图中的边和点进行添加和查询操作。图中的边又进一步封装,外部只能通过给定的方法访问指定的权值。接口设计决定了模块之间沟通的效率和效果。良好的接口设计应该可以让外部通过接口,简明快速地了解到接收的请求和返回的结果,而不关心实现的细节。
在我们的设计中,每个接口中的参数可以划分成两部分,分别是传入的请求相关参数和返回的结果相关参数。相关性强的参数往往放在一起,比如连通块个数和连通子图,后者的数目即是前者的值,这样的信息往往前后连续放置。返回值通常表示函数运行成功或异常。松耦合指模块之间的联系是很小的,对于一个模块的修改不必担心破坏其他关联模块的结构。
我认为,相比于上述两点,松耦合是一种更宽泛的状态,在我们的设计中,由于做到了良好的信息隐藏和接口设计,模块之间的耦合自然地下降,互相之间影响很少。我们将每个字母抽象成一个点,单词则是从首字母所在点到尾字母所在点的一条边。单词的长度作为对应边的边权。同时,我们需要特殊记录每个点的自环。
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
大致相同,不再赘述重复部分,主要差异有以下两点:
主要信息存储在Graph
、Edge
、Word
三个类中,三者逐层包含,将信息层层封装。主要功能封装在getwords
、output
、gens
三个接口中,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(∣Vi∣2)+∣Ei∣)∗∣S∣),其中 V i V_i Vi为第 i i i个连通块的点数, E i E_i Ei为第 i i i个连通块的边数, S S S为所有连通块的集合。
契约式设计(Design by Contract) /代码契约(Code Contrast) :
契约式设计强调三个基本概念:前置条件、后置条件、不变式。契约式设计要求模块在运行(调用)前满足前置条件,在运行之后结果满足后置条件,并且运行的结果中满足不变式所要求某些变量的不变。
契约式编程的优势是思路清晰,对模块之间耦合的规定和要求更加明确,同时可以消除一些模块之间的兼容性问题。缺点是其撰写、检查和实现的过程中往往需要结合语言本身的特性。
计算模块的单元测试部分包含文件读、写和用例测试两部分代码。
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接口,读取异常信息并最终显示结果。
与我们进行接口互换的组是:
我们两组均使用 C++ 做为项目语言,前端均为 python 实现。
我们两组均采用一个交接函数
char* call_by_cmd(int len, char* cmd)
来进行前后端的交接,不同的是我们的输出将答案输出和异常结果分开输出。答案输出到文件 solution.txt 中,异常输出到该函数的返回值中。而另外一组全部输出到返回值中。因此他们跑我们的 dll 模块时修改比较容易,而我们需要适配他们的输出,需要对输出内容进行格式解析,进行的比较复杂。
需要协商二人的时间,尤其在课程时间安排不一致、课余时间紧俏的情况下,需要牺牲很多休息时间来进行结对编程中需要一同完成的环节。