軟件工程實踐寒假作業2/2


軟件工程實踐寒假作業2/2

這個作業屬於哪個課程 2021年春軟件工程實踐|W班
這個作業要求在哪里 作業要求
這個作業的目標 閱讀構建之法並提問;設計一個程序,能夠滿足一些詞頻統計的需求
其他參考文獻

任務一: 閱讀構建之法並提問

單元測試的密度如何安排?

我看了《構建之法》的有關單元測試的文章,其中有提到這樣的一句話:“單元測試應該在最低的功能/參數上驗證程序的正確性。”,之后我對這句話提出了我自己的疑問,如果單元測試都是在最低功能上驗證,那么整體功能的正確性要如何驗證,根據我的實踐,我認為有一些代碼完成的功能十分復雜,通常是由幾個功能函數共同合作實現,如果在單元測試中我只能保證每個函數的正確性,但我要如何保證函數之間的配合不會出錯呢,因為只有這樣我才能得到我想要的功能。 后來查閱了資料才發現,單元測試主要是在開發過程中同步進行,而功能整體的正確性大多數時候要等到全部函數寫完,測試完成之后才可以看出是否正確,但是我還是有疑問,有沒有更高效地,能夠在進行函數單元測試的同時就能預測最終效果的方法?

單元測試一定要由程序的作者來寫嗎?

同樣也是這篇有關單元測試的文章,其中有這樣的一句話:“單元測試必須由最熟悉代碼的人(程序的作者)來寫”,我對這句話以及其接下來的內容有一個疑問,根據我的實踐經歷,有的時候代碼的編寫者可能很難發現自己的錯誤,有可能是因為“思維慣性”,代碼的編寫者有可能比旁觀者更難發現自己的錯誤,比如,在大一的C++課程的一次實踐作業中,我自己看了一個小時也沒看出來的bug,我室友3分鍾就看出來了。單元測試只能由最熟悉代碼的人編寫,從效率上來看,是否過於絕對?我認為單元測試可以讓程序的作者以及不參與編寫的其他程序員一起進行,當然,主要由程序的編寫者來測試是沒有問題的。

敏捷開發一定是好的嗎?

我看了《構建之法》的有關於敏捷開發的文章,文章中提到:“敏捷開發需要盡早並持續地交付有價值的軟件以滿足顧客需求,只有不斷關注技術和設計才能越來越敏捷”,即不斷地推出新功能來保證產品的優勢,這種想法看似沒有什么問題,但是實際執行起來效果如何呢?根據知乎用戶王先生所說,敏捷開發實際上就是將產品經理的工作量轉移到了程序員身上,會極大幅度提高程序員的工作量和疲勞程度,並且有可能因為時間太過於倉促而不去關注功能是否真的滿足客戶的需要,例如知乎里的商城系統,開發麻煩,卻沒有人用,這樣的“敏捷”開發是否違背了敏捷開發的初衷?或者說,如果多花一點時間在功能的設計,以及創意的構想上,讓一周一次的更新“延長”到一月一次的更新,會不會比趕時間開發功能更有效果,更能滿足客戶的需求?敏捷開發需要有多敏捷,這是我們需要考慮的。

團隊之間如何配合做好代碼的整理工作?

我看了《構建之法》中有關於合作開發的內容,文章中提到合作開發需要制定代碼規范,並在最后需要做好代碼復審工作,但是在一個團隊中,往往是多人開發,一個人負責一個獨立的模塊,即使代碼的編寫風格相同,每個人編寫代碼的思維邏輯不同,有可能給代碼復審帶來較大的麻煩,而且如果代碼的標准過於嚴苛,可能會給開發人員帶來一定的負面情緒,而且統一每個人的代碼風格並不是一件容易的事情,需要大家多多交流,都做出一點讓步。

關於創新的思考。

根據書中材料,姜萬勐與孫燕生受圖像解壓縮技術啟發,想到可以用光盤同時記錄圖像、聲音信息,從而記錄視頻信息,並從而開發了世界上第一台VCD,幾乎可以說開創了一個時代。這是一個非常好的創新案例,告訴了我們緊跟技術前沿的意義之所在。個人感悟:在競爭的時候,保護自己的特色、自己的知識產權很重要,把握好技術變現、投入市場的時機也很重要。假如我是當時的競爭者,我會選擇前期好好積累技術、研究需求,並觀察市場的變化;等到第一批用戶有了好的回饋、后期用戶開始接納新技術時進入市場,這時仍然沒有到競爭的白熱化階段,仍然可以占據一個較為領先的地位。此后,一定要觀察市場動向、注重創新,對於新的技術要投入一定的精力去研究。因為媒體介質更新換代較快,不能夠將自己局限;只有不斷地創新,才能盡量不被淘汰。

附加題:bug的來源

1945年9月9日,下午三點。哈珀中尉正領着她的小組構造一個稱為“馬克二型”的計算機。這還不是一個完全的電子計算機,它使用了大量的繼電器,一種電子機械裝置。第二次世界大戰還沒有結束。哈珀的小組日以繼夜地工作。機房是一間第一次世界大戰時建造的老建築。那是一個炎熱的夏天,房間沒有空調,所有窗戶都敞開散熱。
突然,馬克二型死機了。技術人員試了很多辦法,最后定位到第70號繼電器出錯。哈珀觀察這個出錯的繼電器,發現一只飛蛾躺在中間,已經被繼電器打死。她小心地用攝子將蛾子夾出來,用透明膠布帖到“事件記錄本”中,並注明“第一個發現蟲子的實例。”。從此以后,人們將計算機錯誤戲稱為蟲子(bug),而把找尋錯誤的工作稱為(debug)。

任務二:詞頻統計程序WordCount

作業描述

  1. 統計文件的字符數 (對應輸出第一行)
  2. 統計文件的單詞總數(對應輸出第二行),單詞:至少以4個英文字母開頭,跟上字母數字符號,單詞以分隔符分割,不區分大小寫。
  3. 統計文件的有效行數(對應輸出第三行):任何包含非空白字符的行,都需要統計。
  4. 統計文件中各單詞的出現次數(對應輸出接下來10行),最終只輸出頻率最高的10個。

項目地址:Fino的Github

PSP表格

PSP2.1 Personal Software Process Stages 預估耗時(分鍾) 實際耗時(分鍾)
Planning 計划 40 55
·Estimate 估計這個任務需要多少時間 5 10
Development 開發 60 60+30
·Analysis 需求分析(包括學習新技術) 30 40
·Design Spec 生成設計文檔 30 40
·Design Review 設計復審 60 53
·Coding Standard 代碼規范(為目前的開發制定合適的規范) 14 10
·Design 具體設計 1*60 1*60
·Coding 具體編碼 1*60 1*60
·Code Review 代碼復審 30 30
·Test 測試(自我測試,修改代碼,提交修改) 60 60
·Reporting 報告 20 20
·Test Repor 測試報告 20 20
·Size Measurement 計算工作量 10 10
·Postmortem & Process Improvement Plan 事后總結,並提出過程改進計划 60 60
合計 559 618

解題思路描述

  1. 統計文件的字符數,按題目所述,問題的關鍵在於如何區分ASCII字符和非ASCII字符,查閱資料發現,java中ASCII的字符轉成int類型后值在0~127之間,故只需要判斷下即可。
  2. 統計文件單詞總數,這里注意到“空格,非字母數字符號為分隔符”,故只需要遍歷一遍文件,將其分割符統一設為空格,然后再調用java的字符串的split函數即可,注意還需要寫一個函數判斷單詞的合法性
  3. 統計文件的有效行數,任何包含非空白字符的行,都需要統計。按行讀取文件,然后過濾掉只有空白字符的行,可以用正則表達式實現。
  4. 統計文件中出現最多次數的10個單詞,這里可以用java中的Map<String,Integer>來實現,需要注意下應該將單詞轉為小寫。

代碼規范連接(節選自阿里巴巴java編程規范)

https://github.com/Fino123/PersonalProject-Java/blob/main/221801435/codestyle.md

計算模塊接口的設計與實現過程。

代碼總共有三個類,Lib類和WordCount類,以及Lib的測試類LibTest。Lib類作為一個工具類為WordCount類提供處理文件的接口,LibTest類用於測試Lib的功能函數是否正確,這三個類的關系如下圖所示:
類之間的關系
其中主要的核心在於Lib類,Lib一共有6個函數,其中5個函數對外開放,分別是getAsciiCount,getLinesCount,getWordsCount,getMostFreguentlyWords,fileToString,這幾個函數分別負責統計Ascii字符,統計文件行數,統計文件中出現次數最多的10個單詞,以及將文件讀取為字符串的形式,最后還有一個isWord函數,用於判斷是否是合法單詞,這里的fileToString和isWord函數被另外四個函數多次調用,這六個函數的關系可以表示成如下的形式:
函數關系
這里先介紹兩個被調用的函數:

fileToString函數

該函數的目的主要是讀取文件,並將文件內容保存在一個字符串當中。之所以把讀取文件提取出來,是因為這樣可以在后續調用的過程中,少一些文件讀取時的異常檢查,提高代碼的可讀性,同時,該字符串可以保留,不需要在執行完一個功能之后再次讀取文件,這也提高了代碼的效率。

public String fileToString(String file_path){
       BufferedReader reader = null;
       StringBuilder builder = new StringBuilder();
       try {
            reader = new BufferedReader(new InputStreamReader(new FileInputStream(file_path)));
            int c;
            while ((c=reader.read())!=-1){
                builder.append((char)c);
            }
        }catch (FileNotFoundException e){
            System.out.println(file_path+"文件不存在");
        }catch (IOException e){
            System.out.println(file_path+"文件打開失敗");
        }
        ...
       return builder.toString();
}

isWord函數

該函數負責判斷傳進來的字符串是否是一個正確的單詞,即由四個英文開頭,后接字母和數字。

    private boolean isWord(String word){
        if (word.length()<4){
            return false;
        }
        //不區分大小寫
        String test = word.toLowerCase();
        //前4位是字母
        for(int i=0;i<4;i++){
            char c = test.charAt(i);
            if (!(c>='a'&&c<='z')){
                return false;
            }
        }
        //后跟字母數字符號
        for(int i=3;i<word.length();i++){
            if(!Character.isDigit(test.charAt(i))&&!Character.isLetter(test.charAt(i))){
                return false;
            }
        }
        return true;
    }

getAscii函數

該函數接收一個文件信息字符串file_info,然后遍歷整個字符串,判斷字符的范圍,若在0~127之間,則計數器加1。

    public int getAsciiCount(String file_info){
        int counter = 0;
        for (int i=0;i<file_info.length();i++){
            int c = (int)file_info.charAt(i);
            if (c>=0&&c<=127){
                counter++;
            }
        }
        return counter;
    }

getLinesCount函數

該函數接收一個文件的地址file_path。之所以不使用fileToString,是因為不好在一個字符串中判斷該行是否是空行。如果按行讀取文件,則可以利用正則表達式"\s+"來匹配任意長度的空行。雖然犧牲了一點性能(但是時間復雜度還是和文件長度線性相關),但可以提高開發效率,代碼也更易於閱讀。

    public int getLinesCount(String file_path){
        String file_info = fileToString(file_path);

        BufferedReader reader = null;
        int counter = 0;
        try {
            reader = new BufferedReader(new InputStreamReader(new FileInputStream(file_path)));
            String line = null;
            while((line=reader.readLine())!=null){
                //判斷改行是否非空
                if (line.length()>0&&!line.matches("\\s+")){
                    counter++;
                }
            }
        }catch (IOException e){
            System.out.println(file_path+"文件打開失敗");
        }
        ...
        return counter;
    }

getWordsCount函數

該函數先將文件字符串中的非數字字母的字符替換為空格字符,然后再調用String的split函數將其分開,得到一個字符串數組,最后對字符串數組中的每一個字符串進行判斷,調用isWord函數,如果該字符串是合法單詞,則計數器加1。

    public int getWordsCount(String file_info){
        //先遍歷一遍,將所有非字母數字的符號都換成空格符
        StringBuilder builder = new StringBuilder(file_info);
        for (int i=0;i<builder.length();i++){
            char c = builder.charAt(i);
            if (!Character.isDigit(c)&&!((c>='a'&&c<='z'||c>='A'&&c<='Z'))){
                builder.setCharAt(i,' ');
            }
        }
        //將其按空格拆分
        String []words = builder.toString().split("\\s+");
        //判斷是否是合法字符,統計單詞數量
        int counter = 0;
        for(int i=0;i<words.length;i++){
            if (isWord(words[i])){
                counter++;
            }
        }
        return counter;
    }

該算法的獨到之處在於,其將文件看成是一個字符流,將所有的非字符數字的字符看成“剪刀”,首先將“剪刀”格式化成空格(這一步很重要,因為分隔符的可能性太多了),然后再統一分割,即可得到一個個獨立的“候選單詞”,最后再統一過濾即可,其示意圖如下:
算法示意圖

getMostFrequentlyWords函數

該函數返回文件流中出現次數最多的10個單詞及其頻率。這里的原理和getWordsCount函數類似,只不過在判斷合法單詞的時候,需要用Map記錄每個單詞出現的次數,最后將Map導入到一個List中,對於List里的每一個Map.Entry按出現頻率->單詞字典序的順序排序即可。

    public List<Map.Entry<String,Integer>> getMostFrequentlyWords(String file_info){
        //先遍歷一遍,將所有非字母數字的符號都換成空格符
        StringBuilder builder = new StringBuilder(file_info);
        for (int i=0;i<builder.length();i++){
            char c = builder.charAt(i);
            if (!Character.isDigit(c)&&!((c>='a'&&c<='z'||c>='A'&&c<='Z'))){
                builder.setCharAt(i,' ');
            }
        }
        //將其按空格拆分
        String []words = builder.toString().split("\\s+");
        //保存每個單詞和其出現的頻率
        Map<String,Integer> words_map = new HashMap<>();
        //該數組用於排序
        List<Map.Entry<String,Integer>> words_arr = null;
        //開始統計
        for (int i=0;i<words.length;i++){
            //首先得是合法單詞
            if (isWord(words[i])){
                ...
            }
        }
        //排序
        words_arr = new ArrayList<>(words_map.entrySet());
        ...
        //從大到小,所以要翻轉
        Collections.reverse(words_arr);
        return words_arr;
    }

計算模塊接口部分的性能改進

改進方向一:減少文件的讀取次數

一開始我設計的函數是孤立存在的,每個函數都接收一個字符串參數,代表讀取文件的路徑,然后在每個函數中都進行一次文件存取,使得程序的效率十分低下。后來我利用fileToString函數,每次在程序執行最開始讀取文件,將文件的內容保存到一個字符串當中,隨后只對這個字符串進行處理,這使得程序進行IO的次數減少了一倍,速度也提升了許多。利用java里Date類的getTime函數可以計算程序總的執行時間,對於一個760339字符的文本,優化前后的結果如下所示。

優化前 優化后
425ms 371毫秒

改進方向二:使用正則表達式來匹配空字符。

利用 split("\s+") 來划分字符串,而不是split(" "),這樣做的好處是split(" ")有可能划分出空字符串,雖然空字符串不是一個正確合法的單詞,但是函數的調用也需要額外的時間。
對比

計算模塊部分單元測試展示

部分單元測試代碼

所有的測試函數均位於LibTest類中,對於Lib的每一個函數XXX,LibTest中都有一個testXXX函數與之對應。

    @Test
    //用於測試getAsciiCount函數
    public void testGetAsciiCount(){
        Lib lib = new Lib();
        assertEquals(36,lib.getAsciiCount(lib.fileToString("test0.txt")));
        assertEquals(0,lib.getAsciiCount(lib.fileToString("test1.txt")));
        assertEquals(41,lib.getAsciiCount(lib.fileToString("test3.txt")));
    }
    @Test
    //用於測試getWordsCount函數
    public void testGetWordsCount(){
        Lib lib = new Lib();
        assertEquals(0,lib.getWordsCount(lib.fileToString("test1.txt")));
        assertEquals(0,lib.getWordsCount(lib.fileToString("test0.txt")));
        assertEquals(2,lib.getWordsCount(lib.fileToString("test3.txt")));
    }
    ...

單元測試思路

  1. 應該考慮到空文件的輸入情況。
  2. 應該考慮到輸入參數少於2或者大於2的情況。
  3. 應該盡可能的使用ASCII字符和非ASCII字符,如漢字。
  4. 測試應該要包括空行,空格,以及其他非字母數字字符(作為分割符)
  5. 盡可能使得代碼的每一個if,每一個case,每一個catch里的內容都得到執行。
    其中,部分測試輸入如下:
    空文件test1.txt:
    空文件test1.txt
    包含空格、空行、以及非ASCII字符的文件test4.txt:
    test4.txt

單元測試覆蓋率截圖

單元測試覆蓋率

如何優化覆蓋率?

在IDEA中,執行完Run Test with Coverage之后,會在代碼的最左邊顯示綠條或者紅條。如果是綠條,則說明測試代碼已經覆蓋該區域;若為紅色,則說明沒有覆蓋,這時候需要專門設計一個針對該區域代碼的測試用例,來提升覆蓋率。
該圖中FileNotFoundException被測試到,而IOException則沒有

計算模塊部分異常處理說明

在WordCount開頭,先判斷用戶輸入的參數個數是否正確?

對應場景:用戶輸入的文件地址參數過多或者過少。測試用例如下:

      public void testWordCount(){
        WordCount.main(new String[]{"input.txt"});
        WordCount.main(new String[]{"input.txt","output.txt","aaa.txt"});
      }

測試結果:
測試結果

在WordCount驗證參數個數正確之后,還要判斷input.txt文件是否存在。

對應場景:用戶輸入的Input.txt文件不存在,或者路徑錯誤。測試用例如下:

      public void testWordCount(){
        //這里input3333.txt不存在
        WordCount.main(new String[]{"input3333.txt","output.txt"});
      }

測試結果
測試結果

輸出的時候,判斷output.txt是否創建成功。

對應場景:程序輸出時,用戶內存空間不足,或者創建output.txt文件失敗。其測試用例如下:

    @Test
    public void testWordCount(){
        //這里f:output.txt不存在,因為我的系統上沒有f盤
        WordCount.main(new String[]{"input.txt","f:output.txt"});
    }

測試結果
測試結果

關閉文件時,判斷文件是否關閉成功。

對應場景:輸出結果到程序,最后文件關閉失敗。對應代碼如下:

finally {
        if (writer!=null){
          try {
              writer.close();
          }catch (IOException e){
              System.err.println(args[1]+"文件關閉失敗");
          }
        }
}  

PSP表格完善

PSP2.1 Personal Software Process Stages 預估耗時(分鍾) 實際耗時(分鍾)
Planning 計划 40 55
·Estimate 估計這個任務需要多少時間 5 10
Development 開發 60 60+30
·Analysis 需求分析(包括學習新技術) 30 40
·Design Spec 生成設計文檔 30 40
·Design Review 設計復審 60 53
·Coding Standard 代碼規范(為目前的開發制定合適的規范) 14 10
·Design 具體設計 1*60 1*60
·Coding 具體編碼 1*60 1*60
·Code Review 代碼復審 30 30
·Test 測試(自我測試,修改代碼,提交修改) 60 60
·Reporting 報告 20 20
·Test Repor 測試報告 20 20
·Size Measurement 計算工作量 10 10
·Postmortem & Process Improvement Plan 事后總結,並提出過程改進計划 60 60
合計 559 618

心路歷程與收獲

在本次作業中,我學會了:

  1. 利用測試單元來驗證代碼的准確性。
  2. 利用github來管理源代碼。
  3. 體會了軟件從設計到開發,再到測試,最后上線(上傳到github)的全過程。


免責聲明!

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



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