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

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.设计实现过程。设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图?单元测试是怎么设计的?
代码组织:

  • 设计了一个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()后使用,即生成字典后再排序;

单元测试:

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判断答案。
我似乎是没有深刻理解单元检测之“单元”的精髓。我事后想,应该是需要分别检测每个函数的;但是我将所有的函数都放在一个单元里检测了;
这样的坏处是:一个函数出错,其后的函数将无法检测。不过我当前这个集所有函数为一体的单元检测还是比较全面了,我有做到将运行结果输出到文件后,
再重新读入并与预设答案比较。其中也设置了一些出错处理,当预设与计算的答案不符时输出计算答案。它对于我设计的样例检测达到预期。
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;若遇到分隔符或者行尾,若长度大于四将单词加入字典;
③如果空格符之后是数字开头,则一直舍弃直到遇到分隔符。特别之处在于巧妙利用了行的处理流程,同时却也显得太基于过程。

7.结合在构建之法中学习到的相关内容,撰写解决项目的心路历程与收获。
首先我有惨痛经历。我错在:看完输入输出后立马动手写程序。我应该做的是:做一个有计划的人,去通读全文,仔细设计结构,充分考虑要求。我的得到的惩罚是:在封装接口之后又经历了一段时间的debug。
而且!!我一开始还不是用vs写的!!移植到vs后又又经历了一段时间的debug!我以后一定做一个规范的人。
也有我想要表扬自己的地方。首先我认为我考虑问题比较全面,尤其是我想到了字符编码的问题,以及我考虑到了txt文末“hahaha\n”和“hahaha”在字符统计上的区别(因为getline会自动舍去换行,我觉得还是挺难想的)。
其次我认为我自行解决问题的能力尤其强,碰到了想要多从所未见的问题,都可以在网络的帮助下解决。同时我也更加认为报错是个好东西,往往指出了问题所在。比如:在单元测试时,这么陌生的东西一debug起来我真的很怕,结果真的不行。觉得无路可走!
但是后来我仔细看了报错,提示no file found并给出了路径,我由此发现单元测试时,取txt输入文件的地方和普通运行时的地方不一样!在相应的地方加入输入文件后ok!
再其次我自学以及理解能力真的好强哦,完全陌生的单元测试我都可以写!厉害!
此外我还有需要改进的地方,比如时间安排(这点真的很重要),我不可以再将作业延后到这么晚。以及上文中已经提到的部分。
在最后我也有对老师和助教布置任务时的建议,我希望要求可以给得更明晰一些,尤其是输出要求和统计要求,有的部分其实有些模糊。其实给一个样例就能很好地帮助我们理解啦!

(计算模块部分单元测试展示。 展示出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路。并将单元测试得到的测试覆盖率截图,发表在博客中;
计算模块部分异常处理说明。 在博客中详细介绍每种异常的设计目标。每种异常都要选择一个单元测试样例发布在博客中,并指明错误对应的场景。)

(计算模块接口的设计与实现过程。 设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图?说明你的算法的关键(不必列出源代码),以及独到之处。)

猜你喜欢

转载自www.cnblogs.com/kofyou/p/9637963.html