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


    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
    ";
    .
    .
    .
    if (outFile.fail())cout << "fail to open output file
    ";
    ```
    

    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.
    ");
    		
    			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.
    ");
    
    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信息为每一行加上' '便于统计,同时也区分了"hahaha "和"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
    ”和“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)
  • 相关阅读:
    ZOJ 3327 Friend Number
    ZOJ 3324 Machine
    2010-2011 ACM-ICPC, NEERC, Southern Subregional Contest C Explode 'Em All
    UVA 12594 Naming Babies
    POJ 3709 K-Anonymous Sequence
    POJ 1180 Batch Scheduling
    POJ 1160 Post Office
    HDU 3516 Tree Construction
    HDU 3045 Picnic Cows
    UVALive 5097 Cross the Wall
  • 原文地址:https://www.cnblogs.com/kofyou/p/9637963.html
Copyright © 2020-2023  润新知