寒假作业2/2
这个作业属于哪个课程 | 2021春软件工程实践|W班 (福州大学) |
---|---|
这个作业要求在哪里 | 寒假作业2/2 |
这个作业的目标 | 1.阅读《构建之法》并提问 2.设计一个程序,能够满足一些词频统计的需求 |
项目地址 | PersonalProject-Java-WordCount |
其他参考文献 | 无 |
part1:阅读《构建之法》并提问
阅读构建之法并提问
1.《构建之法》3.3节中提到一个招聘C#程序员的例子,提出 “他把时间都花在‘解决(低层次)问题’上了”,这样的程序员当然不能说是精通C#。我个人学习并偏爱java,能使用java写的程序都使用java,因此构建基本的类、java的循环、条件等语句当然也能不用大脑就自然写出来,可这就算 “精通”了吗?当然也不算吧。书中提出的:“‘提高技能’就是通过长时间的练习来使得不需要动脑就能解决低层次问题”,我认为并不全面,我能够不用动脑就能书写出基础语句和循环条件等语句,我就已经精通这项语言了吗?当然不是,可继续长时间练习更上一级的内容,我就能变得精通吗?也不尽然。“长期练习到不需动脑”说到底只是在固化大脑和身体本能,让我们想到“循环”的时候不需要思考就能敲出重复过很多很多遍的循环语句,可是当我固化了更上一级的内容,将某一个问题的算法作为身体本能记录下来,当遇到这一类问题的时候没有思考脑中就会自然蹦出这一算法并书写,这真的是一件好事吗?编程就是在用代码解决实际问题,书中所说的“提高技能”的极致也不过只是不断重复和累加前人的做法来堆砌出程序,丧失了自己的思考和创新来做程序算得上是一种提升吗?这是我与作者这句话理念相悖的地方。
2.《构建之法》在4.3.4中对于“如何处理C++中的类”,这一问题,提出“仅在必要时,才使用类”。在我过去几年的学习生活和了解中,面向对象的思想是相当好用而且受到市场承认的。我认为类的概念作为面向对象编程的核心概念,正是C++语言比起C语言来得更加优越的核心之一,类所体现的面向对象思想和各种相关内容毫无疑问是有用而且有必要的。而《构建之法》却提出“仅在必要时”使用类,却体现出了并不是很想使用的意愿,这到底是为什么?
3.《构建之法》的第4章中详细阐述了结对编程的概念和它的诸多好处,然而具有诸多好处的结对编程“只是一个美好的概念还是业界普遍使用的一种开发方式呢?”这一点我在上次的提问中也有涉及,但是没有得到回答。我在上网查询了许多资料后,发现“结对编程”在国内似乎并不是一种公司乐意使用的方法。书中如此详尽地介绍了如此之多结对编程的好处,可它似乎并没有被市场接纳,到底在哪个环节出现了问题呢?
4.《构建之法》16.1中提及,“改良式”和“颠覆式”的创新,改良式的创新往往会受到欢迎。虽然颠覆式的创新往往更能更大幅度的改良技术和推进历史进程,但却因为会引起不安而受到抵制,就像文中提及的“电话”颠覆“电报”一样。可对于软件开发而言,能否被用户广泛接受可以说是产品的生命线,因此,作为软件开发从业者的角度来看,是不是比起“颠覆式”的创新,更多地尝试“改良式”的创新会更合适呢?
5.《构建之法》第6章提及敏捷流程的概念,敏捷流程作为一个与通常的编程流程几乎完全不通的流程,书中花了大量的篇幅来介绍它的好处,可是我依旧无法理解,这种“没有计划、没有文档、马上写代码、随时发牢骚”的流程不正是我们在学习软件工程这一门课之前,写作业的时候用的流程吗,事实证明我们写的非常痛苦,成品也并不十分优秀。这样的流程不正是我们学习软件工程后要来避免的东西吗?这样的流程真的具有实用价值吗?
冷知识和故事
1996 - James Gosling发明了Java。Java是一个相对繁冗的、带垃圾收集的、基于类的、静态类型的、单分派的面向对象语言,拥有单实现继承和多接口继承。Sun不遗余力地宣传着Java的独一无二不同凡响之处。
2001 - Anders Hejlsberg发明了C#。C#是一个相对繁冗的、带垃圾收集的、基于类的、静态类型的、单分派的面向对象语言,拥有单实现继承和多接口继承。微软不遗余力地宣传着C#的独一无二不同凡响之处。
part2:WordCount编程
1.github项目地址
2.PSP表格
PSP2.1 | [Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 10 | 10 |
·Estimate | ·估计这个任务需要多少时间 | 10 | 10 |
Development | 开发 | 550 | 645 |
·Analysis | 需求分析(包括学习新技术) | 30 | 35 |
·Desgin Spec | ·生成设计文档 | 20 | 30 |
·Design Review | ·设计复审 | 20 | 20 |
·Coding Standard | ·代码规范(为目前的开发制定合适的规范) | 10 | 20 |
·Design | ·具体设计 | 100 | 120 |
·Coding | ·具体编码 | 300 | 360 |
·Code Review | ·代码复审 | 30 | 30 |
·Test | ·测试(自我测试,修改代码,提交修改) | 40 | 30 |
Reporting | 报告 | 35 | 40 |
·Test Repor | ·测试报告 | 10 | 15 |
·Size Measurement | ·计算工作量 | 10 | 10 |
·Postmortem&Process Improvement Plan | ·事后总结,并提出过程改进计划 | 15 | 15 |
合计 | 595 | 695 |
3.代码规范
4.解题思路
在设计阶段所做的主要工作是将WordCount程序进行合理的分划功能。作业要求提出实现统计文件字符数、统计单词总数、统计有效行数、统计各单词出现次数并输出频率最高的10个单词,共计4个功能。
考虑到每一个功能都必须读取input.txt中的内容才能进行统计,因此将读取源文件划分出来作为一个功能。
考虑到各个功能统计结束后,都要将结果按格式输出至output.txt中作为程序运行结果,因此将输出结果到目标文件中划分出来作为一个功能。
因此,作为设计结果,一共需要实现上述6个功能。
具体思路流程:
1.完成读取文件的方法,以txt源文件路径作为传入参数,返回保存了源文件全部内容的字符串。后续的方法只接受该字符串作为传入参数,就可以做到读取一次文件就完成所有统计。
2.完成统计文件字符数方法,以1方法得到的字符串作为传入参数,设置int变量count,通过toCharArray方法将字符串变成字符数组,通过遍历字符数组并修改count,返回count得到字符数。
3.完成统计单词总数方法,以1方法得到的字符串作为传入参数,设置int变量count,通过split方法和正则表达式分割字符串为疑似单词字符串数组,遍历该数组,在值符合单词条件时修改count,返回count得到单词数。
4.完成统计有效行数方法,以1方法得到的字符串作为传入参数,通过split方法和正则表达式将字符串分割为行字符串数组,返回数组长度得到行数。
5.完成统计各单词出现次数并输出频率最高的10个单词方法,以1方法得到的字符串作为传入参数,通过统计单词总数中使用的方法来得到单词数组,利用hashmap,将单词作为键,出现次数作为值来保存单词与出现次数的对应关系,该功能还要求在输出时,根据值和字典顺序进行排序后输出,通过将entrySet转换为List并修改比较器可以达到排序目的,返回排序后的list,该list的每一个值都保存了单词和出现次数的对应关系(Map.Entry)。
6.完成将统计结果输出至目标文件的方法。以1方法得到的字符串和txt目标文件路径作为传入参数。使用BufferedWriter类,调用之前写的2-5的统计方法,在文件中按格式写入统计结果,唯一需要注意的是方法5的返回值是保存了单词和出现次数对应关系的list,需要遍历list写入结果。
5.设计与实现过程
在Lib类中设计6个方法来实现上述6个功能,以下代码非全貌,只贴出关键代码,中间部分用……代替。
Ⅰ.读取源文件
传入参数为源文件地址,用BufferedReader类对象读取文件,StringBuilder类对象储存读入结果,采取按字符读入的方式,通过StringBuilder类对象的appand方法储存结果,返回时用StringBuilder类对象的toStirng方法转换为字符串。
StringBuilder result = new StringBuilder();//StringBuilder类对象储存读入结果
……
BufferedReader br = new BufferedReader(new FileReader(file));//BufferedReader类对象读取文件
int temp;
while ((temp = br.read()) != -1) //按字符读入的方式
result.append((char) temp);
……
return result.toString();//返回时用StringBuilder类对象的toStirng方法转换为字符串。
Ⅱ.统计文件字符数方法
传入参数为保存了源文件内容的字符串str,返回int类型的count,通过toCharArray方法将字符串变成字符数组,通过遍历字符数组并修改count,返回count得到字符数。
char[] ch = str.toCharArray();//通过toCharArray方法将字符串变成字符数组
……
for (int i = 0; i < ch.length; i++) {//只统计ACSII字符
if (ch[i] >= 0 && ch[i] < 128) {
count++;
}
……
return count;
Ⅲ.统计单词总数方法
传入参数为保存了源文件内容的字符串str,返回int类型的count,由于不分大小写,因此先将str字符串用toLowerCase方法变成小写,然后利用split方法和正则表达式匹配非字母和单词的字符来分割出单词数组,取出字符串数组中字符数量为四个或以上的值,对前四个字符判断是否全是字母,是则为单词并修改count,返回count得到单词总数。
String[] strArray = str.split(wordLikeRegex);//此处的wordLikeRegex为匹配非字母数字字符的正则表达式,利用split方法将str分割成疑似单词数组
……
for (int i = 0; i < strArray.length; i++) {
if (strArray[i].length() >= 4) {//至少要四个英文字母开头,也就是说单词至少需要四个字符
temp = strArray[i].substring(0, 4);//取出分割后字符串的前四个字符
if (temp.matches(wordRegex))//此处的wordRegex为匹配字符串的字符是否全部为字母的正则表达式
count++;
}
……
return count;
Ⅳ.统计有效行数方法
传入参数为保存了源文件内容的字符串str,返回int类型的lineList.size()。由于含有非空白字符的行才计入有效行数,首先通过字符串的replaceAll方法将除了\n换行符以外的空白字符删除,然后将得到的新字符串用split方法以\n为间隔切割开为字符串数组count,则count的数组长度即为有效行数——为最初的设想。后来发现split分割位于句首的换行符(即某一行只有换行符)时,会存入一个空字符串,导致有效行数计算出错,因此加入可变长度的List类对象linelist,遍历count数组将非空值存入lineList,返回lineList的大小为有效行数。
List<String> lineList = new ArrayList<String>();
str = str.replaceAll("[ |\r|\t]+", "");//将str中除了换行符以外的空白字符删掉
String count[] = str.split("\n+");//将新字符串以split方法进行换行符分割,作为行数统计基准
for (int i = 0; i < count.length; i++) {//由于后来发现如果两个换行符连续出现,则会split会将空字符串一并计入,因此用另一个表只记录非空值的部分
if (count[i] != "") lineList.add(count[i]);
}
return lineList.size();
Ⅴ.统计各单词出现次数并输出频率最高的10个单词
传入参数为保存了源文件内容的字符串str,返回List类型的list。通过统计单词总数中使用的方法来得到单词数组,利用hashmap,将单词作为键,出现次数作为值来保存单词与出现次数的对应关系,该功能还要求在输出时,根据值和字典顺序进行排序后输出,通过将entrySet转换为List并修改比较器可以达到排序目的,返回排序后的list,该list的每一个值都保存了单词和出现次数的对应关系(Map.Entry)。
……//遍历判断单词方法与统计单词总数时所用方法一致,不再粘贴
if (temp.matches(WordRegex)) {//如果判定strArray[i]为符合条件的单词。
int freq = map.get(strArray[i]) == null ? 0 : (int) map.get(strArray[i]);
map.put(strArray[i], freq == 0 ? 1 : freq + 1);////利用hashmap,将单词作为键,出现次数作为值,存入对应关系,不曾出现过的键,对应值置1,出现过的键,对应值+1,实现单词的出现频率统计
}
……
//实现排序
List<Map.Entry<String, Integer>> list = new ArrayList<Map.Entry<String, Integer>>(map.entrySet());//将entrySet转换为List
Collections.sort(list, new Comparator<Map.Entry<String, Integer>>() {//修改比较器达到排序目的
@Override
public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
if (o2.getValue().compareTo(o1.getValue()) == 0) {//同值情况按字典排序
return o1.getKey().compareTo(o2.getKey());
} else
return o2.getValue().compareTo(o1.getValue());//不同值情况按值排序
}
});
return list;
Ⅵ.将统计结果输出至目标文件
完成将统计结果输出至目标文件的方法。传入参数为保存了源文件内容的字符串str。使用BufferedWriter类,调用之前写的2-5的统计方法,在文件中按格式写入统计结果,唯一需要注意的是方法5的返回值是保存了单词和出现次数对应关系的list,需要遍历list写入结果。
List<Map.Entry<String, Integer>> list = Lib.getWordFrequency(str);//调用词频统计方法获取返回的list
……
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "utf-8"));
bw.write("Characters: " + Lib.getCharactersCount(str) + "\n");//调用字符统计方法
bw.write("words: " + Lib.getWordsCount(str) + "\n");//调用单词统计方法
bw.write("lines: " + Lib.getLineCount(str) + "\n");//调用行统计方法
for (int i = 0; i < (list.size() < 10 ? list.size() : 10); i++) {//将词频统计返回的list遍历输出
bw.write(list.get(i).getKey() + ": " + list.get(i).getValue() + "\n");
}
……
return 0;
6.性能改进
1.在最初的设计中,writeTotxt方法调用其他方法的时候参数都直接调用readFormTxt方法,后来意识到这样每次调用方法都将进行一次文件读取,过于耗时。于是引入一个字符串变量储存readFormTxt方法读入文件的结果,这样就运行一次程序就只需要读一次文件了。
2.使用BufferedReader和BufferedWriter来进行文件的读写,用以提高IO性能。
7.单元测试展示
Ⅰ.测试读入文件方法
由于单纯只测试能否读入文件,所以不需要特别的方法来验证数据
@Test
public void readFormTxtTest() {
String str = Lib.readFormTxt("D:\\从旧电脑转移来的各种东西\\软件工程实践作业\\WordCount\\test\\input.txt");//调用读取文件方法
}
Ⅱ.测试统计文件字符数方法
@Test
public void getCharactersCountTest() {
String str = "a\n1\nword\n1234\nwindows95\nwindows98\nwindows98\nwindows2000\nwindows2000\nwindows2000\naction\nfi1234\r\n \n\n";
int count = 1000;
String testStr = "";
for (int i = 0; i < count; i++) {
testStr += str;
}
assertEquals(Lib.getCharactersCount(testStr), 98000);//调用字符数统计方法
}
Ⅲ.测试统计单词数方法
@Test
public void getWordsCountTest() {
String str = "a\n1\nword\n1234\nwindows95\nwindows98\nwindows98\nwindows2000\nwindows2000\nwindows2000\naction\nfi1234\r\n \n\n";
int count = 1000;
String testStr = "";
for (int i = 0; i < count; i++) {
testStr += str;
}
assertEquals(Lib.getWordsCount(testStr), 8000);//调用单词统计方法
}
Ⅳ.测试统计行数方法
@Test
public void getLineCountTest() {
String str = "a\n1\nword\n1234\nwindows95\nwindows98\nwindows98\nwindows2000\nwindows2000\nwindows2000\naction\nfi1234\r\n \n\n";
int count = 1000;
String testStr = "";
for (int i = 0; i < count; i++) {
testStr += str;
}
assertEquals(Lib.getLineCount(testStr), 12000);//调用行数统计方法
}
Ⅴ.测试统计各单词出现次数并输出频率最高的10个单词方法
@Test
public void getWordFrequencyTest() {
String str = "a\n1\nword\n1234\nwindows95\nwindows98\nwindows98\nwindows2000\nwindows2000\nwindows2000\naction\nfi1234\r\n \n\n";
List<Map.Entry<String, Integer>> list = Lib.getWordFrequency(str);//调用词频统计方法
String[] keyResult = {"windows2000", "windows98", "action", "windows95", "word"};
Integer[] valueResult = {3, 2, 1, 1, 1};
for (int i = 0; i < (list.size() < 10 ? list.size() : 10); i++) {
assertEquals(list.get(i).getKey(), keyResult[i]);
assertEquals(list.get(i).getValue(), valueResult[i]);
}
}
Ⅵ.测试写入文件方法
由于单纯只测试能否成功按格式写入文件,所以不需要特别的方法来验证数据,通过查看目标TXT文件可以看到符合期望的结果
@Test
public void writeToTxtTest() {
String str = "a\n1\nword\n1234\nwindows95\nwindows98\nwindows98\nwindows2000\nwindows2000\nwindows2000\naction\nfi1234\r\n \n\n";
Lib.writeToTxt("D:\\从旧电脑转移来的各种东西\\软件工程实践作业\\WordCount\\test\\output.txt", str);//调用写入文件方法
}
Ⅶ.测试结果
Ⅷ.测试覆盖率
Ⅸ.如何提高覆盖率?
1.构造符合工作场景描述的测试用例。
2.简化代码逻辑。
3.减少不必要的判断。
4.删除重复代码。
8.异常处理说明
本程序没有构造独立的异常类,只有IOException和FileNotFoundException的异常,都进行了捕获,出现异常将输出异常信息。
9.心路历程与收获
本次实践属实让我懂了很多。
实践内容乍看十分简单,做一个命令行词频统计程序,比起以前的各种实践内容来说可以说是比较简单的一类了,只要花上几个小时,哪怕是从零开始也不难完成。
然而本次实践却布置了很多写本体程序以外的内容。
比如填写PSP表格,规划自己的各个步骤的完成时间等,第一次这么做,确实给我的整个编程过程带来了便利,但是也由于是第一次,我规划的时间和实际完成时间差的属实有点多,下次应该能安排的更贴合实际吧。
还有github的使用,以前也尝试使用过github,但是git命令繁琐又难以记忆,后面就搁置了,在看到作业要求以前我完全不知道原来还有github desktop这么好用的东西,这就是被点拨了的感觉吧。
印象最为深刻的当然是单元测试,非常惭愧,在这次实践以前、甚至在实践进行的过程中,我都是手动测试的。具体来说,比如这一次的WordCount程序,在完成程序的功能后,我不断手动更改input.txt的各种内容,然后反复打开output.txt,每次修改完input.txt,运行,再打开output.txt对结果进行确认。因为不知道到底有没有哪个方法出错,我需要不断反复打开、修改和确认这两个txt文件,将我能想到的各种样例手动输入运行再确认,反复这样做浪费的时间和精力真是比起从零开始到把这个程序写完还要让我心神俱疲。开始写博客了以后,我看到单元测试的要求去查询资料和学习,花了一点时间编写出了用于测试的test类和test方法,虽然在之前的精神污染般的努力下,进行的测试是一遍通过的,但是我脑中的想法是要是我昨天就知道这么干多好,能省多少时间和精力啊。
学会单元测试应该是我在这次的实践中非常大的收获了,真的是非常关键,让我对以后的实践内容更加期待了。