zoukankan      html  css  js  c++  java
  • 软工实践作业(二)


    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正则表达式的语法与示例
    正则表达式匹配解析过程探讨分析(正则表达式匹配原理)

  • 相关阅读:
    tyvj 1031 热浪 最短路
    【bzoj2005】 [Noi2010]能量采集 数学结论(gcd)
    hdu 1394 Minimum Inversion Number 逆序数/树状数组
    HDU 1698 just a hook 线段树,区间定值,求和
    ZeptoLab Code Rush 2015 C. Om Nom and Candies 暴力
    ZeptoLab Code Rush 2015 B. Om Nom and Dark Park DFS
    ZeptoLab Code Rush 2015 A. King of Thieves 暴力
    hdoj 5199 Gunner map
    hdoj 5198 Strange Class 水题
    vijos 1659 河蟹王国 线段树区间加、区间查询最大值
  • 原文地址:https://www.cnblogs.com/S031602240/p/9613762.html
Copyright © 2011-2022 走看看