《软件工程实践》第二次作业-个人项目实战
1.在文章开头给出Github项目地址。
https://github.com/zhoujingping/PersonProject-C.git
2.给出PSP表格。
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
Planning | 计划 | ||
• Estimate | • 估计这个任务需要多少时间 | 460 | 1070 |
Development | 开发 | ||
• Analysis | • 需求分析 (包括学习新技术) | 30 | 120 |
• Design Spec | • 生成设计文档 | 30 | 15 |
• Design Review | • 设计复审 | 20 | 5 |
• Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 0 | 0 |
• Design | • 具体设计 | 30 | 40 |
• Coding | • 具体编码 | 120 | 170 |
• Code Review | • 代码复审 | 30 | 10 |
• Test | • 测试(自我测试,修改代码,提交修改) | 90 | 620 |
Reporting | 报告 | ||
• Test Repor | • 测试报告 | 60 | 30 |
• Size Measurement | • 计算工作量 | 20 | 20 |
• Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 30 | 40 |
合计 | 470 | 1070 | |
代码规范为零是因为只有我一人编码,就沿用了我一直的方式。
其中有些部分真正的含义可能与我的理解有些出入;我没有实际上写出设计文档。
对于这个表格,很惭愧,心里真是没有一点数!预估非常不准。
我尤其低估了debug的耗时,又因为我自己行为的不规范,平添诸多烦恼。还有单元测试也消耗了大量的时间。
3.解题思路描述。即刚开始拿到题目后,如何思考,如何找资料的过程。
看到题目得到思路,很普通:一行一行读入文件,对每一行进行扫描,一遍扫描结束可以:知道是否为空行、
有多少字符、分离本行的单词。遍历一遍时间复杂度也不大。因此其实现不需要特别的知识。但后来考虑到接口封装,
于是将字符统计与单词分离分开,总共遍历文本两遍。这样带来了一些时间消耗,但代码结构变得清晰,便于修改找错
和理解。找资料主要在之后写单元测试时,以及在写代码遇到问题时上网求解。单元测试在网上找到了教程,
对此有了一定理解,依葫芦画瓢成功完成;我遇到的所有问题,通过自行处理结合从他人那里得来的经验,全部解决了。
4.设计实现过程。设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图?单元测试是怎么设计的?
1.代码组织:
-
设计了一个counter类
-
私有数据成员int char_num,int line_num,int word_num,vector dic;
四个私有成员用于计数,一个容器类似字典,存放单词及词频;
-
函数countCharLine,countWord,frequency,print
"countCharLine( )" 用于计算字符数和有效行。
为这个函数设计了countPerLine()方法,一行一行遍历整个文件,对于每行,判别每个字符并计数,同时判断空行;"countWord( )" 用于初始化dic容器(生成字典)。
为这个函数设计了splitPerLine()方法,一行一行遍历文件,对于每行,分割单词统计词频;"frequency( )" 用于排序字典,使用了快排,
自定义了cmp排序方式。因此frequency()必须先countWord()后使用,即生成字典后再排序;
2.异常处理:
对输入输出文件无法打开的情况有输出提示。目标是杜绝读入都没成功下的debug!场景可以是文件权限、应该采用绝对路径
时采用了相对路径。
```
if (inFile.fail())cout << "fail to open input file\n";
.
.
.
if (outFile.fail())cout << "fail to open output file\n";
```
3.单元测试:
TEST_METHOD(TestMethod1)
{
//init answer
aChar = 165;
aWord = 18;
aLine = 10;
aFrequency = pair<string, int>("file", 9);
//init result
rChar = 0;
rWord = 0;
rLine = 0;
rFrequency = pair<string, int>("", 0);
//init file name
char inFilename[] = "input.txt";
char outFilename[] = "result.txt";
//running
testCounter->initInFilename(inFilename);
testCounter->countCharLine();
testCounter->countWord();
testCounter->frequency();
testCounter->print(outFilename);
//result read in
ifstream checkFile(outFilename, ios::in);
if(checkFile.fail()) Logger::WriteMessage("faile to open check file.\n");
string temp;
checkFile >> temp >> rChar;
if (rChar != aChar)
Logger::WriteMessage(temp.c_str());
checkFile >> temp >> rWord;
if (rWord != aWord)
Logger::WriteMessage(temp.c_str());
checkFile >> temp >> rLine;
if (rLine != aLine)
Logger::WriteMessage(temp.c_str());
if (aWord != 0 && rWord != 0)
{
checkFile >> temp;
Logger::WriteMessage(temp.c_str());
rFrequency.first = temp.substr(1, (int)temp.length() - 3);
Logger::WriteMessage(rFrequency.first.c_str());
checkFile >> rFrequency.second;
}
checkFile.close();
//check
Assert::AreEqual(aChar, rChar);
Assert::AreEqual(aWord, rWord);
Assert::AreEqual(aLine, rLine);
if (aWord != 0 && rWord != 0)
{
Assert::AreEqual(aFrequency.first, rFrequency.first);
Assert::AreEqual(aFrequency.second, rFrequency.second);
}
}
-
单元测试的设计思路其实是模拟了整个程序的运行,即读入--数字符与行--数单词(生成字典)--排序--输出,
并借用assert判断答案。我似乎是没有深刻理解单元检测之“单元”的精髓。我事后想,应该是需要分别检测每个
函数的;但是我将所有的函数都放在一个单元里检测了;这样的坏处是:一个函数出错,其后的函数将无法检测。
不过我当前这个集所有函数为一体的单元检测还是比较全面了,我有做到将运行结果输出到文件后,再重新读入
并与预设答案比较。它对于我设计的样例检测达到预期。
4.异常处理:
单元检测中也设置了一些出错处理
if(checkFile.fail()) Logger::WriteMessage("faile to open check file.\n");
if (rChar != aChar)
Logger::WriteMessage(temp.c_str());
...
if (rWord != aWord)
Logger::WriteMessage(temp.c_str());
...
if (rLine != aLine)
Logger::WriteMessage(temp.c_str());
当预设与计算的答案不符时输出提示。方便debug。
5.测试数据:思路:题目提供了几种符号类型,随即我们需要考虑的就是它们的任意组合,并从中挑选出满足条件的
成为单词。我拟了几种需要考虑情况:
1.空文件
2.空行
3.文件末尾有换行与无换行
4.分隔符分割单词
5.单词不区分字母大小写
6.长度小于四的准单词
7.字母与数字的组合,包括abcd123和123abc甚至123abc123abc以及abc123abc123。注意只要数字开头就不是单词
8.频率统计和排序情况,注意少于十种单词甚至没有单词时的输出list
9.编码方式不同的txt文件
以上大致覆盖了我的所有代码。其中除了第九点我没有完成,其余我的程序是可以处理的。我没有分别写出十个样例
(有单独尝试空文件),尽量综合了以上的情况拟了一个输入文件,对应如下:
- visual studio 2017 community似乎没有代码覆盖率支持。
5.记录在改进程序性能上所花费的时间,描述你改进的思路。
至此只完成了基本工作,没有多余时间完成性能改进。但我在过程中也发现了问题,至少是找到了优化目标。
首先我将单词及其频率存储在map中。这是一读题就做出的决定,因为考虑到map可以直接通过key值访问索引,
方便我对词频计数;然而在后期发现,在对单词词频自增前,需要查询其存在,这就必须要搜索。既然都搜
索了,想必也找到了其索引,因此“通过key值访问value”的操作显得不必要了。并且,题目要求先对词频排序
再对key值排序,而map不便于实现。为此我将map拷贝到vector<pair<string,int> >中。既然总是要使用到
vector,而map有没有体现其优势,不如一开始就使用vector。我这样使用map,浪费了空间,拷贝到vector
浪费了时间。(这么简单的问题为什么不改!!因为时间紧迫,生怕动一下就坏了,已经很怕了。)
其次是在考虑中文字符时发现的。如果需要考虑中文字符,(我觉得)就需要考虑其编码方式。但是我看了一
段时间没有理解故放弃,所幸现在输入不存在中文字符。但是!在考虑汉字的编码问题时我将txt文档的编码
方式修改为ANSI之外的其他,结果忘记改回来,后来在运行时出错。修改回ANSI之后ok。所以我的结论是,
在我当前的判断字符方式下(使用ctype.h的函数)就算只有英文数字英文标点等,也是需要考虑编码方式的。
那就需要我判断一个txt文档的编码方式,并将其转为ANSI。但是这又是一摞崭新的知识啊!时间紧迫,暂缓
了。不知道将来的样例会不会涉及编码方式,但是为了程序的兼容性我应该急切地修改它。
6.代码说明。展示出项目关键代码,并解释思路与注释说明。
- 我的代码关键是两个,一个用于行计数字符及判断空行;一个用于分离一行中的单词并记录频率,展示如下:
void counter::countPerLine(string line)
{
int i = 0;
// 预处理空格以及空行的情况
while (i < line.length() && isspace(line[i]))
{
char_num++;
i++;
}
if (i != line.length())
{
line_num++;
//简单循环计数
while (i < line.length())
{
if (line[i] > 0)
char_num++;
i++;
}
}
}
思路:先预处理空格,此时可以处理空行情况;之后遍历统计。特别在于
-
①利用isspace()综合考虑所有空白符。
-
②综合输入流的eof信息为每一行加上'\n'便于统计,同时也区分了"hahaha\n"和"hahaha"。
void counter::splitPerLine(string line)
{
int i = 0;
while (i < line.length())
{
// handle characters before a word
while (i < line.length() && !isalpha(line[i]))
{
if (isdigit(line[i])) // handle 123file
{
while (isalnum(line[i]) && i < line.length())
{
i++;
}
}
i++;
}
// handle a word
string tempWord;
while (i < line.length())
{
if (isalpha(line[i]))
{
tempWord += tolower(line[i]);
}
if (isdigit(line[i]))
{
if (tempWord.length() < 4)
{
i++;
break;
}
else
{
tempWord += line[i];
}
}
if (!isalnum(line[i]) || i == line.length() - 1)
{
if (tempWord.length() >= 4)
{
map<string, int>::iterator iter;
iter = dic.find(tempWord);
if (iter != dic.end())
iter->second++;
else
dic.insert(pair<string, int>(tempWord, 1));
}
i++;
break;
}
i++;
}
}
}
思路:对于每一行,
- ①循环直到遇见第一个字母;
- ②遇见字母后,每一个字母加入暂存单词中;若遇到数字,如果长度大于四可以将数字加入单词,否则break;
若遇到分隔符或者行尾,若长度大于四将单词加入字典;
- ③如果空格符之后是数字开头,则一直舍弃直到遇到分隔符。
特别之处在于巧妙利用了行的处理流程,同时却也显得太基于过程。
实际上,是在debug过程中衍生出了上面代码段的一些分支;原先或许还比较简洁,而后愈发繁琐,旁人是难以理解的。
这一方面锻炼我考虑问题的全面性,另一方面敦促我寻找结构上更加简洁且普适的方法。
**7.结合在构建之法中学习到的相关内容,撰写解决项目的心路历程与收获。**
首先我有惨痛经历。我错在:看完输入输出后立马动手写程序。我应该做的是:做一个有计划的人,去通读
全文,仔细设计结构,充分考虑要求。我的得到的惩罚是:在封装接口之后又经历了一段时间的debug。
而且!!我一开始还不是用vs写的!!移植到vs后又又经历了一段时间的debug!我以后一定做一个规范的人。
也有我想要表扬自己的地方。首先我认为我考虑问题比较全面,尤其是我想到了字符编码的问题,以及我考虑
到了txt文末“hahaha\n”和“hahaha”在字符统计上的区别(因为getline会自动舍去换行,我觉得还是挺难想的)。
其次我认为我自行解决问题的能力尤其强,碰到了想要多从所未见的问题,都可以在网络的帮助下解决。同时
我也更加认为报错是个好东西,往往指出了问题所在。比如:在单元测试时,这么陌生的东西一debug起来
我真的很怕,结果真的不行。觉得无路可走!但是后来我仔细看了报错,提示no file found并给出了路径,
我由此发现单元测试时,取txt输入文件的地方和普通运行时的地方不一样!在相应的地方加入输入文件后ok!
再其次我自学以及理解能力真的好强哦,完全陌生的单元测试我都可以写!厉害!
此外我还有需要改进的地方,比如时间安排(这点真的很重要),我不可以再将作业延后到这么晚。以及上文中已经提到的部分。
在最后我也有对老师和助教布置任务时的建议,我希望要求可以给得更明晰一些,尤其是输出要求和统计要求,
有的部分其实有些模糊。其实给一个样例就能很好地帮助我们理解啦!
<br><br>
参考链接
[Visual Studio(VS)C++单元测试](https://www.cnblogs.com/techiel/p/7954142.html)
[使用 Visual Studio 2015 对 C++ 代码运行单元测试](https://blog.csdn.net/lxf200000/article/details/51100094)
[vs2015单元测试总结——3种方法可用](https://blog.csdn.net/u013299585/article/details/73662526)