天天看點

軟工實踐寒假作業(2/2)

這個作業屬于哪個課程 2021春軟體工程實踐|S班 (福州大學)
這個作業要求在哪裡 軟工實踐寒假作業(2/2)
這個作業的目标 閱讀《建構之法》、熟練使用GitHub等工具、獨立完成WordCount項目(包含詞頻統計等需求)
作業正文
其他參考文獻 CSDN、簡書、部落格園等

GitHub連結:https://github.com/XydfLi

項目連結:https://github.com/XydfLi/PersonalProject-Java

作業描述:作業主要包含兩個部分,一個是閱讀《建構之法》并提問、一個是熟悉GitHub并完成WordCount項目。

目錄

  • part1:閱讀《建構之法》并提問
    • 問題1
    • 問題2
    • 問題3
    • 問題4
    • 問題5
  • 附加題:Lenna圖的故事
  • part2:WordCount程式設計
    • Github項目位址
    • PSP表格
    • 解題思路
    • 代碼規範制定連結
    • 設計與實作過程
      • 總體概述
      • 參數合法性判斷
      • 檔案讀入
      • 檔案寫入
      • 計算部分
        • 統計檔案的字元數
        • 統計檔案的單詞總數
        • 統計檔案的有效行數
        • 統計檔案中各單詞的出現次數并輸出top10
        • countAll函數的實作(精華部分)
        • 内容的整理和輸出
    • 性能改進
      • 改進一:多線程改進(精華部分)
        • 初始狀況
        • 多線程優化統計有效行數
        • 多線程優化單詞統計
        • 多線程任務劃分
        • 多線程優化總結
      • 改進二:檔案讀入改進
      • 改進三:初始化改進
    • 單元測試
      • 構造思路
      • 測試覆寫率
      • 輸入參數測試
      • 統計字元數測試
      • 統計單詞數測試
      • 統計top10單詞測試
      • 統計所有資料測試
    • 計算部分異常
    • 心路曆程
    • 收獲與不足

  25-26頁,關于單元測試的部分,文中有提到好幾個單元測試的标準,标準大多注重的是測試代碼的正确性等等,其中還有一個标準又說到單元測試要快。對于這一部分我有一個疑問是:我們是否應該在單元測試中測試代碼是否達到了性能上的要求(尤其是時間性能)。

  提出這個問題的原因是,我在實際的開發過程中,使用過一些單元測試的工具,他們大多都具有測試程式時間性能是否達标的功能,而我們實際開發過程中有時候也必須關注程式的性能是否達标,我使用的方法是在單元測試中來測試性能,比如可以在測試函數中設定該函數的逾時時間,如果逾時則測試失敗,這一點似乎和單元測試要快的标準有一些沖突?

  我個人的觀點是,一些單元測試不一定要快,應該在單元測試中測試程式的性能。比如我可以在一些測試函數中測試性能,如果發現某部分程式性能不達标,這時候再使用性能分析工具,具體分析該部分程式該如何優化。我這裡強調的是開發人員對程式的測試,不是測試人員對程式的測試,畢竟開發者也是要保證性能達标了再交給測試者測試。

  關于測試的部分,我一直都有一個疑問,我們測試的時候應不應該使用真實的使用者資料(不敏感資料)來進行測試?

  這一點在文中沒有提到,使用真實的使用者資料可以使得我們的程式更加貼近真實的運作環境,而且可以極大地減小我們花費在測試上的精力。比如我們寫了一個統計人口各種資訊的程式,測試的時候我們需要建立一些“不存在的人口資料”,這些資料極多,包含了住址、電話、姓名、政治狀況等等資訊,可能我們當當建立測試用的資料就要花費時間,更别提測試資料的品質如何。如果我們使用了真實資料,不能說我們不需要建立測試資料,但是我們可以隻建立一些真實資料覆寫不到的地方,隻建立一些邊界資料等等。

  但是如果我們使用了真實的資料,測試中出現bug,那麼在debug的過程中就必需要檢視這個真實的資料,這樣的話就容易造成使用者資料的洩露,而且這哪怕不是敏感資料,似乎也會侵犯使用者的隐私。

  市面上有很多的測試資料生成工具,比如datafaker,不過它的原理似乎也不是根據真實資料來構造資料的。

  74頁,文中第4章代碼風格規範中提到一個觀點:注釋應該隻用ascii字元,不要使用中文或其他特殊字元。

  這一個觀點我不認同,注釋都使用英文等有可能反而會增加程式員了解的成本。我認為應該要根據具體情況來定。

  例如我們寫的某一部分代碼開發者都是中國人,而且這一部分代碼是公司内部程式的主要邏輯代碼,不能對外公開,那麼使用中文注釋會極大地降低了解成本,畢竟對于大多數中國人來說,肯定是對母語漢語的了解成本更小,日常的交流也使用中文交流。如果對英文不熟悉的人,很有可能會對某些注釋産生誤解,增加了解成本。

  而如果我們維護的是一個開源代碼庫,參與開源的開發者來自世界各個國家,我們代碼的讀者和使用者也來自世界各國,那麼使用英文注釋是最佳選擇。

  我不知道現在主流開發者是否有使用英文注釋的一些規約,這裡引用一下阿裡巴巴《java開發手冊》2020嵩山版中的觀點:24頁第6點:【推薦】與其“半吊子”英文來注釋,不如用中文注釋把問題說清楚。專有名詞與關鍵字保持英文原文即可。

  關于第4章的結對程式設計部分,首先談一談我的了解,我認為這種方式處在單人程式設計和團隊程式設計的中間地帶,其兼具了兩者的一些優點,同時也多了一些缺點。優點很明顯,因為人數少,溝通成本較低,不需要考慮團隊程式設計中溝通的問題(關于這一點文中也做了詳細的說明)。而人數少不代表效率低,相較于單人程式設計,結對程式設計可以分工合作,産生1+1>2的效果。

  我的問題來自于文中的觀點:結對程式設計是一個漸進的過程,一開始可能不比單獨程式設計效率高,但是度過學習階段後會有明顯改善。那麼我可以認為結對程式設計的一個關鍵是如何快速地度過有效階段?

  關于這個問題文中也說了很多技巧(4.6節),但是這些技巧都是注重花費時間來培養兩個人的默契,幫助更好地合作的。這就帶來了結對程式設計的一個缺點:找到一個适合自己的夥伴不容易,互相之間培養默契度過學習階段也需要時間,在這之前,可能效率不如單人分開程式設計。人數少的一個缺點是如果有人中途離開,那麼很難在短時間内找到一個合适的替代者,畢竟他可能承擔了50%的任務!

  我想知道有沒有一些較為有效的方法能夠快速度過學習階段?如果結對的夥伴中途離開,或者中途換人,有沒有有效的解決方法?現在的程式設計很難有兩個人一直長期合作,培養默契需要時間,經常變換不利影響大,換人帶來的後果比團隊程式設計嚴重得多,我想知道現在開發者結對程式設計的多嗎?

  關于第9章項目經理的部分,提到微軟PM負責開發和測試之外的所有事情,其中包括了産品市場定位、優化方向、和各方面溝通等等職責。關于這一部分,我有一個很大的疑問是他們是否也是一個多人組成的團體?他們是否也應該要有一個精細的分工?

  在我的認知中,産品定位、市場發展的重要性遠遠高于軟體的開發和測試,畢竟一個不符合主流、不符合需求的軟體項目是不會被市場接受的,一旦因為産品定位的問題導緻整個團隊多個月的成果白費,責任在誰?

  在一個10人以下的小團隊中,PM負責了客戶、開發者之間的溝通交流,還負責了項目開發流程的管理,總之是開發和測試之外的所有事情,如果因為某些原因,該PM中途離開,那麼可能直接導緻這個項目失敗,畢竟PM可能就他一個人!相較而言,開發者的離開可以很快地找到下一位替代者,或者由其他開發者代勞,而PM的離開,導緻短時間内開發者需要直面客戶,可能之前所有溝通的成果都沒有了,新來的PM并不能短時間内接上上一位PM的工作,而且新來的PM可能要更改工期的安排,又會打亂各方面的工作進度。

  我想知道,有沒有比較好的辦法可以降低更換PM所帶來的壞處?比如更換了一個開發者,新的開發者隻需要看懂需求文檔等各種文檔就能很好地上手工作,而更換了PM,可能會出現更嚴重的後果,比如新的PM可能認為之前做的工作不符合市場定位,要重做等等情況。

軟工實踐寒假作業(2/2)

  我在進行圖像處理的時候經常會看到以上這張圖,可以說這張圖在計算機視覺領域算是鼎鼎大名了。

這張圖是刊于1972年11月号《Playboy》雜志上的一張裸體插圖照片的一部分。為什麼這麼多人喜歡在圖像進行中使用這張圖檔呢?首先,該圖檔很好的包含了平坦區域,陰影,紋理等細節,這些都有益于測試各種不同的圖像處理算法。它是一幅很好的測試照片!其次,由于這是一個非常有魅力的女人的照片,是以圖像處理研究行業(多數由男性組成)傾向于使用一幅他們認為很有吸引力的圖檔也并不令人驚奇。

Alexander Sawchuk估計大概是在1973年6月或7月間,那時他還是南加州大學信号與圖像處理研究所(SIPI)的一名助教,當時他正在與一名研究所學生以及SIPI研究室的經理正在匆忙地尋找一副高品質的圖檔用于大學的會議論文。他們不喜歡1960年代早期所使用的電視标準所用的普通檢驗圖,他們希望找到一幅能夠得到很好動态範圍的有光澤的圖像,并且希望能有一幅人臉圖像。正在那時,碰巧有人走了進來并且帶着一幅最近出版的《花花公子》,這張圖檔就被他們看上了。

  後來在1997年,圖像科學和技術協會(英語:Society for Imaging Science and Technology)的第50屆會議上,這張圖檔的女主人萊娜·瑟德貝裡被邀為貴賓出席會議,直到這時,萊娜才發現,她在圖像領域有很大的名氣。

相關資料:https://blog.csdn.net/huapenguag/article/details/51077495

PSP2.1 Personal Software Process Stages 預估耗時(分鐘) 實際耗時(分鐘)
Planning 計劃 45 60
• Estimate • 估計這個任務需要多少時間
Development 開發 1360 1375
• Analysis • 需求分析 (包括學習新技術) 180 200
• Design Spec • 生成設計文檔 30
• Design Review • 設計複審 35
• Coding Standard • 代碼規範 (為目前的開發制定合适的規範) 20
• Design • 具體設計 150 125
• Coding • 具體編碼 700 670
• Code Review • 代碼複審
• Test • 測試(自我測試,修改代碼,送出修改) 250
Reporting 報告 70 120
• Test Repor • 測試報告
• Size Measurement • 計算工作量
• Postmortem & Process Improvement Plan • 事後總結, 并提出過程改進計劃
合計 1475 1555

  • 思考過程
  在讀完需求,提出自己的問題,然後徹底搞懂需求之後我再開始思考解題思路。思考的第一步是先思考如何實作需求裡的所有要求,劃分出所有的功能,不考慮性能,不考慮代碼延展性等,提取出需求裡實際需要我們完成的功能。第二步對第一步中得到的功能劃分進行進一步的分析,思考如何具體實作,如何做性能上的提升,提高整體代碼的品質。
  • 功能劃分
  • 輸入部分

    1、指令行指令的輸入和參數的擷取

    2、參數的合法性判斷

  • 1、檔案讀入

    2、統計檔案的字元數

    3、統計檔案的單詞總數

    4、統計檔案的有效行數

    5、統計檔案中各單詞的出現次數并輸出前10個

  • 輸出部分

    1、檔案寫入

  • 異常部分

    1、枚舉并處理所有可能的異常

  • 查找資料

  第一步我先學習完作業要求中提供的各方面學習資料。

  第二步根據之前進行的功能劃分來查找資料。查找資料時找出實作該功能的盡可能多的解法,最後再選擇其中最合适的解法做标記。查找資料的途徑非常多,有CSDN、簡書、百度、知乎、中國知網等等。

  • 初步解題思路
軟工實踐寒假作業(2/2)

代碼規範:https://github.com/XydfLi/PersonalProject-Java/blob/main/221801334/codestyle.md

軟工實踐寒假作業(2/2)

  第一步,定義一個枚舉類型ExceptionInfo,裡面枚舉了所有可能出現的異常資訊,并對各種異常資訊做分類。對異常資訊做分類的目的是為了更好的對使用者做出相應的提示,有的異常提示并退出程式,而有的異常僅需要提示,程式正常執行等等。
  第二步,在WordCount中判斷參數的合法性,主要使用衛語句,通過每一條if語句列舉出所有可能的異常的情況加以判斷和處理。

  檔案讀入主要使用了MMAP,選擇該方法的原因有兩個。

第一個是效率高,MMAP是一種記憶體映射檔案的方法,正常的檔案讀取需要兩次的資料拷貝過程,而MMAP技術僅一次,故效率高。

第二個是可以很好地解決java中緩存流無法讀取到'\r'的問題,緩存流會将'\r'、'\n'、'\r\n'均當成換行,導緻readLine函數讀取不到'\r'字元。

  MMAP學習資料:認真分析mmap:是什麼 為什麼 怎麼用
/**
 * 檔案讀入,使用mmap
 *
 * @param file 輸入檔案
 * @return 檔案内容,如果空則為""
 */
public static String readMMAP(File file){
    RandomAccessFile raf = null;
    MappedByteBuffer mbb = null;
    try {
        raf = new RandomAccessFile(file, "r");
        mbb = raf.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, file.length());
        if (mbb != null){
            return ENCODING.decode(mbb).toString();
        } else {
            return "";
        }
    } catch (IOException e) {
        ...
    } finally {
        ...
    }
    return "";
}
           

  此次檔案寫入的資料量比較小,是以我沒使用mmap方法,使用了緩存流的方法,即BufferedWriter。在這邊我考慮到輸出檔案的内容大多比較少,是以直接設定了緩沖區的大小為1024位元組,減少空間資源的消耗。

  因為需求已經說明了,輸入的字元均為ASCII字元,統計的也是ASCII字元,是以我們直接輸出文本字元串的長度即可。
  實作的原理很簡單,主要是通過正規表達式來比對字元串。如果比對到一個字元串,再判斷該字元串的前一個字元是不是非字母數字的字元,如果是表示找到一個單詞。其中需要注意的一點是第一個比對到的字元串需要單獨處理。
/**
 * 統計文本中單詞個數
 *
 * @return 單詞數
 */
@Override
public int countWord() {
    // 正規表達式為:[a-z]{4}[a-z0-9]*
    Matcher wordMatcher = wordPattern.matcher(content);
    int wordNum = 0;
    if (wordMatcher.find()){ // 第一個比對到的字元串單獨處理
        int start = wordMatcher.start() - 1;
        if (start >= 0){ // 可以判斷前一個字元
            if (wordLegal(content.charAt(start))){
                ++wordNum;
            }
        } else {
            ++wordNum;
        }
    }
    while (wordMatcher.find()){
        if (wordLegal(content.charAt(wordMatcher.start() - 1))){
            ++wordNum;
        }
    }
    return wordNum;
}
           

  這一部分也可以使用正規表達式很友善地解決,但是為了效率我使用了周遊字元串的方法。

  首先我先找到一個子字元串,該字元串以'\n'結尾,就是代碼中startIndex,endIndex之間的字元串。然後從startIndex開始周遊這個子串,一旦找到一個非空白字元就退出循環,表示找到了一個有效行。

/**
 * 計算行數
 *
 * @param start 開始位置
 * @param end 結束位置
 */
private void countLine(int start, int end){
    String temp = content.substring(start, end);
    int tempLen = temp.length();
    // 一行的開始位置
    int startIndex = 0;
    // 一行的結束位置
    int endIndex;
    int i;
    int count = 0;
    char c;
    while (startIndex < tempLen) {
        // 找到一個以'\n'結尾的字元串
        for (endIndex = startIndex;endIndex < tempLen;endIndex++){
            if (temp.charAt(endIndex) == '\n'){
                break;
            }
        }
        if ((endIndex - startIndex) > 0){
            for (i = startIndex;i < endIndex;++i){
                c = temp.charAt(i);
                if ((c >= 33) && (c <= 126)){// 找到一個非空白字元
                    break;
                }
            }
            if (i < endIndex){
                ++count;
            }
        }
        startIndex = endIndex + 1;
    }
    lineNum.getAndAdd(count);
}
           
  第一步,我通過map來記錄每一個單詞的個數,和統計單詞數的原理一樣,通過正規表達式得到每一個單詞,然後更新Map。

  第二步,通過Map中的資料來擷取top10單詞。原理是建構一個優先隊列,然後周遊map,維護這個隊列。如果隊列大小小于10,則該元素入隊,如果大于等于10,那麼判斷該元素是否比隊頭元素大,大則隊頭元素出隊,将更大的元素入隊。最後逆向輸出隊列即可。

  值得一提的是,這種方法的時間複雜度比把map進行排序然後輸出前10個的方法更加小,性能更高。

/**
 * 擷取top10的單詞
 *
 * @param wordIntegerMap Map類,各個單詞的個數
 * @return top10單詞清單
 */
private List<Map.Entry<String, Integer>> getTopTen(Map<String, Integer> wordIntegerMap){
    // 建構優先隊列
    Queue<Map.Entry<String, Integer>> wordQueue = new PriorityQueue<>(16, (o1, o2) -> {
        if (o1.getValue().intValue() != o2.getValue().intValue()){
            return o1.getValue() - o2.getValue();
        }
        return o2.getKey().compareTo(o1.getKey());
    });
    List<Map.Entry<String, Integer>> words = new ArrayList<>(wordIntegerMap.entrySet());
    // 
    for (Map.Entry<String, Integer> word : words){
        if (wordQueue.size() < 10){
            wordQueue.add(word);
        } else if (isReplace(word, wordQueue.peek())){
            wordQueue.poll();
            wordQueue.add(word);
        }
    }

    List<Map.Entry<String, Integer>> wordList = new ArrayList<>(16);
    while (!wordQueue.isEmpty()){
        wordList.add(wordQueue.poll());
    }
    return wordList;
}
           
  有關于這一部分的優化過程,我已經詳細地寫在性能改進的部分,點選這裡前往
  countAll方法的實作并不是簡單地調用前面幾個統計的方法拼接而成的,而是經過了多線程的優化。
  countAll函數的實作主要分為有效行數統計、單詞及詞頻統計兩個部分。原理是首先将文本字元串劃分為幾個子字元串,每個字元串都以'\n'結尾。開線程處理每一個子字元串的有效行數和單詞個數,以及單詞詞頻的統計。
/**
 * 統計字元數、單詞數、行數、top10單詞并輸出到檔案
 */
@Override
public void countAll() {
    ...
    int len = content.length();
    // 字元串按照oneLen的長度來劃分
    int oneLen = Math.max(MIN_THREAD_LENGTH, len >> 4);
    int start = 0;
    int j;
    for (int i = 0;i < len;i += oneLen){
        if (i > start){
            for (j = i;j < len;++j){
                if (content.charAt(j) == '\n'){
                    break;
                }
            }
            int finalStart = start;
            int finalJ = j;
            threadPool.execute(() -> countLine(finalStart, finalJ));
            threadPool.execute(() -> countWordThread(finalStart, finalJ));
            start = j + 1;
        }
    }
    if (start < len){
        int finalStart1 = start;
        threadPool.execute(() -> countLine(finalStart1, len));
        threadPool.execute(() -> countWordThread(finalStart1, len));
    }
    ...
}
           
  這部分主要是調用檔案寫入函數将運算的結果寫入檔案中。其中因為頻繁修改字元串,使用了StringBuilder,它可以極大提高頻繁修改字元串的性能。同樣,因為輸出檔案較小,提前設定了StringBuilder的大小為1024位元組,減小空間消耗。

注意:這一部分作為測試樣例的檔案大小均為:149mb

  當我一開始完成項目的時候,進行性能分析,149mb檔案需要總時間:7497ms。觀察性能分析結果,得出結論:CPU負載在大部分時間内隻有20%-30%。好家夥,作為一個老闆,我不能允許我的CPU大部分時間在磨洋工,解決的方法就是多線程。

具體性能分析如下:

軟工實踐寒假作業(2/2)

  通過查閱資料,我總結出多線程優化的兩個關鍵地方:一個是負載均衡,就是每個線程的工作量差不多。還有一個是盡可能少的加鎖等,以防止線程間過多阻塞造成資源消耗。

  經過細緻地分析代碼,我打算從有效行數的統計開始。既然用到了多線程,那麼線程池也安排上。

  然後将有效行數的統計任務分割好,交給線程池,分割是按照正規表達式比對字元串,一旦比對到一個'\n'字元就配置設定一個線程進行處理。

結果為:149mb檔案需要總時間:8402ms,性能分析如下:

軟工實踐寒假作業(2/2)
  分析結果可知:當線程多的時候,CPU負載确實上升了,當時線程那部分一片紅意味着一堆的線程阻塞。總的時間反而增加了。
  線程阻塞的問題我們先放着,先把單詞統計的部分也加入線程池,将單詞統計部分劃分好交給線程池。這邊需要注意的一點是,Map我改為使用線程安全的ConcurrentHashMap,同時map更新部分的代碼也要修改:
/**
 * 往map中修改資料
 *
 * @param key 單詞
 */
private void dealMap(String key){
    Integer oldValue;
    while (true) {
        oldValue = wordIntegerMap.get(key);
        if (oldValue == null) {
            if (wordIntegerMap.putIfAbsent(key, 1) == null) { // 表示資料添加成功
                break;
            }
        } else {
            if (wordIntegerMap.replace(key, oldValue, oldValue + 1)) { // 表示資料更新成功
                break;
            }
        }
    }
}
           
結果為:149mb檔案需要總時間:6218ms,性能分析如下:
軟工實踐寒假作業(2/2)
線程情況如下:
軟工實踐寒假作業(2/2)
  分析結果可知:CPU負載大部分在40%以上了,不錯,CPU更加努力工作帶來的結果是總體時間的下降。但是還不夠,目前仍然存在兩個問題:一個是CPU負載還是不夠高,另一個是阻塞的線程太多了,這就像是員工之間有很多的地方需要互相配合,但是他們之間的默契又不好。

  經過對代碼分析,線程阻塞的原因是他們要通路同一個資料,為了線程安全,他們不能同時修改通路,造成了阻塞。而我給每一個線程配置設定的任務太少,這就導緻了每一個線程頻繁地通路這個資料,也就頻繁地造成阻塞。

  我的解決辦法是,增加配置設定給線程的任務量,降低配置設定的線程數。任務量劃分的原理是将文本字元串分割為幾段,每個線程工作一段,每一段字元串都以'\n'字元結尾,為了防止每段字元串太小,這裡設定了一個最短字元串的長度。同時,把有效行數和單詞統計的線程工作量都劃分了。代碼請看這裡

  結果為:149mb檔案需要總時間:3598ms,性能分析如下:

軟工實踐寒假作業(2/2)
軟工實踐寒假作業(2/2)
分析結果:CPU負載一半的時間在60%以上,線程也基本都處在運作狀态,沒出現阻塞,時間也下降了。

  總結一下結果,對于149mb的檔案,經過多線程優化後,時間從7497ms下降到3598ms,時間性能提升了52.03%。

  多線程優化對于資料量比較大的情況下,優化的效果比較明顯,而如果資料量太小,因為線程建立、銷毀、切換等開銷,性能有可能反而會下降。是以在這裡我建議老師測試的時候既要有小資料的檔案,也要有大資料的檔案。

  檔案讀入部分使用了MMAP技術,對性能的提升較大,具體實作思路及代碼請點選這裡

經過測試,150mb的檔案,使用mmap讀入花費682ms,使用緩存流的方式讀入花費1548ms。從結果來看,mmap确實使得檔案讀入的時間降低,而且檔案越大,這個差距就越明顯。

  給項目中的List、Map、緩存空間預配置設定一個初始空間。

  對于ArrayList,如果空間不足的時候,将會申請一個更大的數組空間,然後把原數組中的資料複制過去,這一操作如果經常進行,會極大影響性能。

  對于Map,如果元素個數超過負載因子*容量的大小,那麼會執行reHash操作,這個操作也很影響性能。經過測試,添加3750000資料,未配置設定空間的Map需要2388ms,預配置設定空間的Map僅需要741ms。

  緩存空間大小的配置設定主要是在檔案寫入的部分,因為檔案寫入采用緩存流的方式,而輸出内容的大小又比較小,配置設定一個合适的緩存空間大小可以減少緩存重新整理的次數,增加代碼性能。

單元測試主要使用的是TestNG,以及Assert斷言。

  構造單元測試的思路主要是從兩個方面來測試。一個是從使用者的角度,測試是否符合使用者需求,測試在所有使用者輸入的可能性下程式的響應。還有一個是開發者的角度,把程式分塊進行測試,盡可能提高代碼覆寫率,邊界測試(大資料測試),特殊情況測試。

  我設計的測試基本覆寫了所有的代碼,隻剩下一些比較難出現的異常處理測試不到(例如多線程InterruptedException異常等)。

  關于如何優化覆寫率,這裡我使用的編譯器是IDEA,測試的時候可以顯示覆寫率等情況,同時編譯器也标記出了覆寫到的地方和沒覆寫到的地方。我們測試的時候可以觀察有哪些地方沒有覆寫到,然後設計測試樣例覆寫過去。

  最終我的測試覆寫率情況是:類的覆寫率100%,方法覆寫率100%,代碼行覆寫率90%。

測試覆寫率情況截圖如下:

軟工實踐寒假作業(2/2)

  這一部分的單元測試主要模拟在各種使用者輸入下,程式的響應。以下函數主要測試的是WordCount中的Main函數和argsLegal函數。

  因為這個接口由使用者調用,是以測試需要包含所有的情況,在測試中包含了:參數為null、無參、隻有一個參數、輸入檔案不存在、輸入檔案是一個圖檔、輸出檔案不存在、輸出檔案是一個圖檔、輸出檔案已經有内容、輸入輸出正常、輸出檔案所在的檔案夾不存在等等情況。

主要測試的是接口中的countCharacter函數,該函數的作用是統計字元數。
/**
 * 測試字元數
 * 
 */
@Test
public void testCountCharacter() {
    Assert.assertEquals(wordOperation.countCharacter(),47853);
}
           

主要測試的是接口中的countWord函數,該函數的作用是統計單詞數
/**
 * 測試單詞數
 * 
 */
@Test
public void testCountWord() {
    ...
    File input = new File(INPUT_ROOT + "test11.txt");
    File out = new File(OUTPUT_FILE + "test11_output.txt");
    WordOperation wordOperation2 = new WordOperationImpl(input, out);
    Assert.assertEquals(wordOperation2.countWord(),1);
}
           

主要測試的是接口中的countTopTenWord函數,該函數的作用主要是統計top10單詞
/**
 * 測試統計top10單詞
 * 
 */
@Test
public void testCountTopTenWord() {
    List<Word> answer = new ArrayList<>();
    answer.add(new Word("error", 153));
    answer.add(new Word("state", 87));
    answer.add(new Word("quantum", 85));
    answer.add(new Word("logical", 83));
    answer.add(new Word("code", 74));
    answer.add(new Word("qubit", 67));
    answer.add(new Word("correction", 66));
    answer.add(new Word("with", 56));
    answer.add(new Word("that", 53));
    answer.add(new Word("fault", 47));
    List<Word> topTen = wordOperation.countTopTenWord();
    for (int i = 0;i < topTen.size();i++){
        Assert.assertEquals(topTen.get(i).getSpell(), answer.get(i).getSpell());
        Assert.assertEquals(topTen.get(i).getCount(), answer.get(i).getCount());
    }
    ...
}
           

  這一部分主要測試的是接口中的countAll函數,該函數使用多線程優化了各方面的計算。測試中我自己構造了14個檔案,基本包含了所有的情況。

  14個檔案包括了:149mb大資料檔案1、149mb大資料檔案2、自定義空白字元開頭檔案、空檔案、随機生成60000000個字元(範圍0-127)、一篇英文論文、随機生成60000000個空白字元、随機生成60000000個非空白字元、測試類似"\r\n\r\n\r\n"等情況下的空行、7500000個以制表符隔開的"apple123"單詞、以"abcd"開頭的60000004個字母數字、第二篇英文論文、第三篇英文論文(全大寫)

/**
 * 測試計算部分
 * 
 * @param fileName 檔案名
 */
private void test(String fileName){
    String input = INPUT_ROOT + fileName;
    String output = OUTPUT_ROOT + fileName.substring(0, fileName.lastIndexOf(".")) + "_output.txt";
    String answer = ANSWER + fileName.substring(0, fileName.lastIndexOf(".")) + "_answer.txt";
    WordCount.main(new String[]{input, output});
    Assert.assertEquals(FileUtil.readMMAP(new File(answer)), FileUtil.readMMAP(new File(output)));
}
           
這裡就不展示測試檔案了,太多了。

  關于異常這個方面,本次項目中主要異常是使用者輸入方面的異常和檔案操作方面的異常。

  使用者方面的異常的測試和說明我已經寫在單元測試部分了,點選這裡檢視。

  檔案操作部分的異常我這裡補充一下。有關于IOException異常的,該異常已被捕獲并做了相應的處理,出現該異常的情況是輸入檔案不存在,而這個我在使用者輸入參數部分就做了處理,是以該情況不可能出現在這裡。

  • 這次的作業我剛看到的第一眼感覺不難,需求清晰明了,要求我們實作的一些功能也不難。大多數作業的内容都是以前學過的,需要去複習一下重新撿起來,這次項目上我花費時間最多的還是在改進代碼上。
  • 此次的項目開發雖然功能看上去不難,但是我真正開始編碼了才發現,需要我們注意的點,需要我們注意的一些細節還是非常多的。這些細節往往就意味着一個bug,為了解決這些問題,我和很多朋友一起讨論過,這裡感謝和我讨論,一起分享想法的朋友們( ̄▽ ̄)~*。
  • 開發項目的過程中,我開發第一個版本的代碼花費的時間比較少。第一個版本的代碼主要考慮的是先把功能都實作了,性能方面不怎麼考慮。
  • 此次開發中我遇到的第一個問題就是檔案讀取上的問題,緩存流的方法無法讀取到'\r'字元,我當初第一個想法是既然傳統IO不行,那我試試通道的方法。可是通道的方法我又不熟悉,嗯。。。又進入了查資料的狀态。後來發現java這邊也可以用mmap,這讓我很高興,雖然我之前隻在C++上用過mmap,不過原理是一樣的。不過上手以後又發現java的mmap沒有ummap類似的方法,嗯。。。我又開始猶豫了。不過後來經過不斷查資料,找到了解決的方法,而且在性能測試中149mb的資料需要的記憶體不超過2g,我就放心了。
  • 釋出了baseline以後,緊接着又開始進行v2代碼的開發。這一部分耗費了我大部分的時間,我開始思考如何優化我的算法。最開始的想法是用字典樹替換掉Map,字典樹在比對字元串上有優勢,時間複雜度也是O(1),是以我實作了字典樹的代碼(v2中移除了,可以在v1中檢視)。經過多次測試,發現性能還更低!嗯。。。我似乎做了無用功 (`皿´)ノ ,算了,這就是性能優化的常态。
  • 字典樹優化失敗以後,我又開始了多線程的優化,優化過程這裡我不多說了,前面我已經詳細寫下了過程,點選這裡前往,還好,多線程優化給我帶來了好消息。

  • 這次的項目讓我重新複習了一遍java,撿起了很多java的知識。在編碼的過程中我經常要去看jdk的源碼,這也讓我意識到了我的java基礎并不牢固,我也在java中嘗試了許多我之前沒用過,不會用的一些東西。
  • 這次的項目中和朋友們一起讨論給了我很多的思路,項目裡很多的想法也是在讨論中出現的,希望大家以後也能更經常在一起互相讨論(▽),有時候和别人的讨論比自己獨自研究進步更快。這邊特别感謝一下陪我度過大半時間的李宇琨同學,給我帶來很多想法和思路。
  • 之前參加的華為軟體精英挑戰賽學到的一些知識沒想到竟然在這次作業中用上了,兩者都是要求我們對程式的性能盡可能地優化,都需要我們進行性能分析,mmap的知識也是那時候學的。
  • 這裡還要特别感謝一下我們的助教徐明盛學長,這次的作業中我經常找徐學長提問題,每次學長都耐心回答我的問題,非常感謝,希望學長不要嫌我煩( • ̀ω•́ )✧。
  • 這次作業還有一個收獲是讓我熟悉了一些常用工具的做法,像是TestNG、JProfiler等等。
  • 希望後面的作業有性能優化要求的話,可以給出測試計算機的一些資訊,例如作業系統、CPU核數、記憶體等等,這對于代碼的性能優化有很大的幫助。也對于程式設計語言的選擇有很大幫助,例如C++在不同作業系統下自帶的包不同。也希望在測試的過程中能夠多次測試或增大資料集,以減小時間的抖動。
  • 關于代碼還有一個不足之處是top10單詞的統計我還沒開始嘗試優化,這裡标記一下,以後有機會優化,優化思路參考的論文是:張軍,楊家海,王繼龍.基于多次過濾的TopN統計算法[J].清華大學學報(自然科學版),2006(04):604-608.