軟工實踐作業(二)



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正則表達式的語法與示例
正則表達式匹配解析過程探討分析(正則表達式匹配原理)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM