软工实践作业(二)
PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 65 | 68 |
· Estimate | · 估计这个任务需要多少时间 | 65 | 68 |
Development | 开发 | 510 | 558 |
· Analysis | · 需求分析 (包括学习新技术) | 150 | 180 |
· Design Spec | · 生成设计文档 | 30 | 24 |
· Design Review | · 设计复审 | 20 | 11 |
· Coding Standard | · 代码规范(为目前的开发制定合适的规范) | 10 | 5 |
· Design | · 具体设计 | 30 | 10 |
· Coding | · 具体编码 | 120 | 158 |
· Code Review | · 代码复审 | 40 | 26 |
· Test | · 测试(自我测试,修改代码,提交修改) | 120 | 144 |
Reporting | 报告 | 90 | 65 |
· Test Report | · 测试报告 | 40 | 18 |
· Size Measurement | · 计算工作量 | 20 | 14 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 33 |
合计 | 665 | 691 |
需求分析
基本功能点:
- 程序可通过命令行读取输入文件;
- 程序可统计文件的字符数,具体要求:
- 只需要统计Ascll码,汉字不需考虑;
- 空格,水平制表符,换行符,均算字符;
- 程序可统计文件的单词数,具体要求:
- 程序可统计文件的有效行数,具体要求:
- 任何包含非空白字符的行,都需要统计;
- 程序可统计文件的单词词频,具体要求:
- 最终只输出频率最高的10个;
- 频率相同的单词,优先输出字典序靠前的单词;
- 按照字典序输出结果至文件result.txt,具体要求:
- 输出的单词统一为小写格式;
- 需按格式5输出.
非功能性需求:
- 对三个核心功能统计字符数、统计单词数、统计最多的10个单词及其词频进行封装;
- 使用Github进行源代码管理,代码有进展即签入Github。根据需求划分功能后,每做完一个功能,编译成功后,应至少commit一次;
- 至少应采用白盒测试用例设计方法来设计测试用例,并设计至少10个测试用例.
备注:
[1、英文字母:A-Z,a-z;](#header1) [2、字母数字符号:A-Z, a-z,0-9;](#header1) [3、分割符:空格,非字母数字符号;](#header1) [4、例:file123是一个单词,123file不是一个单词。file,File和FILE是同一个单词;](#header1) [5、输出格式示例:](#header1) ```java characters: number words: number lines: number解题思路
看到这个题目后,我其实第一想法是用MapReduce....这也算是MapReduce的Hello World了。不过题目是在单机上测试,所以用分布式框架毫无意义(之后应该会补充基于MapReduce的WordCount)。
这次需要实现的功能其实主要是两个部分:字词计数和文件读写。下面进行具体描述:
对于文件读写,因为很多计数处理在读文件时可以一起完成,所以我选择将文件读取放进计数模块中。而写文件则独立出来,避免过多功能写在一起显得太臃肿。
对于核心的计数模块,其实字符和行数还是比较好实现的。但在字符计数中也碰到了一个问题,用readLine读取文件时,无法将换行符读取进来,更改成read一个一个读就没问题了。对于单词的读取,我一开始想直接用split进行切分,但又有些担心正则的效率。。经过测试,最后还是选择了stringTokenizer进行切分,正则用来匹配。不过官方并不推荐用stringTokenizer,,但简单切分还是蛮好用的。
关于怎么做词频排序,我起初想了几个方案:转换为list直接sort、建堆、BFPTR加快排。实测BFPTR加快排还是会比堆快一点的。但最终实现时,我还是用了sort,写起来干净方便。。其实也是有些地方没修好,因为很少用java写算法,所以虽然能跑起来,但中间冗余部分还是有点多,看着非常别扭,于是弃用了。很难说这样扯出来的代码性能究竟怎么样,因为时间有限,所以没有再进行对比测试,之后修复好还是得多试试。
代码规范
代码规范我用的是实验室的代码规范:阿里巴巴的码出高效,并加上了一些补充。
设计说明
总体设计简述
整体由一个计数模块提供字词计数功能,分为字符计数、单词计数、行数计数、词频计数四个部分.
类图及流程图
类图
流程图
模块设计
计数模块
模块说明
通过传入文件名,提供统计字符总数、单词总数、总行数和总词频的功能.
类说明
CharCounter
(1) countChar(String fileName):long
功能:计算字符数
输入:fileName:文件名
输出:文件总字符数
WordCounter
(1) countWord(String fileName):long
功能:计算单词数
输入:fileName:文件名
输出:文件总单词数
LineCounter
(1) countLine(String fileName):long
功能:计算行数
输入:fileName:文件名
输出:文件总行数
WordsFrequencyCounter
(1) countWordsFrequency(String fileName):long
功能:计算单词词频
输入:fileName:文件名
输出:各单词词频
(2) topTenFrequentWords(HashMap<String, Long> wordMap):ArrayList<HashMap.Entry<String, Long>>
功能:求出频率最高的10个单词
输入:wordMap:各单词词频
输出:频率最高的10个单词
关键代码
词频计算器部分,使用StringTokenizer分词,然后用regex匹配,存入HashMap中,再转换为ArrayList进行排序。
/**
* 词频计算器,包括计算文件中各单词词频,只输出频率最高的10个.
* 频率相同的单词,优先输出字典序靠前的单词.
*
* @author xyy
* @version 1.0 2018/9/12
* @since 2018/9/11
*/
public class WordsFrequencyCounter {
/**
* 读取并计算文件词频.
*
* @param fileName 文件名
* @return 各单词词频
*/
public static HashMap<String, Long> countWordsFrequency(String fileName) {
InputStreamReader inputStreamReader = null;
BufferedReader bufferedReader = null;
String in = null;
String regex = "[a-zA-Z]{4,}[a-zA-Z0-9]*";
String delim = " ,.!?-=*/()[]{}\\\"\\';:\\n\\r\\t“”‘’·——…()【】{}\\0";
String word = "";
HashMap<String, Long> wordMap = new HashMap<String, Long>(16);
//读入文件
try {
inputStreamReader = new InputStreamReader(new FileInputStream(fileName));
} catch (FileNotFoundException e) {
System.out.println("找不到此文件");
e.printStackTrace();
}
if (inputStreamReader != null) {
bufferedReader = new BufferedReader(inputStreamReader);
}
//计算单词词频
try {
while ((in = bufferedReader.readLine()) != null) {
in = in.toLowerCase();
//根据分隔符分割
StringTokenizer tokenizer = new StringTokenizer(in, delim);
while (tokenizer.hasMoreTokens()) {
word = tokenizer.nextToken();
//匹配单词
if (word.matches(regex)) {
if (wordMap.get(word) != null) {
wordMap.put(word, wordMap.get(word) + 1);
} else {
wordMap.put(word, 1L);
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
inputStreamReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return wordMap;
}
/**
* 求频率最高的10个单词
*
* @param wordMap 各单词词频
* @return 频率最高的10个单词
*/
public static ArrayList<HashMap.Entry<String, Long>> topTenFrequentWords(HashMap<String, Long> wordMap) {
ArrayList<HashMap.Entry<String, Long>> wordList =
new ArrayList<HashMap.Entry<String, Long>>(wordMap.entrySet());
Collections.sort(wordList, new Comparator<HashMap.Entry<String, Long>>() {
public int compare(Map.Entry<String, Long> o1, Map.Entry<String, Long> o2) {
if (o1.getValue() < o2.getValue()) {
return 1;
} else {
if (o1.getValue().equals(o2.getValue())) {
if (o1.getKey().compareTo(o2.getKey()) > 0) {
return 1;
} else {
return -1;
}
} else {
return -1;
}
}
}
});
return wordList;
}
}
Main部分,建立线程池,并行运行四个任务,然后输出至文件。
/**
* 主函数类,包括提交计数任务、打印结果.
*
* @author xyy
* @version 1.0 2018/9/12
* @since 2018/9/11
*/
public class Main {
public static void main(final String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
//计算字符数
Future<Long> futureChar = executor.submit(new Callable<Long>() {
public Long call() {
return CharCounter.countChar(args[0]);
}
});
//计算单词数
Future<Long> futureWord = executor.submit(new Callable<Long>() {
public Long call() {
return WordCounter.countWord(args[0]);
}
});
//计算行数
Future<Long> futureLine = executor.submit(new Callable<Long>() {
public Long call() {
return LineCounter.countLine(args[0]);
}
});
//计算单词词频
Future<ArrayList<HashMap.Entry<String, Long>>> futureWordFrequnency = executor.submit(
new Callable<ArrayList<HashMap.Entry<String, Long>>>() {
public ArrayList<HashMap.Entry<String, Long>> call() {
return WordsFrequencyCounter.topTenFrequentWords(
WordsFrequencyCounter.countWordsFrequency(args[0]));
}
});
//输出至文件
try {
FilePrinter.printToFile("result.txt",
futureChar.get(), futureWord.get(), futureLine.get(), futureWordFrequnency.get());
executor.shutdown();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
异常处理
对于各个异常情况都会打印异常信息,如读取文件时,如果找不到对应文件:
try {
inputStreamReader = new InputStreamReader(new FileInputStream(fileName));
} catch (FileNotFoundException e) {
System.out.println("找不到此文件");
e.printStackTrace();
}
性能分析
可见最大开销来源于多线程并行以及单词计数部分。
单元测试
单元测试框架用的是JUnit4。
我总共设计了十一个单元测试,其中Main一个,三个字词计数部分各三个,词频计数部分一个。
单元测试 | 测试项 | 被测试代码 |
---|---|---|
CharCounterTest | 分别测试普通字符、换行符和空格 | CharCounter.java |
WordCounterTest | 分别测试普通单词、特殊单词和大小写单词 | WordCounter.java |
LineCounterTest | 分别测试普通行、空白行和混合行 | LineCounter.java |
WordFrequencyCounterTest | 测试混合单词 | WordFrequencyCounter.java |
MainTest | 测试空白文件 | Main.java |
代码覆盖率
检测覆盖率使用的是IDEA的Coverage,截图如下:
因为异常处理并没有单独提出来,而是当场处理了,所以总的代码覆盖率并不高。尤其是功能比较简单的字词行计数部分,许多代码都用来处理读写文件异常了。
感想
这次最大的感想就是差点没赶上deadline。。虽然时间预估看上去没有出现太多问题,但这实际上算是用工程质量的下降换来的,有许多地方没有达到原先预想的水平。因为之前有了几次做小项目的经验,所以我很重视需求分析和设计文档,事前也做了许多学习,但实际上手时,还是遇到比较多的问题。很多问题还是源于我对java编程和各个工具的使用还不够熟练,特别是异常处理和单元测试部分,非常不满意。。
也因为还不熟练,很多知识需要当场查阅学习,浪费了很多时间。最后实际编码时间其实不长,一次编码中也遗留了一些小问题,到测试时才再一一解决。
通过这次的作业,我也对单元测试有了个大概的理解。之前做测试都是手动编写一些样例进行测试,就像做算法一样。不过比较糟糕的是我是在编码结束后才编写单元测试的。。在学习相关内容时,我才了解到单元测试最好在设计时就写好,或者至少也应该跟程序一起写了。而且我编写的单元测试也比较简单,有许多用法还在学习。
还有一点就是对GitHub的使用,其实也是对代码的管理。我之前是不常用Git的,常常是按自己的习惯在本地进行保存和版本管理。做实验室的项目时,也没有很好地利用svn,经常是完成了几个部分才一起提交,但并这不符合实际软件工程的要求。而且我还学会了怎么更好地书写commit message,对比之前惨不忍睹的提交记录。。。
这次也算是第一次像点样子的完成了整个软件开发的工程,深感自己在编码和时间把控上还非常不足,希望在之后的结对和组队中能够有所提高。
参考链接
git commit 规范指南
现代软件工程讲义 2 开发技术 - 单元测试 & 回归测试
在IntelliJ IDEA中查看代码覆盖率结果
IDEA 单元测试覆盖技巧
Java 比较字符串之间大小
BFPRT算法O(n)解决第k小的数
Java的简单单元测试例子
Java正则表达式的语法与示例
正则表达式匹配解析过程探讨分析(正则表达式匹配原理)