敏捷软工结对编程博客
项目 | 内容 |
---|---|
这个作业属于哪个课程 | 2022春季软件工程(罗杰 任健) |
这个作业的要求在哪里 | 结对编程项目-最长英语单词链 |
我在这个课程的目标是 | 学习软工的项目合作管理知识,提升软件开发技术 |
这个作业在哪个具体方面帮助我实现目标 | 学习敏捷开发中的PSP与结对编程的思想并付诸实践 |
Part0 准备
1.必要信息
教学班级:周五班
项目地址:https://github.com/BUAADreamer/Longest-English-word-chain
成员:18373466 战晨曦,19373573 冯张驰
2.PSP开发时间估计
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) |
---|---|---|
Planning | 计划 | 90 |
· Estimate | · 估计这个任务需要多少时间 | 90 |
Development | 开发 | 790 |
· Analysis | · 需求分析 (包括学习新技术) | 60 |
· Design Spec | · 生成设计文档 | 30 |
· Design Review | · 设计复审 (和同事审核设计文档) | 30 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 |
· Design | · 具体设计 | 60 |
· Coding | · 具体编码 | 360 |
· Code Review | · 代码复审 | 60 |
· Test | · 测试(自我测试,修改代码,提交修改) | 180 |
Reporting | 报告 | 100 |
· Test Report | · 测试报告 | 60 |
· Size Measurement | · 计算工作量 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 |
合计 | 980 |
Part1 设计
3.接口设计理念
接口,对于团队开发来说至关重要。只有确定了接口,才能够在开发时让接口的实现者与接口的调用者之间的交流尽可能少,有利于提高效率;反之,如果接口模模糊糊,那么接口的实现者以及接口的调用者各自干的工作就很有可能冲突,少不了重构。在这个事情上我算是体验深刻:在结对进入互换模块阶段时,我们发现我们提供的计算模块接口和互换组的接口不一样(因为官方给的接口是废的,两个组在互相没通知的情况下各自重新设计了接口),这导致我们没法互换模块测试。为了能实现互换模块,当时我们花了 1 个小时改我们这边的计算模块的接口,这个过程很痛苦且没有意义。倘若最开始有一个稳定的接口且双方都严格遵守接口,那估计就没有这种返工了。而在两边接口重新确定后,不管我怎么修改接口的具体实现(比如修 bug,性能调优等),其他部分的代码都不需要改动(如果其他地方是对的话),并且互换模块的时候也不会带来额外的工作量,这样其实就是实现了松耦合,非常舒服。
另外在设计接口的时候,要尽可能做到信息隐藏。信息隐藏指的是在设计和确定模块时,使得一个模块内包含的特定信息(过程或数据),对于不需要这些信息的其他模块来说,是不可访问的。在数据方面,CalcuCore
是我们整个项目的计算核心,但是其构造方法传入的属性对外界并没有什么用处,所以我们把 CalcuCore
中的属性都设计成了 private
。在过程方面,对 PairTestInterface
中的 4 个接口 API 来说,CalcuCore
中只有 4 个对应的方法需要被其知道,所以我们只把 CalcuCore
中的 4 个核心方法设计成 public
;而这几个核心方法所调用的其他工具方法却对外界没有什么用处,我们也不希望外界知道 CalcuCore
内部有这么几个方法,所以工具方法我们都设计成了 private
。
4.接口/算法的设计与实现
接口设计
由于我们事先阅读了后面阶段的要求,并且预先找到了互换模块的小组,所以我们设计了适用于 C#
的类似于官方接口的接口,接口中每个方法的功能,和官方接口同名方法的功能相同。
接口设计如下:
public static int gen_chain_word(List<string> words, List<string> result, char head, char tail, bool enable_loop);
public static int gen_chains_all(List<string> words, List<string> result);
public static int gen_chain_word_unique(List<string> words, List<string> result);
public static int gen_chain_char(List<string> words, List<string> result, char head, char tail, bool enable_loop);
我们将这些接口方法整合到了 PairTestInterface
中。在本地测试时,我们通过 CmdTestInterface
接口去驱动测试,调用 PairTestInterface
提供的接口,而 PairTestInterface
提供的接口调用 CalcuCore
中的具体计算方法。
接口实现
接口方法的实现,实际上是首先通过参数去实例化 CalcuCore
类(这是我们真正的核心计算类),然后调用 CalcuCore
类中对应的真正的计算方法 ,之后对返回的结果做异常处理,体现了层次化的思想。例如:
- 对于
gen_chain_word
,我们用给定的参数words
、head
、tail
、enable_loop
以及一些default
参数(辅助建图)去实例化一个CalcuCore
,然后调用CalcuCore
中的getMaxWordCountChain
方法进行具体的计算。 - 对于
CalcuCore
:- 其构造方法会使用
gen_chain_word
给定的参数,进行参数化建图。 getMaxWordCountChain
首先会调用数据检查方法dataCheck
,去检查数据中是否有隐含环以及是否允许在有环的情况下求解;如果数据中没有环,则调用重构图方法refactorGraph
对图进行预处理,接着调用快速算法fastGetMaxWordCountChain
在DAG
上跑dp
求解;如果数据中有环,且要求在有环的情况下求解,则调用暴力算法trivialGetMaxWordCountChain
求解最长链。
- 其构造方法会使用
- 对于异常:
- 主要是处理结果过长以及数据有环且不能求解这两种情况。
算法设计
首先,如果单词 A
的尾字母和单词 B
的首字母相同,则以 A
和 B
为结点,连接一条从 A
到 B
的有向边,在
O
(
n
2
)
O(n^2)
O(n2) 的时间内建立一张有向图。其中
n
n
n 是不同单词的种类数。有向图的边权我们并不关心,但是点权的设置要根据情况来:如果我们求解的最长链是以个数为指标,则点权为 1;如果是以字母数为指标,那么点权为单词的长度。
然后就是每个具体接口的求解算法:
gen_chain_word
的求解算法主要是 getMaxWordCountChain
,而 getMaxWordCountChain
的算法分为两部分。如果图是一个有向无环图,那么可以使用动态规划求解:
dp[word]
表示word
以word
结尾的单词链的最长长度,lastWord[word]
表示以word
结尾的最长链的前驱结点。- 初始化
dp[word]
为word
的点权,lastWord[word]
为空(即没有前驱)。 - 转移时,采用在拓扑排序的基础上进行状态转移的方式,假设
A
是B
的前驱,那么dp[B] = max(dp[B], dp[A] + weight[B])
,其中weight[B]
是B
的点权。假如有dp[A] + weight[B] > dp[B]
,那么还需要更新前驱,即lastWord[B] = A
。 - 最终统计答案时,需要看所有点的
dp
值,把最大的那个给记录下来,这就是最长链的长度。由于要求解出一条具体的链,所以还需要通过这条最长链的尾结点,根据lastWord
去找到所有的前驱,重构出整条链。 - 上面的算法过程是假设了没有设置头结点的开头字母。倘若要求指定开头字母的最长链,那么需要先进行重构图操作:做一遍拓扑排序,每次把入度为 0 且开头字母不是指定字母的单词加入队列中,等到从队列中弹出时则将其彻底删除掉。这样做完之后,使用剩下的单词重构图,可以保证拓扑序并列第一大的结点都是以给定字母开头,这样可以在重构的图上执行上面几步的操作,即可求出结果。
- 时间复杂度为 O ( n 2 + m ) O(n^2 + m) O(n2+m), n n n 为重构前图的点数, m m m 为重构后图的边数。
- 快速算法的流程图如图所示:
而如果图是一个有环图,那么则使用暴力求解:
- 选择一个以给定字母开头的单词,调用
getOneMaxWordCountChain
方法,找到以该单词开头的最长链。在搜索的每一步判断结尾是否符合字母要求,如果符合要求就看能否更新当前的最长链以及长度。在实现getOneMaxWordCountChain
的时候为了加速计算,需要传递一些冗余的参数,比如当前最长链列表。 - 暴力算法的流程图如图所示:
gen_chain_char
和 gen_chain_word
没有本质区别,二者调用的快速算法是同一个方法,暴力算法仅在更新长度时略有不同,故不再赘述。
gen_chain_all
的求解算法是纯暴力搜索:
-
枚举所有单词作为起点,然后以这个点开始
dfs
,求出来以这个点为开头的所有链。 -
由于最终结果不允许超过
20000
字符,所以可以实时统计答案的字符数,超过20000
就只搜索统计链的个数而不保存具体链的结果,防止内存消耗过大导致程序崩溃。要想在每个链上避免一些重复计算,则需要参数传递时好好考虑,比如传递当前链的时候应该传递最终的字符串而不是单词列表。 -
暴力算法过程大同小异,所以不再重复给出流程图。
gen_chain_word_unique
的求解我们组没有按照是否为有向无环图进行分类,因为我们在讨论的时候不确定线性算法的正确性,且在和其他组互测的过程中发现其他组的线性算法似乎存在一定的问题,所以最后统一使用了暴力搜索:
- 枚举所有单词作为起点,然后以这个点开始
dfs
,需要维护当前的首字母集合,保证在转移的时候不能转移到重复的首字母的单词。 - 由于要随时更新已经求得的最长链,所以要把已经求得的最长链作为参数传递下去。
- 暴力算法大同小异,所以不再重复给出流程图。
函数间关系
CalcuCore
中方法之间的调用关系示意图如图所示:
其中,A -> B
意味着方法 A
调用方法 B
。
5.计算模块UML
计算部分的 UML 如下(CalcuCore
中的私有方法因为过多且无需外界关心,故没有列出):
Part2 编码与测试
6.性能分析与优化
性能优化过程:
- 我们最开始的打算便是所有的求解都复用核心计算函数
getAllWordChain
,使用该方法找到所有单词链,最后把合法链挑出来,统计想要计算的结果即可。这样实现的后果是随手复制英语文章中的两句话都未必能在程序崩溃之前跑出结果。 - 在测好了暴力求解之后,我们发现了有向无环图具有优良的性质,可以按照拓扑序从前往后递推计算很多信息,所以我们针对
-w
和-c
进行了建图后拓扑排序 + 动态规划求解不带环的部分。但对于指定-h
参数,最开始我们没有想到很好的方案。 - 后来,我们发现了
-h
的部分可以额外用一遍拓扑排序删去没用但是拓扑序比较靠前的单词,这样得到的新的图就又可以动态规划求解了,所以我们编写了重新建图方法来扩大上面想到的算法的适用范围。至此,-w
和-c
参数搭配-h
和-t
参数,且数据无环时都可以快速求解。 - 最后,我们对每种问题的暴力方法根据题目的不同去单独编写:对于
-w
,-c
,-m
,其暴力算法无需保所有的链,所以只需要维护当前链以及已经找到的最长链即可;对于-n
,由于题目要求求出所有的链,但是又要求输出长度不能超过20000
,所以我们在保存链的同时维护了当前结果的长度,如果超过20000
字符则只统计链的个数,而不保存链,这样就能较好地避免因为链太多导致内存空间消耗过多。 - 最后的最后,说下没有优化的
-m
。我们注意到有别的组的同学使用动态规划了来解决,但是和他们对拍时发现了他们似乎存在 bug ,且我们组内两人也不确定这是不是假算法,于是就没优化这个部分,所以我们-m
的求解性能可能相比其他同学要差一些。
整个性能调优和回归测试上总计花了约 5 小时,调优前:
优化后:
可见优化后只有过于暴力(实际上也只能暴力)的 gen_chain_all
占用 CPU 仍然很高。
优化后几百个随机单词的有向无环图可以秒出 -w
、-c
,几十个随机单词的 -r
在对拍时也可以在三五秒之内算出结果。
7.契约式设计在结对中的体现
关于契约式设计,其强调三个基本概念:前置条件、后置条件、不变式。契约式设计要求模块在运行(调用)前满足前置条件,在运行之后结果满足后置条件,并且运行的结果中满足不变式所要求某些变量的不变。
在结对编程过程中,我发现自己设计计算模块的接口的时候常常忽略了把契约确定好(OO 白学了)。当在接口实现和测试的时候,我才会突然想到“忘了告诉接口调用者我想要参数满足xxx的性质了”,然后就是担心自己的接口被错误调用而导致 bug,最后解决方案就变成了防御式编程,最终“任尔风吹雨打,我自岿然不动“。这导致我的接口里面有时候存在一些冗余的对参数的检查,虽然接口更不容易被误调用了,但是写了很多不必要的代码。
综合来看,结对中我写的那部分代码可能并没有体现契约式的理念,反而体现了很多防御式的理念。
8.测试
本次项目我们用两种方式进行测试,一种是 visual studio
内部进行单元测试,另一种是使用 WordList.exe
接口在本地进行大量数据对拍测试。
单元测试
本次项目我们建立了四个单元测试类分别对四个主要模块进行测试
总的来说,单元测试采用局部和整体两部分测试逻辑。
局部测试
局部的测试主要针对于特定的类的方法进行测试,对于四个接口函数进行单独的样例测试,示例代码如下。数据主要是制造量少但是可以直接看出正确结果的典型数据。力求对输入输出和计算类,异常类中各个函数都有基本的正确性测试检验
public void CoreTest5()
{
List<string> words = new List<string>() { "gbps", "generate", "google",
"growing", "handle", "handling", "hardware", "has"};
List<string> res = new List<string>();
PairTestInterface.gen_chains_all(words, res);
Assert.AreEqual(res.Count, 10);
res = new List<string>();
PairTestInterface.gen_chain_word(words, res, 'g', 'e', true);
Assert.AreEqual(res.Count, 2);
res = new List<string>();
PairTestInterface.gen_chain_word_unique(words, res);
Assert.AreEqual(res.Count, 2);
res = new List<string>();
PairTestInterface.gen_chain_char(words, res, 'i', 'e', true);
Assert.AreEqual(res.Count, 0);
res = new List<string>();
PairTestInterface.gen_chain_char(words, res, 'h', 't', false);
Assert.AreEqual(res.Count, 0);
}
public void InputTest1()
{
string[] args = { "-w" ,"-t","a"};
CommandParser commandParser = new CommandParser(args);
ParseRes parseRes = commandParser.getParseRes();
Assert.AreEqual(parseRes.mode,1);
Assert.AreEqual(parseRes.end, 'a');
}
整体测试
整体测试主要是强调将整个预处理到计算的流程完整性,采用大量数据进行测试。由于这样的情况下并无法手动确定出正确结果,因此采用了对拍这一思路,使用和我们组在第四阶段交换的小组的 Core.dll
引入进行测试。具体在测试时调用了如下方法 TestOneSample
方法。
public void TestOneSample(String[] args)
{
List<string> res;
List<string> res1;
int a = 0;
try
{
res = CmdTestInterface.testOneSample(args);
}
catch(Exception e)
{
Console.WriteLine(e.Message);
a = 1;
try
{
res1 = TestOneSampleByOther(args);
}
catch (Exception e1)
{
a = 2;
Console.WriteLine(e1.Message);
}
Assert.AreEqual(2, a);
return;
}
try
{
res1 = TestOneSampleByOther(args);
Assert.AreEqual(res.Count, res1.Count);
}
catch (Exception e)
{
a = 1;
printArgs(args);
Assert.AreEqual(2, a);
}
}
其中 CmdTestInterface.testOneSample
方法是我们定义的一个顶层接口,输入参数列表,返回字符串列表形式的结果。TestOneSampleByOther
则是调用交换的小组的 core
进行计算逻辑,而其他的输入输出使用我们自己的代码进行处理。这两个函数都运行之后,将得到的两个结果进行比较即可判定整体的正确性。即类似计算机网络中的 ping
通,一旦可以跑完就说明整个流程都是正确的。
数据生成上主要包含回归测试和指令组合覆盖性测试。对于每一个文件都使用四层循环遍历所有可能的两两指令组合以及首尾字母组合,覆盖性测试代码如下。
public void CoreTest2()
{
int testNum = 5;
String baseFile = "C:/Users/fzc/source/repos/Longest-English-word-chain/Longest-English-word-chain/TestFile{0}.txt";
String testFile = String.Format(baseFile,testNum);
for (int j = 0; j < 26; j++)
{
for(int k = 0; k < 26; k++)
{
String head = ('a' + j).ToString();
String tail = ('a' + k).ToString();
foreach (char c1 in validCmdChars)
{
ArrayList argList = new ArrayList() { "-" + c1 };
if (c1 == 't') argList.Add(tail);
if (c1 == 'h') argList.Add(head);
argList.Add("");
for (int i = 1; i <= testNum; i++)
{
testFile = String.Format(baseFile, i);
argList[argList.Count - 1] = testFile;
TestOneSample(getArgs(argList));
}
foreach (char c2 in validCmdChars)
{
argList = new ArrayList() { "-" + c1 };
if (c1 == 't') argList.Add(tail);
if (c1 == 'h') argList.Add(head);
argList.Add("-" + c2);
if (c2 == 't') argList.Add(tail);
if (c2 == 'h') argList.Add(head);
argList.Add("");
for (int i = 1; i <= testNum; i++)
{
testFile = String.Format(baseFile, i);
argList[argList.Count - 1] = testFile;
TestOneSample(getArgs(argList));
}
}
}
}
}
Console.WriteLine(correctNum);
}
单元测试结果展示
可以看到每个模块的单元测试覆盖率都达到了90%以上,总体覆盖率为96%,这个过程也确实发现了自己和交换小组的很多bug,效果还是较好的。
本地CLI随机数据对拍
本地 CLI 对拍主要是使用我们的 me.exe
和其他小组的 other.exe
对正确性部分进行随机数据测试。这部分并不属于单元测试,但因为我们做了,所以这里也介绍一下。
对拍主要编写了 test.cpp
和 create.cpp
两个文件。
test.cpp
负责驱动对拍以及检查答案:
- 编译一遍数据生成脚本
create.cpp
。 - 执行
create.exe
,并把输出的数据重定向到in.txt
。 - 随机从参数池中生成一组合法命令行参数:
- 首先是从
-n
,-m
,-w
,-c
中随机选择一个作为主参数。 - 然后根据参数的自身限制,随机生成其他可以组合的辅参数。
- 首先是从
- 执行
me.exe
和other.exe
,将输出的内容写到各自的输出文件中。 - 比较结果的一致性,我使用的判定方法都比较简单:
- 对于
-n
,只看第一行的个数是否一样。 - 对于
-m
和-w
,只看输出的单词行数以及首尾字母。 - 对于
-c
,看首尾字母,并计算字符个数,比对是否一致。
- 对于
create.cpp
的负责数据生成,由于时间紧张,所以只生成了两类特殊的随机数据:
- 纯随机单词数据的生成策略:每次随机从可见字符里面输出一串字符。根据被测程序的性能,可以控制输出字符串的长度。
- 有向无环图生成策略:考虑按顺序枚举首字母从
a
到z
,假设当前枚举到首字母为ch
,则可以生成随机个数的以ch
为开头的单词,且这些单词的尾字母必须字典序比ch
大,这样保证生成出来的数据不能往之前生成的单词连有向边,所以生成的数据就是有向无环图。
对拍时,我们组发现的自己的 bug 有:
- 最长单词链算法中单个单词也误当作链了。
-n
时没有输出到stdout
。- 性能调优过程中出现的算法错误。
我们组发现的别的组的 bug 有:
- 最长单词链算法中单个单词也误当作链了。
-m
算法错误。-c
最长链算出来不是最长的。
OO 互测既视感。
9.异常处理
本次项目主要确定了5种异常并进行测试。描述和测试样例如下:
错误的参数 CommandInvalidException
描述
- 没有指定主参数,即
n/w/m/c
至少出现一个 - 文件参数没有放在最后的位置
- 出现非法字符(出现了
- n
这样的横线没有和字母连起来的情况或者其他非法字符)
测试
public void CommandInvalidExceptionTest()
{
string[] args = { "-n", "t", "C:/Users/fzc/source/repos/Longest-English-word-chain/Longest-English-word-chain/TestFile1.txt" };
int ans = 0;
try
{
CmdTestInterface.testOneSample(args);
}
catch (CommandInvalidException e)
{
ans = 1;
Console.WriteLine(e.Message);
}
Assert.AreEqual(1, ans);
args = new string[] { "-n", "-!", "C:/Users/fzc/source/repos/Longest-English-word-chain/Longest-English-word-chain/TestFile2.txt" };
ans = 0;
try
{
CmdTestInterface.testOneSample(args);
}
catch (CommandInvalidException e)
{
ans = 1;
Console.WriteLine(e.Message);
}
Assert.AreEqual(1, ans);
args = new string[] { "-r", "C:/Users/fzc/source/repos/Longest-English-word-chain/Longest-English-word-chain/TestFile2.txt" };
ans = 0;
try
{
CmdTestInterface.testOneSample(args);
}
catch (CommandInvalidException e)
{
ans = 1;
Console.WriteLine(e.Message);
}
Assert.AreEqual(1, ans);
}
错误的参数组合 CommandComplexException
描述
两两参数冲突如下
-n | -w | -m | -c | -h | -t | -r | |
---|---|---|---|---|---|---|---|
-n | × | × | × | × | × | × | × |
-w | × | × | × | × | |||
-m | × | × | × | × | × | × | × |
-c | × | × | × | × | |||
-h | × | × | × | ||||
-t | × | × | × | ||||
-r | × | × | × |
上表中打 ×
的位置说明两者是冲突的
测试
public void CommandComplexExceptionTest()
{
string[] args = { "-n","-n", "C:/Users/fzc/source/repos/Longest-English-word-chain/Longest-English-word-chain/TestFile1.txt" };
int ans = 0;
try
{
CmdTestInterface.testOneSample(args);
}
catch (CommandComplexException e)
{
ans = 1;
Console.WriteLine(e.Message);
}
Assert.AreEqual(1, ans);
args = new string[] { "-n", "-m", "C:/Users/fzc/source/repos/Longest-English-word-chain/Longest-English-word-chain/TestFile2.txt" };
ans = 0;
try
{
CmdTestInterface.testOneSample(args);
}
catch (CommandComplexException e)
{
ans = 1;
Console.WriteLine(e.Message);
}
Assert.AreEqual(1, ans);
}
错误的文件格式 FileInvalidException
描述
- 没有以
.txt
结尾 - 文件不存在
测试
public void FileInvalidExceptionTest()
{
string[] args = { "-n", "C:/Users/fzc/source/repos/Longest-English-word-chain/Longest-English-word-chain" };
int ans = 0;
try
{
CmdTestInterface.testOneSample(args);
}
catch(FileInvalidException e)
{
ans = 1;
Console.WriteLine(e.Message);
}
Assert.AreEqual(1, ans);
args = new string[] { "-n", "C:/Users/fzc/source/repos/Longest-English-word-chain/Longest-English-word-chain/1.txt" };
ans = 0;
try
{
CmdTestInterface.testOneSample(args);
}
catch (FileInvalidException e)
{
ans = 1;
Console.WriteLine(e.Message);
}
Assert.AreEqual(1, ans);
}
结果过长 ResultTooLongException
描述
- 计算得到的结果列表过长
测试
样例数据:aa ab bb bc cc cd dd de ee ef ff fg gg gh hh hi ii ij jj jk kk kl ll lm mm mn nn no oo op pp pq qq
public void ResultTooLongExceptionTest()
{
string[] args = { "-n", "C:/Users/fzc/source/repos/Longest-English-word-chain/Longest-English-word-chain/TestFile7.txt" };
int ans = 0;
try
{
CmdTestInterface.testOneSample(args);
}
catch (ResultTooLongException e)
{
ans = 1;
Console.WriteLine(e.Message);
}
Assert.AreEqual(ans, 1);
ans = 0;
args = new string[] { "-n", "C:/Users/fzc/source/repos/Longest-English-word-chain/Longest-English-word-chain/TestFile7.txt" };
try
{
CmdTestInterface.testOneSample(args);
}
catch (ResultTooLongException e)
{
ans = 1;
Console.WriteLine(e.Message);
}
Assert.AreEqual(ans, 1);
}
出现单词环 HasImplicitLoopException
描述
- 输入的数据中含有隐含环
- 比如:
ab ba bc
这样的文本
测试
样例数据:abc ca
public void HasImplicitLoopExceptionTest()
{
string[] args = { "-n", "C:/Users/fzc/source/repos/Longest-English-word-chain/Longest-English-word-chain/TestFile6.txt" };
int ans = 0;
try
{
CmdTestInterface.testOneSample(args);
}
catch (HasImplicitLoopException e)
{
ans = 1;
Console.WriteLine(e.Message);
}
Assert.AreEqual(ans, 1);
ans = 0;
args = new string[] { "-w", "C:/Users/fzc/source/repos/Longest-English-word-chain/Longest-English-word-chain/TestFile6.txt" };
try
{
CmdTestInterface.testOneSample(args);
}
catch (HasImplicitLoopException e)
{
ans = 1;
Console.WriteLine(e.Message);
}
Assert.AreEqual(ans, 1);
}
10.UI设计
首先是原型设计,由于时间有限,只是在脑海中大致想了一下布局,即左半部分是输入部分,右半部分是输出部分。两边都是有一个文本框和若干操作按钮组成。
之后是技术调研,最开始选择 c#
就是为了体验一下 gui
设计,于是主要学习了 WinForm
的使用
之后就开始正式开发了,先新建了一个 C# windows
应用程序项目,之后利用工具箱中的组件拖动进主界面进行组件化开发。
主要使用了 Button/TextBox/Label/ToolTip/MessageBox/ShowDialog
这6个组件,拖动到设计界面后,调整位置和文字参数,之后在相应的 .cs
文件的绑定的控制函数中进行逻辑编写。这里以一个导入文件的按钮举例。当按下按钮时,就会触发这个事件函数,就会先弹出一个文件选择框,选择之后就会将文本导入到输入框内。其他的按钮的逻辑也类似,都是通过相应的按钮事件函数进行绑定。给每个按钮和文本都写好相应的事件后,就差不多写好了GUI界面的基本逻辑。
private void button1_Click(object sender, EventArgs e)
{
if (openFileDialog1.ShowDialog(this) == DialogResult.OK)
{
//保存路径
string filePath = Path.GetFullPath(openFileDialog1.FileName);
Console.WriteLine(filePath);
//读取数据
StreamReader str = new StreamReader(filePath);
//获取每行字符
string line;
string inputStr = "";
while ((line = str.ReadLine()) != null)
{
//通过','将行分裂为字符串组
inputStr += line+"\r\n";
}
str.Close();
textBox1.Text = inputStr;
}
}
整体逻辑为,导入文件或者直接输入到文本框内,设置相应的命令,之后对输入文本框内的字符串进行处理和分析,得到输出结果,输出结果实时打印到右侧的输出框。之后可以采用将结果导出到文件等操作。异常处理和完整处理计算流程主要放在导出分析结果文件这个按钮的事件函数中,按下之后就会触发整个的流程得到计算结果。
具体设计时的界面如下图所示

11.模块对接
对接计算模块
先在 visual studio 2019
中点击 项目-添加项目引用-浏览-导入Library.dll Core.dll
主要在 开始分析
按钮处进行预处理和计算流程运行,大致代码架构如下,可以很轻松的修改使用的 Core
主要在核心计算逻辑部分使用 Core
内定义的四个接口进行计算,其余部分都使用 Library.dll
中的类进行操作
private void button4_Click(object sender, EventArgs e)
{
//CoreType:0 使用我们的Core
//CoreType:1 使用交换的Core
int CoreType = 0;
if (CoreType == 0)
{
try
{
//预处理
string[] args = System.Text.RegularExpressions.Regex.Split(textBox3.Text, @"\s+");
CommandParser parser = new CommandParser(args);
ParseRes parseRes = parser.getParseRes();
WordListMaker maker = new WordListMaker();
string article = textBox1.Text;
List<string> wordList = maker.makeWordList(article);
List<string> result = new List<string>();
//核心计算逻辑
int outputMode = 1;
if (parseRes.cmdChars.Contains('n'))
{
outputMode = 0;
PairTestInterface.gen_chains_all(wordList, result);
}
else if (parseRes.cmdChars.Contains('w'))
{
PairTestInterface.gen_chain_word(wordList, result,
parseRes.start, parseRes.end, parseRes.enableLoop);
}
else if (parseRes.cmdChars.Contains('m'))
{
PairTestInterface.gen_chain_word_unique(wordList, result);
}
else
{
PairTestInterface.gen_chain_char(wordList, result,
parseRes.start, parseRes.end, parseRes.enableLoop);
}
//输出逻辑
Output output = new Output();
string outputRes = output.printWordChains(result, outputMode);
outputRes = outputRes.Replace("\n", "\r\n");
textBox2.Text = outputRes;
}
catch (Exception error)
{
MessageBox.Show(error.Message);
}
} else
{
try
{
//预处理
string[] args = System.Text.RegularExpressions.Regex.Split(textBox3.Text, @"\s+");
CommandParser parser = new CommandParser(args);
ParseRes parseRes = parser.getParseRes();
WordListMaker maker = new WordListMaker();
string article = textBox1.Text;
List<string> wordList = maker.makeWordList(article);
List<string> result = new List<string>();
//核心计算逻辑
if (parseRes.cmdChars.Contains('n'))
{
Chain.gen_chains_all(wordList, 0, result);
}
else if (parseRes.cmdChars.Contains('w'))
{
Chain.gen_chain_word(wordList, 0, result,
parseRes.start, parseRes.end, parseRes.enableLoop);
}
else if (parseRes.cmdChars.Contains('m'))
{
Chain.gen_chain_word_unique(wordList, 0, result);
}
else
{
Chain.gen_chain_char(wordList, 0, result,
parseRes.start, parseRes.end, parseRes.enableLoop);
}
//输出逻辑
Output output = new Output();
string outputRes = output.printWordChains(result, 1);
outputRes = outputRes.Replace("\n", "\r\n");
textBox2.Text = outputRes;
}
catch (Exception error)
{
MessageBox.Show(error.Message);
}
}
}
功能介绍
导入文件

输入命令进行分析
选择命令参数按钮输入命令

必要的输入提示
输入 -h/-t
后会提示再输入一个字母

清空参数
分析结果并给出必要异常提示
正常的输出

遇到异常

导出分析结果到文件

Part3 总结
12.结对经历
3.22 宿舍 初步讨论
进行第一次大致读题和进度安排,用时10min,主要是大概沟通一下和理解基本要求。同时明确了当天晚上需要好好学习 c#
相关的知识以及自己动手写一些demo,做好开发前必要准备工作。
3.23 主M楼 计划与初步开发

深入读题,明确了代码规范和开发计划,完成了设计文档,同时进行了第一次结对实践,完成了输入类和一个简陋的dfs算法。
计划好之后两天自己独自开发的内容。
3.25 新主楼 核心逻辑开发1

zcx重构了算法,使用拓扑排序等方法进行优化,完成了一个输出函数。
fzc写好了GUI以及实现了参数解析,定义了输入输出的一些异常
3.27 新主楼 核心逻辑开发2
(此次忘记拍照了)
zcx继续完善算法逻辑
fzc写完善输入输出,设计单元测试
3.29 主M楼 核心逻辑开发3

修复了解析参数的一些bug,讨论接口实现
zcx发现了之前算法的一些问题,进行重新构建
4.2 主M楼 GUI、CLI与计算模块对接,完善异常设计

基本完成了所有模块的编写,进行测试,以及命令行主程序,GUI界面和计算模块的对接
fzc进行整体文件架构的调整,zcx继续完成接口
4.3-4.5 测试各个接口,模块松耦合
这两天由于各种原因,线上一起进行。
和交换的小组进行互换模块,交流并互测。
编写文档,测试以及做最后的签入
13.结对体会以及自我评价
结对编程的优点:
- 写代码出了错很多时候能被立刻发现。
- 遇到不会用的 API 可以有伙伴提醒或者帮查。
- 没有思路时能马上交流,从而获得思路。
缺点:
- 对不习惯被看着写代码的同学来说有点难以发挥出正常水平。
- 领航员有时候看着有点无聊。
我的优点:
- 平时有写算法题的习惯,所以对本次作业涉及到的算法比较熟悉,开发和调试计算模块的 bug 比较快。
- 人肉 checkstyle,习惯手动维护代码风格。
- 热衷于多人运动(指对拍)。
我的缺点:
- 配环境弱鸡,工具弱鸡,不喜欢接受先进的开发工具和新语言,写代码只会用裸的
VSCode
或者GVim
,Visual Studio
对我来说唯一的用处是命令行编译C#
代码。 - 代码写爽时容易不写注释。
队友的优点:
- 对新技术和新工具有热情,学习快速。
- 善于写 GUI 和单元测试。
- 能肝,负责。
队友的缺点:
代码风格需要依托工具来规范。
总之我觉得队友非常强力,我们的很多工具和技术上的问题都是队友 fzc 解决的,fzc NB!
来点合照:
14.实际花费时间
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 90 | 90 |
· Estimate | · 估计这个任务需要多少时间 | 90 | 90 |
Development | 开发 | 790 | 915 |
· Analysis | · 需求分析 (包括学习新技术) | 60 | 30 |
· Design Spec | · 生成设计文档 | 30 | 30 |
· Design Review | · 设计复审 (和同事审核设计文档) | 30 | 10 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 5 |
· Design | · 具体设计 | 60 | 60 |
· Coding | · 具体编码 | 360 | 480 |
· Code Review | · 代码复审 | 60 | 120 |
· Test | · 测试(自我测试,修改代码,提交修改) | 180 | 180 |
Reporting | 报告 | 100 | 80 |
· Test Report | · 测试报告 | 60 | 60 |
· Size Measurement | · 计算工作量 | 10 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 10 |
合计 | 980 | 1085 |
15. 附加任务-互换核心模块
信息
与我们互换的组为:
- 杨濡冰 19373263
- 黄炜 19373174
交换过程总结
命令行和 GUI,以及测试程序都对杨濡冰和黄炜组的 Core
进行了使用。
基本的使用方式是先添加项目引用,导入他们提供的 HYCore.dll
,之后调用他们提供的接口 Chain.接口函数
进行计算即可,我们将之前使用 PairTestInterface.接口函数
的部分替换成他们的函数即可。值得注意的是版本比较重要,.net
官方文档中明确提出了只保证大版本号的兼容,即 4.x
是可以互相兼容的,但是 3.x
和 4.x
的支持就很不顺畅,所以我们双方都单独生成了对方能够使用的版本。
我们小组单独生成了 .net 4.7.2
版本的 Core.dll
,具体来说就是新建一个 .net framework 用于创建c#类库
的项目,之后添加现有项。把 Core
的相关文件导入,并直接运行一下就会在 bin
路径下找到 dll
文件。
正常导入之后,我们发现了他们的一些小bug,下图中发现他们有环数据遇到了下标越界的错误

同时在单元测试还发现了在遇到找不到某个结尾的链时会抛异常,这些错误我们都及时反馈并帮助他们完善了设计。
无独有偶,他们也发现了我们的类似bug

在完成bug修复之后,我们都使用对方的 Core.dll
得到了正确的运行结果
正确运行效果

