《软件工程实践》第二次作业-个人项目实战

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判断答案。我似乎是没有深刻理解单元检测之“单元”的精髓。我事后想,应该是需要分别检测每个
    函数的;但是我将所有的函数都放在一个单元里检测了;这样的坏处是:一个函数出错,其后的函数将无法检测。
    不过我当前这个集所有函数为一体的单元检测还是比较全面了,我有做到将运行结果输出到文件后,再重新读入
    并与预设答案比较。它对于我设计的样例检测达到预期。

    test

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。
test

5.测试数据:思路:题目提供了几种符号类型,随即我们需要考虑的就是它们的任意组合,并从中挑选出满足条件的
成为单词。我拟了几种需要考虑情况:

1.空文件

2.空行

3.文件末尾有换行与无换行

4.分隔符分割单词

5.单词不区分字母大小写

6.长度小于四的准单词

7.字母与数字的组合,包括abcd123和123abc甚至123abc123abc以及abc123abc123。注意只要数字开头就不是单词

8.频率统计和排序情况,注意少于十种单词甚至没有单词时的输出list

9.编码方式不同的txt文件

以上大致覆盖了我的所有代码。其中除了第九点我没有完成,其余我的程序是可以处理的。我没有分别写出十个样例
(有单独尝试空文件),尽量综合了以上的情况拟了一个输入文件,对应如下:

test

  • 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)
posted @ 2018-09-12 22:56  kofyou  阅读(645)  评论(4编辑  收藏  举报