软工结对作业博客
软工结对作业博客
项目 | 内容 |
---|---|
所属课程 | 2019春季计算机学院软件工程(任健) |
所属作业 | 结对项目-最长单词链 |
课程目标 | 理解软件工程的作用和重要性,提升工程能力,团队协作能力 |
作业目标 | 实战双人结对编程 |
1. GitHub项目地址
https://github.com/sephyli/wordlist_BUAA
2. PSP表格
Personal Software Process Stages | PSP2.1 | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
计划 | Planning | ||
· 估计这个任务需要多少时间 | · Estimate | 5 | 0 |
开发 | Development | ||
· 需求分析 (包括学习新技术) | · Analysis | 80 | 60 |
· 生成设计文档 | · Design Spec | 0 | 0 |
· 设计复审 (和同事审核设计文档) | · Design Review | 0 | 0 |
· 代码规范 (为目前的开发制定合适的规范) | · Coding Standard | 10 | 0 |
· 具体设计 | · Design | 90 | 60 |
· 具体编码 | · Coding | 180 | 120 |
· 代码复审 | · Code Review | 120 | 180 |
· 测试(自我测试,修改代码,提交修改) | · Test | 120 | 180 |
报告 | Reporting | ||
· 测试报告 | · Test Report | 60 | 20 |
· 计算工作量 | · Size Measurement | 0 | 0 |
· 事后总结, 并提出过程改进计划 | · Postmortem & Process Improvement Plan | 0 | 0 |
合计 | 665 | 620 |
3. Information Hiding, Interface Design, Loose Coupling
Information Hiding
在面向对象程序的设计中,有很多的类在实现中会有一些自己独有的属性成员或函数。这样的属性可能和类的功能正确的运行有着千丝万缕的联系。而这样的属性是不应该由外部类进行访问和修改的,属于类的私有属性,即隐藏了类的信息,做到了Information Hiding。
在我们的程序中,我们将一些类在运行过程中的控制变量设为私有并通过函数来访问或修改。从而符合了这一思想。
Interface Design
我们按照作业的要求,设计了int gen_chain_word(char* words[], int len, char* result[], char head, char tail, bool enable_loop)
,与int gen_chain_char(char* words[], int len, char* result[], char head, char tail, bool enable_loop)
两个核心功能的接口并封装为DLL。
Loose coupling
我们在设计时,每个类都的逻辑都仅仅与自己的属性成员相关联。如计算模块,只接受字符串集和模式信息就会进行计算。则设计任何的Input方式都可以与计算模块进行交互。
4. 计算模块接口的设计与实现过程
算法分析
我们在拿到题目后,第一反应是直接使用暴力深搜解决问题。但是看到程序正确性要求中的300s运行时间限制(尽管我们现在也没能弄明白是针对-w
模式,-c
模式,还是两者兼有),我们意识到纯粹使用深搜是不合理的。在对算法进行一定的分析之后,我们意识到,不遍历所有满足条件的单词链,是不可能找出其中满足要求的最长/最多单词链的。也就是说,深度优先搜索,将会是我们必须要使用的算法。
完成这部分分析之后,我们将优化的视角投向了数据结构部分。我们注意到,程序要求单词链满足如下条件。
单词链的定义为:由至少2个单词组成,前一单词的尾字母为后一单词的首字母,且不存在重复单词
这就意味着,我们在从文件中读取单词的时候就该避免重复单词的读入。不仅如此,我们还设计了独到的数据结构来放置单词,从而实现了访查效率的最大化。
举例来说,我们在WordSet
类中,设置了26*26的二位vector
来放置头字母相同、尾字母相同的单词,并在vector
中按照单词的长度来排列,这样的话,不管对于-w
模式还是-c
模式,都可以采取同一套访查算法。不仅如此,如此组织数据结构,可以让我们的核心搜索函数(深搜函数)快速通过找到以目标字母开头的单词,相比传统的深度优先搜索,我们在这一步的复杂度从O(N)
降到了O(1)
。考虑到深搜核心函数的调用次数是一个随着单词链长度增长而成阶乘级增长的,我们的优化方法,应该来说也是有一定作用的。
类关键信息
- Word类
- 属性
char head
: 头字母char tail
:尾字母int length
:单词长度std::string s
:单词bool use
:标志是否被使用
- 方法
Word(const char* s, int length)
:构造函数
- 属性
- WordSet类
- 属性
std::vector<Word> set[26][26]
:单词组
- 方法
void append(Word w)
:添加单词
- 属性
- Mode类
- 属性
bool recurMode
:单词成环模式bool headMode
:头字母指定模式bool tailMode
:尾字母指定模式bool wordNumMaxMode
:单词数量最大模式bool charNumMaxMode
:单词字母最多模式char head
:指定的头字母char tail
:指定的尾字母
- 方法
void append(Word w)
:添加单词
- 属性
- Searcher类
- 属性
Data data
:数据信息Mode mode
:模式信息std::vector<Word> maxWordList
:历史max单词liststd::vector<Word> tmpWordList
:当前max单词list
- 方法
bool exe()
:执行函数
- 属性
5. 计算模块的UML图
6. 改进计算模块
在构造数据结构时,我将问题的思维模式转变为一种带权重的有向图。如hello,即结点h到结点o的有向边,若-w模式,权重为1,-c则为5。那么我存下了每个节点到另一个节点的边的信息并以节点号进行索引,构造出了vector<Word> WordSet[26][26]
这样的结构,每个vector
内用权重进行排序。
由此问题便转变为了不允许重复节点和路径的最长路径问题。考虑过DP,但无法构造高效的子问题,及子问题合并时需要判断重复的路径和节点,猜测并不会提升效率。由此使用了深度优先搜索,在非-r模式,的复杂度约为26!这个数量级,因此构造出了最复杂的样例后,是绝无可能在300s之内完成计算的。由此放弃了在总体算法层次上的优化,仅仅追求最短的搜索路径和较小的访存开销。
结果: 算法在-r模式下的100个词中,可以在20ms内完成单词链搜索。
7. Design by Contract, Code Contract
该模式的核心在于一个类应拥有的不变式,以及每个成员函数在运行前后需要保证不变式为真。
我们在实现Word
类时,保证了head
, tail
, length
等属性与单词本身相匹配。
在实现WordSe
t类时,保证了每个vector
内的Word
都是从长到短排序,从而可以便捷的找到第一个未被使用的最长单词。
8.计算模块部分异常处理说明
单词超长
- 用途:检测输入文本中含有超过长度限制的单词的情况。
- 预期结果:通过try-catch的方法,捕获命令行报错信息。其中,
./test/testfile.txt
文件中包含一个连续长度超过100的字母串。 - 单元测试如下:
TEST_METHOD(TestTooLongWord)
{
FILE *fin;
fopen_s(&fin, "../test/testfile.txt", "r");
char *words[10000], *result[105];
Inputer *inputer = new Inputer();
int wordNum = inputer->getWord(fin, words);
for (int i = 0; i < 105; i++) {
result[i] = new char(100);
}
int len = 0;
len = Core::gen_chain_word(words, wordNum, result, 0, 0, false);
}
隐含单词环
- 用途:检测输入文本中隐含单词环的情况。
- 预期结果:通过try-catch的方法,捕获命令行报错信息。其中,
./test/testfile.txt
文件中包含若干个可以构成单词环的单词。 - 单元测试如下:
TEST_METHOD(TestNonRecureFalse)
{
FILE *fin;
fopen_s(&fin, "../test/testfile.txt", "r");
char *words[10000], *result[105];
Inputer *inputer = new Inputer();
int wordNum = inputer->getWord(fin, words);
for (int i = 0; i < 105; i++) {
result[i] = new char(100);
}
int len = 0;
len = Core::gen_chain_word(words, wordNum, result, 0, 0, false);
}
隐含单词环
- 用途:检测输入文本中隐含单词环的情况。
- 预期结果:通过try-catch的方法,捕获命令行报错信息。其中,
./test/testfile.txt
文件中包含若干个可以构成单词环的单词。 - 单元测试如下:
TEST_METHOD(TestNonRecureFalse)
{
FILE *fin;
fopen_s(&fin, "../test/testfile.txt", "r");
char *words[10000], *result[105];
Inputer *inputer = new Inputer();
int wordNum = inputer->getWord(fin, words);
for (int i = 0; i < 105; i++) {
result[i] = new char(100);
}
int len = 0;
len = Core::gen_chain_word(words, wordNum, result, 0, 0, false);
}
单词链长度过短
- 用途:检测无法找到长度超过1的单词链的情况。
- 预期结果:通过try-catch的方法,捕获命令行报错信息。其中,
./test/testfile.txt
文件中仅包含一个单词,或所包含的单词无法形成长度超过1的单词链。 - 单元测试如下:
TEST_METHOD(TestNonRecureFalse)
{
FILE *fin;
fopen_s(&fin, "../test/testfile.txt", "r");
char *words[10000], *result[105];
Inputer *inputer = new Inputer();
int wordNum = inputer->getWord(fin, words);
for (int i = 0; i < 105; i++) {
result[i] = new char(100);
}
int len = 0;
len = Core::gen_chain_word(words, wordNum, result, 0, 0, false);
}
命令行参数无法正确解析
- 用途:检测命令行参数无法正确解析的情况。
- 预期结果:通过try-catch的方法,捕获命令行报错信息
文件读取错误
- 用途:检测无法找到文件的情况。
- 预期结果:通过try-catch的方法,捕获命令行报错信息。其中,
./test/testfile.txt
文件不存在。 - 单元测试如下:
TEST_METHOD(TestNonRecureFalse)
{
FILE *fin;
fopen_s(&fin, "../test/testfile.txt", "r");
char *words[10000], *result[105];
Inputer *inputer = new Inputer();
int wordNum = inputer->getWord(fin, words);
for (int i = 0; i < 105; i++) {
result[i] = new char(100);
}
int len = 0;
len = Core::gen_chain_word(words, wordNum, result, 0, 0, false);
}
10. 命令行模块描述
参数约定
程序支持通过命令行的方式输入参数以及文件位置信息。参数及其约定如下。
参数名字 | 参数意义 | 范围限制 | 用法示例 |
---|---|---|---|
-w |
需要求出单词数量最多的单词链 | 绝对或相对路径 | 示例:Wordlist.exe -w input.txt [表示从input.txt 中读取单词文本,计算单词数量最多的单词链] |
-c |
需要求出字母数量最多的单词链 | 绝对或相对路径 | 示例:Wordlist.exe -c input.txt [表示从input.txt 中读取单词文本,计算字母数量最多的单词链] |
-h |
指定单词链首字母 | a-z ,A-Z |
示例:Wordlist.exe -h a -w input.txt [表示从input.txt 中读取单词文本,计算满足首字母为a 的、单词数量最多的单词链] |
-t |
指定单词链尾字母 | a-z ,A-Z |
示例:Wordlist.exe -t a -c input.txt [表示从input.txt 中读取单词文本,计算满足尾字母为a 的、字母数量最多的单词链] |
-r |
允许单词文本中隐含单词环 | NONE |
示例:Wordlist.exe -r -w input.txt [表示从input.txt 中读取单词文本,计算单词数量最多的单词链,即使单词文本中隐含单词环也需要求解] |
实现过程
程序将命令行参数的提取部分放在了main函数中,通过引用int main(int agrc, char* agrv[])
函数的方式,将命令行参数的数量读取在agrc
中,分词读取在agrv[]
中。
通过判断分词是否为-
开头,来判断该词是否代表着命令行参数,在通过该词的第二个字母,判断具体属于哪个命令行参数(支持大小写)或者报错。同时通过判断第三个字母是否为\0
来判断参数是否过长。如果为参数有后续的范围限制,如-h
、-t
后需要指定开头、结尾的字母,则继续判断字母是否符合要求(个数为一个且在有意义)。
全部判断完成后,将判断的结果传入构造的Mode
对象中,为后续程序所使用。
具体来看,程序声明了如下变量,在对不同参数做解析的时候,就针对不同情况对变量进行赋值,这样就收集全部的命令行参数信息。
bool rMode = false;
bool hMode = false;
bool tMode = false;
bool wMaxMode = false;
bool cMaxMode = false;
char h = 0;
char t = 0;
char filePath[1000] = "\0";
再通过对Mode
对象的赋值,从而实现了命令行模式的判断。
Mode *mode = new Mode();
mode->Set(rMode, hMode, tMode, wMaxMode, cMaxMode, h, t);
12. 结队编程过程
13. 结对编程的优缺点
缺点:双方较难统一时间,在开发进程中会有所延误。
双人一起进行编程,虽然相对于一个人效率有所提升,但分开编程可能会提供更多的生产力。即使减去两人的沟通成本。
优点:在设计时同步进行Code Review,大大减少了Bug的出现概率。往往一人在Coding的同时,另一人就会在旁边直接提醒代码出现的问题,提升了单人编程的效率。
集两人的想法于一身,在思维的碰撞中产生更好的算法和更精妙的数据结构。
在开发时减少放空,有领航员来督促推进进度。有时在Coding时因为想不起来某个类的一个方法,突然就停住了,这时候如果被其他事情打断,就会去做别的事儿。结对编程时,就会有伙伴进行直接提醒,减少了类似情况的发生。