寒假作业(2/2)
这个作业属于哪个课程 | 2021春软件工程实践|W班 |
这个作业要求在哪里 | 寒假作业2/2 |
这个作业的目标 | 阅读《构建之法》,提出问题。 完成WordCount程序 |
其他参考文献 | 无 |
阅读《构建之法》并提问
问题1
p204原文
p204 在表9-2种中列出项目可能会遇到的风险和类别,我对其中的技术和环境的风险类别有一些疑问,据我查询资料所了解,产品经理可能不需要了解很多的技术,那么PM要怎么判断一个项目的开发和测试工具、平台、安全性呢?还有法律法规这一风险来源,作为一个产品经理,是否还要求需要对行业的法律法规有一定的了解呢?
关于我自己提出的问题,首先在技术这一风险层面,普遍认为产品经理如果懂技术更好,可以充分理解技术人员的难处来平衡各方。在法律法规这一方面,我在网上了解到的是在一个PM接项目时,会由公司的法务部门来进行对接,从而解决PM在法律层面上的知识漏洞。
问题2
p61原文:你发现他把时间都花在“解决(低层次)问题”上了,你想考察的“算法技能”、“C#程序设计技能”都无暇顾及。注意,这是在他认为非常精通的编程工具和编程语言中出现这样的问题。你要这样的员工么?
原文谈到一位简历上“精通Visual Studio C#”编程的面试者在被面试官要求用IDE写一段程序之后,漏洞百出。我的疑问是如何界定一个问题属于“低层次”的问题呢?首先我同意书中对面试过程中出现低级错误的观点,但我对“低层次”问题的界定仍然感到模糊,所谓的“精通”当然不该包含文中所说的“少了一个分号”、“怎么设断点”等问题,但一位程序员所谓的“精通”是否应该掌握一门语言的绝大部分内容呢?
就我个人的观点,“精通”自然应该是自己最为掌握的一项技能,从我个人的经历来说,真正掌握一门语言,应该是能够学习一门技术最前沿的发展方向,比如新版本新特性等等。
问题3
p351原文:大家听了很多创新者的故事,有些人想,他们真了不起,第一个想出了这些美妙的想法,要是我早生几十年,也第一个实现那些想法就好了。其实,大部分成功的创新者都不是先行者,例如搜索引擎,Google是很晚才进入这个领域的。
文中认为“创新者都是一马当先”这个观点是错误,还举到了Google公司涉足搜索引擎行业并不是最早的这个例子,但据我所了解,Google公司在其他领域却有着一马当先的创新,比如手机系统、云计算等领域。再说到我认为最顶尖的产品经理,行业内的认为就是“产品格局&社会价值”,所谓“传说中能够改变世界的产品经理”,那么这类顶尖的产品经理他们所长于的不正是创新吗,但他们能够更清晰地看清创新的社会价值和产品格局,这些不都是创新的例子吗?所以我认为“大部分成功的创新者都不是先行者”这个观点说法并不准确。
问题4
p85原文:在结对编程模式下,一对程序员肩并肩、平等地、互补地进行开发工作。他们并排坐在一台电脑前,面对同一个显示器,使用同一个键盘、同一个鼠标一起工作。他们一起分析,一起设计,一起写测试用例,一起编码,一起做单元测试,一起做集成测试,一起写文档,等等。
首先文中的结对编程与我原先预先的结对编程并不相同,我本来认为结对编程就是两个人组成一个团队完成任务,但看到文中的“同一台电脑,同一个显示器,同一个键盘”,我产生了疑问,这种方式是否会降低一个团队的效率呢?两个人做同样的事情,文中谈到的两个都对代码有着最强的熟悉感,能够比其他审核人员更快的找到问题所在,这点我完全统一,但这种方式是否是对开发人员的浪费呢?
我的理解是结对编程有很多优点,并且由老手带新手能够很快帮助新手快速熟悉自己不熟悉的领域。我在网上还看到一个观点:“结对编程人员产生的代码多于两倍“,我认为结对编程不适用于一整个团队,但对于团队的个别开发人员还是十分有帮助的。
问题5
P215原文:如果用户有不同的安全需求,切记要定义不同的角色来适应这些需求。如下面的例子:
受欢迎的典型用户—指那些按设计者的期望使用系统的用户,如“网站的购物者”;不受欢迎的典型用户——指那些有不正当目的的用户,如在一个房地产业主论坛中滥发房屋中介广告的用户,这些用户也许在别的系统中(如房屋中介论坛)是受欢迎的。
书中说到用户可能有不同的需求,同时还要定义不受欢迎的典型用户,但下面又说到”我们的软件不是为所有人服务的“,我认为这中间有一定的矛盾,既然要定义用户,是否应该包含所有的用户使用可能?甚至包括利用软件漏洞进行功击的黑客。我个人的看法是应该仅可能多的定义出用户,虽然可能无法模拟出所有的使用环境,但更多的典型用户可以帮助软件团队更好地分析项目使用环境,包括安全问题的考虑。
WordCount编程
Github地址
PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
• Estimate | • 估计这个任务需要多少时间 | 20 | 20 |
Development | 开发 | ||
• Analysis | • 需求分析 (包括学习新技术) | 40 | 60 |
• Design Spec | • 生成设计文档 | 30 | 30 |
• Design Review | • 设计复审 | 20 | 30 |
• Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 20 | 20 |
• Design | • 具体设计 | 20 | 20 |
• Coding | • 具体编码 | 600 | 720 |
• Code Review | • 代码复审 | 20 | 30 |
• Test | • 测试(自我测试,修改代码,提交修改) | 20 | 60 |
Reporting | 报告 | ||
• Test Repor | • 测试报告 | 10 | 20 |
• Size Measurement | • 计算工作量 | 10 | 15 |
• Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 15 | 30 |
合计 | 825 | 1055 |
解题思路
第一遍看完题目,我首先想到的是对单词的判定,以及如何编写代码可以提升自己程序的性能。在充分理解完需求之后,我决定按三个核心模块来重新分析需求。
1、统计字符数
由于助教已经确认过不会出现ACSII范围外的字符,且需求是除了英文、数字外,空格、制表符和换行符都算作字符,所以我的思路是在读取文件后直接遍历文件获取字符数。
2、统计单词数
需求是至少以4个英文字母开头,以分隔符分割,其中除英文字母和数字外的任意字符均算作分隔符,其中单词不区分大小写。
我的思路是在遍历文件的同时,维护一个单词标记位,当标记位满足要求时,将其判定为单词。
3、统计最多的10个单词及词频
我对统计词频的思路就是维护一个以单词为key、出现次数为value的map,同时这个map需要按value降序排序,当value相同时,按key的字典序排序。
代码规范
设计与实现
类设计
public class Lib{
public static int lineNum;
//文件工具类,用于读取文件和写入文件
public static class FileUtil{...}
//统计字符数,返回long
public static long countChars(String inputPath);
//统计单词数
public static long countWords(String inputPath);
//统计词频,返回string
public static String countWordFrequency(String inputPath);
//统计每行单词,返回map
private static Hashtable<String,Long> countLineWords
(String line,Hashtable<String,Long> mapWord,boolean countWords);
//map排序,将值按升序排序,当值相同时键按字典序排序 返回map
private static Map<String, Long> sortWord(Map<String,Long> map);
//遍历文件统计单词
private static Hashtable<String,Long> countWordsTable
(BufferedReader tempReader,boolean countLine);
}
I/O设计
选用BufferedReader,最初的想法是用BufferedReader的readLine()方法处理文件每行的字符串,用每行字符串的长度来统计字符数,但发现在统计字符时readLine()方法无法统计到换行时的换行符号,后来又发现用read()方法读取每个字符可以读到换行符,所以统计字符模块使用read()方法,统计单词数和词频模块使用readLine()方法。
数据处理设计
因为我统计单词和词频都是用每行的字符串来统计一行内的单词,所以我只需要设计一个函数处理字符串即可,因为统计单词数和统计词频是两个独立的模块,但又都需要遍历文件处理,处理时一样要判断单词,为了减少代码的重复率,我设计了一个函数让他们调用相同的函数,并通过一个布尔值来判断是谁调用了函数。
统计字符数
利用BufferedReader的read()方法,遍历文件即可。
while(tempReader.read()!=-1) //遍历reader
charsNum++;
判断单词逻辑
因为我没有对文件进行分割,直接利用每行的字符串来处理,所以我需要判断一个字符串包含的单词数和出现次数,并且还需要维护一个map,将新判断的单词和出现次数添加到map中。我利用维护一个wordFlag来判断单词。
流程图如下:
因为整部分都是我的核心判断逻辑,所以我这一段贴的代码比较多。核心代码如下:
for(int i=0;i<line.length();i++){
if(Character.isLetter(line.charAt(i))){
wordFlag++; //如果是字母,将wordFlag++;
tempWord.append(line.charAt(i)); //用于清空tempWord
}
else if(Character.isDigit(line.charAt(i)) && wordFlag>=4){
wordFlag++; //当wordFlag>=4时,判定为一个单词,若后续为数字,将wordFlag继续++
tempWord.append(line.charAt(i));
}
else if(wordFlag<4){ //当wordFlag<4且遇到字符为数字时,说明不可能为单词,将wordFlag置零
wordFlag=0;
tempWord.delete(0,tempWord.length());
}
if(!Character.isLetterOrDigit(line.charAt(i)) || i==line.length()-1){
if(wordFlag>=4){ //当遇到分隔符或检索到行尾时
wordsNum++; //若有wordFlag>=4,判定为单词有效,将有效单词++
String tempWordString=tempWord.toString().toLowerCase();
if(!countWords){ //加入map前判断单词是否存在,存在则取出其value,将其+1后放入map
long count=mapWord.containsKey (tempWordString)?mapWord.get(tempWordString):0;
mapWord.put(tempWordString,count+1);
}
}
wordFlag=0; //不论是否为有效单词,都将wordFlag置零
tempWord.delete(0,tempWord.length());
}
}
统计单词数
将文件打开得到BufferedReader,将其传入判断单词函数。
Hashtable mapWord=countWordsTable(tempReader,true);//调用函数
return (long) mapWord.get("wordsNum");
统计有效行数
因为判断有效行数不是一个独立的模块,所以我将其与判断单词数一起判断。
统计词频
因为判断单词的函数返回一个map,那统计词频只需将其排序后生成字符串返回即可,按照value降序排序,当value相同时,按key的字典序排序。
Map<String,Long> sortMap=sortWord(countWordsTable(tempReader,false));//调用函数
for (Map.Entry<String, Long> entry : entrySet) {
frequency.append(entry.getKey()).append(": ").append(entry.getValue()).append('\n'); //遍历map生成字符串
}
return frequency.toString();//返回字符串
性能改进
改进思路
我认为可以从以下几个方面改进程序的性能。
I/O方面
读取文件采用了带缓存的BufferedReader,减少了I/O操作。
判断单词
我的程序采用的方式是用BufferedReader一次读取一行,每行独立判断单词的数量、出现频率等。在与同学交流之后我发现还可以采用按分割符分割字符串,后再单独进行判定单词的方式,可以进一步提高性能。但因为我已经在测试阶段,所以没有修改我的程序。
词频的输出
由于我是按map来存单词的出现次数的,所以需要单独对整个存单词的map进行一次按value排序,在参考了其博客后,选择用流式编程来排序。
三个模块多线程运行
我让三个模块开三个线程同时处理文件,而不是等一个模块处理完后轮到下一个模块处理,该方法极大得缩短了处理时间,但缺点是运行时可能会造成内存占用过高的问题。
改进前后
测试数据规模
随机生成,文件大小320M,一千五百万个有效单词,五十万行,超过三亿个字符。
for(int i=0;i<=50000000;i++){
for(int j=0;j<(int)(Math.random()*10);j++){ //随机生成0~10的字符串
c=(char)(int)(Math.random()*26+97);
stringBuilder.append(c);
}
stringBuilder.append((int)(Math.random()*1000)).append(',');//随机加上1000以内数字
if((int)(Math.random()*100)==50)
stringBuilder.append('\n'); //随机换行
}
改进前
运行时间52s
,内存占用最高1.53G
。
改进后
运行时间35s
,内存占用最高2.53G
。
程序测试
覆盖率
总覆盖率78%,因为我在WordCount类中保留和生成随机文本的测试函数,仅在测试时使用,所以覆盖率应该更高。
覆盖率的优化:
- 简化代码逻辑
- 剔除重复代码
单元测试
字符统计测试
@Test
public void testCountChars() throws IOException {
String testFile="charsTest.txt";
BufferedWriter out=new BufferedWriter(new FileWriter(testFile));
String testString="abcABC\n...123adg\n";//构造包含字母、数字、换行符、符号的字符串;
for(int i=0;i<1000;i++)
out.write(testString); //循环写入一千次
out.close();
int charsNum= Math.toIntExact(Lib.countChars(testFile));//实际结果
assertEquals(testString.length()*1000,charsNum);
}
单词数量统计测试
@Test
public void testCountWords() throws IOException {
String testFile="wordsNumTest.txt";
BufferedWriter out=new BufferedWriter(new FileWriter(testFile));
String testString="abcd123 test456 .123 lqww3\n"; //构造三个单词的字符串
for(int i=0;i<1000;i++)
out.write(testString); //循环写入一千次
out.close();
int wordsNum= Math.toIntExact(Lib.countWords(testFile));
assertEquals(3000,wordsNum); //实际结果应为3000个单词
}
单词频率测试
@Test
public void testCountWordFrequency() throws IOException {
String testFile="frequencyTest.txt";
BufferedWriter out=new BufferedWriter(new FileWriter(testFile));
String testString="aaAA AAaa asd123 abcd ABCD lqwLQW \n";//每行aaaa两个、abcd2个、lqwLQW1个
for(int i=0;i<1000;i++)
out.write(testString); //循环写入一千次
out.close();
String actual=Lib.countWordFrequency(testFile);
String expected="aaaa: 2000\nabcd: 2000\nlqwlqw: 1000\n"; //预期结果
assertEquals(expected,actual);
}
测试结果
三个函数均通过测试。
异常处理说明
程序的异常基本上是文件处理异常,如果找不到输入文件、输出文件错误等等。
当输入文件错误时,抛出找不到文件异常。
当命令行参数不为2个时会提示,"参数错误!"
。
if(args.length!=2){
System.out.println("参数错误!");
return ;
}
心路历程与收获
心路历程
- 我最开始看到作业要求的时候就一心想着先把单词判断的逻辑写好,也没注意到要把核心模块化的问题,认为先把大致逻辑写好,再去分模块应该也没关系,这直接导致了我的代码耦合度过高,要分模块时无从下手,这还导致了我错误地判断了我的程序的性能,在和同学一起测试的时候发现自己的程序比他们跑得更快(但是我也不知道是为什么),等分完模块,算法的不足就逐渐显现出来。所以我学到在开始编写代码之前,应该先分析清楚所有的需求,就算是对需求的某一方面立刻有了想法,应该是先记录自己的想法,再去分析完整的需求。
- 在性能改进方面,我认为只有对底层有一定的了解,才能真正找到对自己项目性能改进的最优解;以及对项目性能测试的用例,一开始我只是简单生成大文件,认为只要足够大的文件就足够测试程序的性能,没有意识到测试用例的随机性。在我重新生成了完全随机的测试用例之后,就发现了与用了更好的算法的同学们的性能差距。
收获
- 意识到版本管理的重要性,以及每写出一些有效代码就提交一次代码,这样还可以很方便得帮助自己查错,除此之外,我认为也很重要的就是在每次提交时的描述,能让自己清晰的看到自己每次提交的内容,而不是还要回看修改的代码。
- 在测试用例方面,我发现一个人很容易有自己的盲区,独自测试很难覆盖自己程序的所有BUG,但多人一起想测试用例就可以找出更多的BUG。
- 第一次进行单元测试,也是第一次进行比较系统化的测试。单元测试可以帮助我们改善整体设计,把整个程序的错误细分到每个模块的错误,有效得帮助我们差错。