BUAA软件工程:结对编程——寻找单词链
- 教学班级:周二班
- 项目地址:https://github.com/1aureate/2022-BUAA-SE-pair
项目 | 内容 |
---|---|
这个作业属于哪个课程 | 2022春季软件工程(罗杰 任健) |
这个作业属于哪个课程 | 结对编程项目-最长英语单词链-CSDN社区 |
我在这个课程的目标是 | 了解并提高自己对软件工程的认识和实践能力 |
这个作业在哪个具体方面帮助我实现目标 | 了解了何为结对编程,增加了项目合作开发经验 |
1. 简介
结对编程是指两个人坐在一起,用一台电脑一套键鼠进行编码。一个人写代码,一个人在旁边“领航”。通过结对编程,可以使代码审查变为实时进行,两个人的思考效率也要更高,还防止了自己写代码容易划水摸鱼。
本次与同学结对完成一个寻找单词链的小程序,从简单的命令行程序到单元测试、封装动态库、编写图形界面、处理异常信息、提高单元测试的代码覆盖率,我们体验了一个需求的实现从简单、不可靠,逐渐变得规范化、更有可靠性、更有可拓展性。通过这次实验,我们也体会到了结对编程的好处与缺点,为今后的软件编写积累了宝贵的经验。
2. PSP表
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 40 |
· Estimate | · 估计这个任务需要多少时间 | 60 | 40 |
Development | 开发 | 1510 | 1710 |
· Analysis | · 需求分析 (包括学习新技术) | 80 | 40 |
· Design Spec | · 生成设计文档 | 120 | 240 |
· Design Review | · 设计复审 (和同事审核设计文档) | 40 | 60 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 10 |
· Design | · 具体设计 | 100 | 120 |
· Coding | · 具体编码 | 600 | 700 |
· Code Review | · 代码复审 | 200 | 180 |
· Test | · 测试(自我测试,修改代码,提交修改) | 360 | 360 |
Reporting | 报告 | 590 | 520 |
· Test Report | · 测试报告 | 210 | 180 |
· Size Measurement | · 计算工作量 | 60 | 40 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 320 | 300 |
合计 | 2160 | 2270 |
3. 接口设计方法
我们将程序的各个部分分成了不同的类,并尽量减少类之间的依赖性,使他们尽量”通过数据交互“,这样只要保证某一阶段的输出格式不变,内部如何实现可以随意改动。
这种方法保证了不同类之间是相互独立的、不透明的,耦合度较低。
4. 接口设计与实现
不同阶段对源代码结构进行了较大的改动。
第一阶段中,我们的主题任务是实现命令行程序,因此将所有的类都放在命令行项目中组织编写。第一阶段的末尾另建了一个dll项目与gui项目,dll通过调用命令行项目的源代码生成动态库。
第二阶段时,我们发现命令行程序也应该调用动态库,而不应该直接使用源代码,因此我们将主体代码转移到了dll项目中,并将命令行程序修改为调用动态库的版本。
在这里,我仅说明最终的项目中的主要类。不同项目中的类互相不影响。
在dll项目中,主要有:
类名 | 作用 |
---|---|
ParamHandler |
保存从接口传入的参数信息,在后续计算中获取参数信息,如是否允许隐藏链 |
WordListHandler |
根据参数和已经得到的单词列表,找出相应的单词链并返回结果 |
Word |
由head(char), tail(char), contents(vector<string>) 组成,将有相同头尾的单词组合到一起,减小计算量。由于单词仅有小写字母,因此最多只有\(26\times26=676\)个结点。 |
在命令行项目中,主要有:
类名 | 作用 |
---|---|
ParamHandler |
处理从命令行读入的参数,包括异常处理 |
InputsHandler |
根据ParamHandler 得到的文件名,打开文件并从中提取单词列表 |
OutputsHandler |
将动态库的返回结果打印到标准输出或者文件中 |
ErrorCodeHandler |
将动态库返回的错误码对应到具体的错误信息,其实就是个map<int, string> |
类的数量较少,交互也非常简单,再次就不详细赘述了。
算法的核心部分在WordListHandler
中,具体的算法流程如下图所示:
5. UML图
6. 性能改进
只能想到用DFS计算,没想出什么好的性能改进方法。
性能分析图如下:
可以看到,WordListHandler部分(也就是计算单词链)占据了大部分的时间,IO以及提取单词占据的时间不多。
7. Design by Contract, Code Contract
优点:
- 形式严谨,方便测试人员根据Constract设计测试用例
- 逻辑清晰,减少代码编写过程中产生的小疏忽,如忘记处理空字符串
缺点:
- 繁琐。比如一个方法的作用是“找出最短路“,这一条简单的自然语言需求需要转换成极其复杂的形式语言才能说明。
由于本次开发工作量大,因此没有采取这种对代码约束较强的开发形式。但是,我们有许多口头上的contract
- 类之间尽量不互相引用,通过数据传递信息
- 尽量使用int作为返回值,成功返回0,不成功返回负数。
8. 单元测试展示
结对编程的时间紧迫,我们并没有时间正交开发或者寻找小组对拍,因此仅通过手动构造测试样例的方法对代码进行单元测试。
以对gen_chain_word
函数的测试为例:
TEST_METHOD(testGenChainWord) {
// 将每个测试样例抽象化为一个类,批量处理
class GenChainWordTestCase {
public:
char* words[100];
int len;
char head;
char tail;
bool enable_loop;
int stdChainNum;
GenChainWordTestCase(char* words[], int len, char head, char tail, bool enable_loop, int stdChainNum)
: len(len), head(head), tail(tail), enable_loop(enable_loop), stdChainNum(stdChainNum) {
for (int i = 0; i < len; i++) {
this->words[i] = words[i];
}
}
};
std::vector<GenChainWordTestCase> testCases;
char* words1[] = { "woo", "oom", "moon", "noox" };
testCases.push_back(GenChainWordTestCase(words1, 4, 0, 0, false, 4));
testCases.push_back(GenChainWordTestCase(words1, 4, 'w', 0, false, 4));
testCases.push_back(GenChainWordTestCase(words1, 4, 'o', 0, false, 3));
testCases.push_back(GenChainWordTestCase(words1, 4, 'n', 0, false, 0));
testCases.push_back(GenChainWordTestCase(words1, 4, 0, 'x', false, 4));
testCases.push_back(GenChainWordTestCase(words1, 4, 0, 'n', false, 3));
testCases.push_back(GenChainWordTestCase(words1, 4, 0, 'o', false, 0));
char* words2[] = { "Algebra", "Apple", "Zoo", "Elephant", "Elephant", "Under",
"Fox", "Dog", "Moon", "Leaf", "Trick", "Pseudopseudohypoparathyroidism" };
testCases.push_back(GenChainWordTestCase(words2, 12, 0, 0, false, 4));
char* words3[] = { "woo" };
testCases.push_back(GenChainWordTestCase(words3, 1, 0, 0, false, 0));
char* words4[1] = {};
testCases.push_back(GenChainWordTestCase(words4, 0, 0, 0, false, 0));
char* words5[] = { "woo", "oom", "moow" };
testCases.push_back(GenChainWordTestCase(words5, 3, 0, 0, false, -3));
testCases.push_back(GenChainWordTestCase(words5, 3, 0, 0, true, 3));
testCases.push_back(GenChainWordTestCase(words5, 3, 'w', 0, true, 3));
testCases.push_back(GenChainWordTestCase(words5, 3, 'o', 0, true, 3));
testCases.push_back(GenChainWordTestCase(words5, 3, 'm', 0, true, 3));
testCases.push_back(GenChainWordTestCase(words5, 3, 0, 'o', true, 3));
testCases.push_back(GenChainWordTestCase(words5, 3, 0, 'm', true, 3));
testCases.push_back(GenChainWordTestCase(words5, 3, 0, 'w', true, 3));
testCases.push_back(GenChainWordTestCase(words5, 3, 1, 'w', false, -4));
testCases.push_back(GenChainWordTestCase(words5, 3, 0, 2, false, -4));
char* words6[] = { "woo", "o", "oon" };
testCases.push_back(GenChainWordTestCase(words6, 3, 0, 0, false, -1));
char* words7[] = { "woo", "123", "oon" };
testCases.push_back(GenChainWordTestCase(words7, 3, 0, 0, false, -1));
auto result = new char* [10000];
// 仅比较结果的大小,比较返回结果比较困难。
for (auto& testCase : testCases) {
int chainNum = gen_chain_word(testCase.words, testCase.len, result,
testCase.head, testCase.tail, testCase.enable_loop);
Assert::AreEqual(chainNum, testCase.stdChainNum);
}
}
构造思路:
先构造正常数据:输入数据是正常的,简单的规定一下-h, -t
参数,看看程序是否能跑出正确结果
再构造不那么正常的数据:只有一个单词、没有单词。
再构造会抛出异常的数据:非法字符、存在单词链、参数异常。
单元覆盖率截图:
使用Visual Studio 2019企业版自带的覆盖率测试工具进行测试,覆盖率如下:
有一些点比较零碎,因此没有被覆盖到。比如返回结果超过20000个、文件不存在等等。整体的覆盖率达到93%以上。
9. 异常处理说明
动态库:
每个对外暴露的函数都会catch各种抛出的异常,并根据错误代码表返回不同的错误代码。
0: Everything is alright;
-1: Illegal word exists.
-2: More than 20000 results.
-3: EnableLoop is false, but loop is found.
-4: Illegal head or tail letter.
others: Unexpected exception occured, please contact the developer.
具体的:
-
-1:words中存在非法单词
-
测试:
-
char* words6[] = { "woo", "o", "oon" }; testCases.push_back(GenChainWordTestCase(words6, 3, 0, 0, false, -1)); char* words7[] = { "woo", "123", "oon" }; testCases.push_back(GenChainWordTestCase(words7, 3, 0, 0, false, -1));
-
-
-2:超过20000个结果
- 这一条并没有构建测试用例
-
-3:没有允许隐藏环的情况下出现环
-
// false代表不允许环,true是允许环 char* words5[] = { "woo", "oom", "moow" }; testCases.push_back(GenChainWordTestCase(words5, 3, 0, 0, false, -3)); testCases.push_back(GenChainWordTestCase(words5, 3, 0, 0, true, 3));
-
-
-4:
head或tail
不是小写字母-
testCases.push_back(GenChainWordTestCase(words5, 3, '*', '@', false, -4)); testCases.push_back(GenChainWordTestCase(words5, 3, 0, '$', false, -4));
-
命令行应用:
命令行应用是直接调用动态库的,并没有给命令行单独进行单元测试,无法提供测试样例。
- 输入参数异常:
- 没有参数
- 出现不存在的参数
-h, -t
后没有紧跟一个字母- 没有输入文件名
- ......
- 输入数据异常:
- 打不开文件
- 文件名不以“.txt”结尾
- 在没有
-r
参数的情况下出现单词环
- 动态库给出的异常
当命令行应用发现异常,会以错误信息的形式输出到窗口中,并终止程序。
10. 界面模块设计过程
使用pyqt简单的设计了一个界面,实现了课程组的基本要求。
首先,用QTDesigner实现界面元素的设计
然后,用pyui将ui文件转换成py文件
最后,手动给py文件中的类添加逻辑关系。
11. 界面模块与计算模块对接
通过动态库与计算模块进行对接。由于我们没有找到使用python传递二维数组的方法,因此为gui单独设计了一套接口,叫xxx_python
extern"C" __declspec(dllexport) int gen_chains_all_python(char* words, char** result, char** error_msg);
extern"C" __declspec(dllexport) int gen_chain_word_python(char* words, char** result, char head, char tail, bool enable_loop, char** error_msg);
extern"C" __declspec(dllexport) int gen_chain_word_unique_python(char* words, char** result, char** error_msg);
extern"C" __declspec(dllexport) int gen_chain_char_python(char* words, char** result, char head, char tail, bool enable_loop, char** error_msg);
效果展示
正常运行 | |
---|---|
出现异常 |
12. 结对的过程
第一次尝试两个人坐在一起写一份代码这种形式,刚开始两人的热情都很高涨,用一个下午初步确定了如何分析、保存命令行参数并供给后续方法调用。
然而到了后期,两个人的时间渐渐无法统一,结对的机会越来越少,项目变成了实际上的“二人合作开发”。
两人结对编程时的照片:
13. 优缺点
结对编程的优缺点:
优点 | 缺点 |
---|---|
不宜走神,减少摸鱼 | 对于简单的模块,并不需要两个大脑,降低了效率 |
两个人的思路互相结合,形成1 + 1 > 2的效果,加快了项目的推进 | 两个人的时间很难统一 |
代码审查实时进行,减少了代码出错的可能性 | 对于一些“非理性”的问题,比如添加一种设计麻烦/不麻烦,两个人很难形成共同意见 |
结对成员的优缺点:
李浩宇 | 徐家乐 |
---|---|
优:面向对象思想十分先进 | 优:十分喜欢面向过程编程 |
优:十分讨厌c++语言 | 优:比较喜欢c++语言 |
优:喜欢良好的设计 | 缺:做事比较急功近利 |
缺:容易陷入个性的设计 | 优:python gui 开发较快 |
优:单元测试能力 very good | 优:会不断督促项目进度 |
缺:懒惰成性,不爱干活 | 缺:菜,算法纯纯白给 |