寒假作业(2/2)

作业基本信息

这个作业属于哪个课程 2021春软件工程实践 | W班 (福州大学)
这个作业要求在哪里 寒假作业(2/2)
这个作业的目标 1. 深入思考并提出5-10个问题
2. 学习使用github
3. 制定代码规范
4. 学会使用单元测试
5. PSP表格记录
作业正文 part1:阅读《构建之法》并提问
part2:WordCount编程
其他参考文献 CSDN、博客园、简书、git

part1. 阅读《构建之法》并提问

Q1: 面对用户评价褒贬不一的BUG应该怎么办?

什么是BUG呢?简单地说,软件的行为和用户的期望值不一样,就叫BUG。是否是BUG,取决于用户、开发者的不同角度。
例如,某聊天软件不支持视频聊天,用户期望这个聊天软件支持视频聊天。但是该软件的开发人员说,这个软件根本没打算支持视频聊天。这还是一个BUG么?用户下载了一个公司的软件,结果第二天发现电脑上突然多了好几个新软件,但用户从来也没有同意安装。这是BUG,还是用户应该感激的福利?

第一章中提到了BUG这个概念,依作者这么说,不同用户眼中的BUG很有可能是不一样的,而且很多软件的BUG大多是由用户反馈的,特别是开发游戏的软件BUG会出奇的多。这里我因为自己的假设和书中的不同而提问,但是根据我的经验,很多游戏玩家会以游戏中的某些BUG为乐趣(前提是BUG没有打破游戏的公平性),一段时间BUG被修复了以后,都会抱怨BUG怎么没了……

  • 于是我就产生了一个问题,这样用户评价褒贬不一的BUG的出现,我们到底是修复还是不修复它呢?

我的见解是,其实开发人员很大程度上并不了解玩家的想法,我认为可以向玩家收集调查问卷,如果这个娱乐性的BUG被大部分玩家接受的话,其实这个BUG是可以永久存在的因为他并没有打破游戏本身的公平性。但对于那些已经影响到游戏公平性的BUG,开发人员应该立即做出反馈并在第一时间修复它。

Q2: 走进思维误区该怎么办?

分析麻痹:一种极端情况是下弄清楚所有细节、所有依赖关系之后再动手,心理上过于悲观,不想修复问题,出了问题都赖在相关问题上。分析太多,腿都麻了,没法起步前进,故得名“分析麻痹”(Analysis Paralysis)。

第三章中提到的思维误区分析麻痹的问题,我在平时的编程中偶尔也会碰到,特别要解决一个无从入手的功能的时候,我会从网上找源代码进行测试,测试期间肯定会有一系列的问题和我看不懂的代码,而我的强迫症让我一定要把这个问题解决掉。

  • 于是我就产生了一个问题,遇到这样令人无从入手只能找百度将代码一步一步挖掘下去的时候(很多时候最后还是得推倒重来但我也不知道还有什么别的方法),这样算是误区吗?还是有什么其它更好的解决办法呢?

Q3: 结对编程有必要吗?

结对编程让两个人所写的代码不断地从处于“复审”的过程,程序员们能够不断地审核,提高设计和编码质量,可以及时发现并解决问题,避免把问题拖到后面的阶段去。

第四章中提到了结对编程这个概念,作者提到结对编程能使代码始终处于复审的状态,这里书中的描述和我的间接经验有些许的矛盾,根据我的经验,两个人一起思考,若其中一个人提出一个算法,会出现两种极端的情况:

  1. 另一个人没有get到这个人的思路就容易被这个人带着跑,思维也会跟着走偏(直到最后的测试才发现思路不对);
  2. 另一个人对这个逻辑提出质疑并认为自己的算法才是解决之道,而这个人坚持己见,于是两个人就陷入了僵持……
  • 于是我就产生了一个问题,出现这种情况的时候再继续结对编程是不是一种低效率的选择?或者说还是各自编程负责各自需要完成的功能更好呢?因为这样可以有自己的逻辑,不用跟着对方走。以我自己平时的实践经验,我自己独自思考的效率比多人一起思考的效率更高。

我的见解是,要不要结对编程还是因人而异,看自己适不适合和人结对编程,也看结对编程的那个伙伴与自己是否有默契。

Q4: 在理解敏捷流程上的困惑

敏捷方法能够帮助你更早地知道你是否能如期完成任务,仅此而已。敏捷的方法(迭代的方式)能帮你尽快让用户看到项目的部分价值。当你尽早交付部分价值时,也许用户对你目前交付的东西已经很满意了,这样你就不用再花时间来实现其他需求。另一种可能是,用户看到了部分系统,他们有新的需求,这样你就可以实现新的需求,而不用再浪费时间实现过时的需求了。

第六章中介绍了敏捷流程,并将它和瀑布流程进行了一个对比,从我的理解来看,瀑布流程就是先整体布局,最后再按照设计好的流程进行开发。但是这里我还是不太懂书中提到的敏捷流程。

  • 那敏捷流程是不是意味着直接着手开发,做好一个功能后再去思考下一功能做什么呢?如果是这样的话会不会带了些盲目性?会不会出现推翻重来的情况呢?

Q5: 关于开发者和测试者之间的矛盾处理

有些团队把开发和测试有意无意对立起来,好像二者是矛盾是。一个典型的例子是,有时开发人员不想给测试人员提供足够的信息,好像不想“帮”测试人员找到缺陷;与此同时,测试人员一旦找到缺陷,会有些得意,“看,你写的代码那么臭,我又发现了N个Bug”。这种对立情绪,也许在短期内能刺激成员的工作热情,而从长远来看是有害的。很少人会希望在这种充满对立情绪的环境中工作。

下面文字让我产生了共鸣,可是即便我们心里懂得这个道理,但现实中难免会遇到这种情况:

  • 于是我就产生了这个问题,如果遇到了开发者不配合,总是不提供全面的信息有所隐藏的时候,我作为测试者该如何劝说他呢?(鉴于本人交际能力有限)反之我作为开发者提供给测试者详细的信息,结果却遭到测试者的嘲笑,打击了我的自信心,我又该怎么让他减少这种现象的发生呢?

冷知识: 程序中bug的名称源自“虫子”

 在程序中bug一词用于技术错误。这一术语最初由爱迪生在1878年提出的,但当时并没有流行起来。在这的几年之后,美国上将Grace Hopper在她的日志本中,写下了她在Mark II电脑上发现的一项bug。不过实际上,她说的真的是“虫子”问题,因为一只蛾子被困在电脑的继电器中,导致电脑的操作无法正常运行。如图片所见,她写道“这是我在电脑上发现的第一个bug”。结果被政府拒绝,因为当时荷兰没有这个职业。
参考来源

part2:WordCount编程

1. Github项目地址

2. PSP表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划
• Estimate • 估计这个任务需要多少时间 20 10
Development 开发
• Analysis • 需求分析 (包括学习新技术) 120 150
• Design Spec • 生成设计文档 60 20
• Design Review • 设计复审 30 20
• Coding Standard • 代码规范 (为目前的开发制定合适的规范) 30 30
• Design • 具体设计 60 40
• Coding • 具体编码 200 150
• Code Review • 代码复审 50 30
• Test • 测试(自我测试,修改代码,提交修改) 120 120
Reporting 报告
• Test Repor • 测试报告 60 30
• Size Measurement • 计算工作量 60 10
• Postmortem & Process Improvement Plan • 事后总结, 并提出过程改进计划 60 30
合计 870 640

3. 解题思路描述

  • 首先编程语言选择的问题,最后选择使用C/c++来编写程序;
  • 考虑到这次编程作业需要用到大量字符串处理的相关知识,于是在网上找了许多这类的资料;
  • 统计字符数这个指标,直接对得到的字符串进行一个length就能得出结果;
  • 统计单词数量这个指标,需要对字符串进行分割,这边需要一个分割函数来实现,再对单词进行判断和存储;
  • 统计有效行数这个指标,可以截取每一行的字符串进行判断;
  • 单词出现的频率这个数据,可以使用map容器来存储,再通过vector容器与sort函数的结合来对数据进行排序和输出。

4. 代码规范制定链接

5. 设计与实现过程

首先我创建一个名为WordFile的类,在类中使用功能函数,这个类中有3个私有数据成员:输入文件instream和输出文件ofstream,以及存储单词出现频率的map容器。

        ifstream infile;
	ofstream outfile;
	map<string, int> mword;

WordFile中还有8个功能函数,其中打开输入输出文件各划分一个函数来实现。注意,输入文件需要对文件是否存在进行一个判断否则会出现异常。打开文件后,调用getWordFile()函数从输入文件中获取到文本字符串,存储在主函数里,接着就开始依次获取需要的四个指标。

	int getCharacters(string str);
	int getWords(string str);
	int getLines(string str);
	void getWordsNum();
  1. getCharacters()用来获取字符总数,这个函数调用一个str.length()就能得到结果。
  2. getWords()用来获取单词总数,由于先前得到的字符串是连续的,需要对字符串进行一个分割处理,而C没有自带的split函数,我自己写了一个split函数对字符串进行分割,对字符串进行分割是本程序的关键之一。
vector<string> split(const std::string &s) {
	vector<string> vec_ret;
	typedef string::size_type string_size;
	string_size i = 0;

	while (i != s.size()) {
		while (i != s.size() && isspace(s[i]))
			++i;
		string_size j = i;
                // 获取空白字符之间连续的非空白字符串的位置
		while (j != s.size() && !isspace(s[j]))
			++j;
                //截取字符串
		if (i != j) {
			vec_ret.push_back(s.substr(i, j - i));
			i = j;
		}
	}
	return vec_ret;
}

然后先对字符串进行大小写转换(都转换为小写),再对每个分割出来的字符串进行单词判断,这个也独立出来一个isWord()函数对单词进行一个判断。我将存储单词的出现频率也放在了这个功能函数里,先使用map自带的find()函数判断单词是否已经存储在map中,若已存储则value++,若还未存储,则value=1。

  1. getLines()用来获取有效行数,我的逻辑是循环搜索换行符出现的位置,再对每一个分离出来的字符串,去除掉空白字符后,判断是否为空,若不为空则有效行数加一。注意还要对最后一个换行符后面的字符串进行判断,在测试的时候我才发现了这个问题。

  2. getWordsNum()用来输出出现频率最高的十个单词,这里用到C自带的sort函数,与vector容器结合,并通过自己写的cmp函数实现map容器value的排序以及key的排序。

	vector< pair<string, int> > vec(mword.begin(), mword.end());
	sort(vec.begin(), vec.end(), cmp);
bool cmp(const pair<string, int>& a, const pair<string, int>& b) {
	if (a.second != b.second)
		return a.second > b.second;
	return a.first < b.first;
}

程序的独到之处:

  • 代码精简,整个程序不过200行,没有特别复杂的算法,逻辑较为简单;
  • 在排序上使用sort函数与cmp函数通过vector容器排序一步到位,减少了许多的工程;
  • 代码不多,却有十个函数,实现了主测试函数与功能函数的分离,并将文件分离为一个用来声明函数的头文件,一个用来实现所有函数功能的源文件,以及一个包含主测试函数的源文件。

6. 性能改进

  • 在性能测试方面,我生成了一个由100万个“abcd \n"字符串组成的输入文件,下面是性能测试结果截图:
    性能测试截图
  • 为了更显著地看到结果,我将测试文件改成1000万个字符,结果还是可观的:
    性能测试截图
  • 我又生成了长度为4~10的50万个随机字符串的输入文件,结果如下:
    性能测试截图
  • 在缓存优化上,对于多次使用的计算结果及时缓存,避免重复计算,特别修改循环中需要用到的数据;
  • 整个程序较为复杂的部分就是map排序的部分,我使用C++自带的sort函数,是我所了解到的较为便捷也不太复杂的方法;

7. 单元测试

  • 打开输入文件函数测试:测试是否能正确打开已存在的文件;
		TEST_METHOD(OpenInFile)
		{
			Assert::AreEqual(true, wf1.openInFile("input.txt"));
			Assert::AreEqual(false, wf2.openInFile("input"));
			Assert::AreEqual(false, wf3.openInFile("input3.txt"));
			Assert::AreEqual(false, wf4.openInFile("input1"));
		}
  • 统计字符数函数测试:测试是否能统计空格、制表符、换行符等空白字符;测试能否正确统计字符数;
		TEST_METHOD(GetCharacters)
		{
			Assert::AreEqual(0, wf.getCharacters(""));
			Assert::AreEqual(3, wf.getCharacters("   "));
			Assert::AreEqual(8, wf.getCharacters("	aa\nb\n		"));
			Assert::AreEqual(2,wf.getCharacters("	\n"));
			Assert::AreEqual(9, wf.getCharacters("abc*65m. "));
		}
  • 判断是否为单词函数测试:测试能否正确判断单词;
		TEST_METHOD(IsWords)
		{
			Assert::AreEqual(false, wf.isWord("abc123b"));
			Assert::AreEqual(false, wf.isWord("a"));
			Assert::AreEqual(false, wf.isWord("1235"));
			Assert::AreEqual(false, wf.isWord("ab3ff"));
			Assert::AreEqual(true, wf.isWord("abff"));
		}
  • 统计单词数函数测试:测试是否能正确分割字符串,并输出正确的单词数;
		TEST_METHOD(GetWords)
		{
			Assert::AreEqual(2,wf.getWords("abc123b	 abbb	23sa\n\nAsshb"));
			Assert::AreEqual(1, wf.getWords("	absad123\nbbb\n		"));
			Assert::AreEqual(0, wf.getWords("	\n"));
			Assert::AreEqual(1, wf.getWords("abcr*65m."));
		}
  • 统计有效行数的测试:测试空行是否被忽略计算;测试有效行的判断是否正确;
		TEST_METHOD(GetLines)
		{
			Assert::AreEqual(2, wf.getLines("abc123b	 abbb	23sa\n\nAsshb"));
			Assert::AreEqual(2, wf.getLines("	absad123\nbbb\n		"));
			Assert::AreEqual(1, wf.getLines("	\n  \nf		hhdsa\n		\n"));
			Assert::AreEqual(0, wf.getLines(""));
			Assert::AreEqual(0, wf.getLines(" \n	\n "));
			Assert::AreEqual(1, wf.getLines("ab*65m\n"));
		}
  • 单元测试结果截图:
    单元测试
  • 代码覆盖率截图:
    代码覆盖率
    在获取代码覆盖率结果这一步上,我卡了很久。本来是使用VS来做的,但下载了各个版本的VS,运行代码覆盖率的时候都报告错误“无法生成覆盖率”,网上查了很多也都没有找到和我出现类似情况的资料。网上也极少关于C++代码覆盖率的获取途径,最后只能使用GCC自带的GCOV来获取代码覆盖率,虽然结果不太直观,但还是可行的。
  • 在优化覆盖率上,重点检查带有分支的语句,减少不必要的判断和重复的代码,增加特殊的测试例子。

8. 异常处理说明

  • 异常处理命令行参数无输入/输出文件的情况:
	if (agrs< 3) {
		cout << "Please input and output files!";
		return 0;
	}    
  • 异常处理输入文件不存在的情况:
  	if (!wf.openInFile(arg[1])) {
		cout << "Could not find the file!";
		return 0;
	}

9. 心路历程与收获

在这次的作业中,我接触了许多之前没有接触过的知识:

  1. 我初步学习使用了github,在对代码的管理上确实很方便;
  2. 刚开始我根本不知道什么是单元测试,通过《构建之法》的阅读以及网上对单元测试的操作,学会了如何使用单元测试,在对代码的测试上便捷了许多,一次性能测试许多数据;
  3. 之前没有使用过对性能的查看,以及对代码覆盖率的测试,现在对这两个指标有了初步的认识;
  4. 其次就是PSP表格,对自己整个工程工作量的记录,刚开始还使用的不太习惯,对整个工作量的计算还不太上手,不过我相信在之后的项目中多使用PSP表格我能更好估计任务量。
posted @ 2021-02-23 10:11  满天都是小欣欣~  阅读(174)  评论(7编辑  收藏  举报