软工实践作业(二)


PDF
GitHub


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码,汉字不需考虑;
    • 空格,水平制表符,换行符,均算字符
  • 程序可统计文件的单词数,具体要求:
    • 单词4:至少以4个英文字母1开头,跟上字母数字符号2,单词以分隔符3分割,不区分大小写
  • 程序可统计文件的有效行数,具体要求:
    • 任何包含非空白字符的行,都需要统计;
  • 程序可统计文件的单词词频,具体要求:
    • 最终只输出频率最高的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 : number : 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正则表达式的语法与示例
正则表达式匹配解析过程探讨分析(正则表达式匹配原理)

posted @ 2018-09-12 22:55  Eventide  阅读(572)  评论(10编辑  收藏  举报