天天看點

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

這個作業屬于哪個課程 2021春軟體工程實踐|S班
這個作業要求在哪裡 軟工實踐寒假作業(2/2)
這個作業的目标

1.閱讀《建構之法》提問

2.WordCount程式設計

其他參考文獻 CSDN相關部落格以及部落格園相關部落格

目錄

  • 任務一 重新閱讀《建構之法》并提問
    • 1. 問題一
    • 2. 問題二
    • 3. 問題三
    • 4. 問題四
    • 5. 問題五
  • 任務二 WordCount程式設計
    • 1. Github項目位址:
    • 2. PSP表格:
    • 3. 代碼規範制定連結
    • 4. 設計與實作過程
    • 5. 性能改進
    • 6. 單元測試
    • 7. 異常處理說明
    • 8. 心路曆程與收獲

4.5 結對程式設計 中 "結對程式設計讓兩個人所寫的代碼不斷地處于“複審”的過程,程式員們能夠不斷地稽核,提高設計和編碼品質,可以及時發現并解決問題,避免把問題拖到後面的階段去。 "

在本次個人作業中,我與同學也曾為了實作一個功能連着麥修改了一下午的代碼,我想這也算是結對程式設計吧,但是在這個過程中我發現如果有一方主導意識較強,就容易将問題帶入一個死結。且在這個過程中我們還産生了分歧,那麼這個時候是否應該結束結對程式設計,各自實作自己的想法呢?還是兩人應當按順序,一起先嘗試其中一個人的想法,再一起嘗試另一個思路,然後對比取更優呢?

3.2 軟體工程師的職業發展 "邁克康奈爾把工程師分為8個級别(8—15),一個工程師要從一個級别升到另一個級别,

需要在各方面達到一定的要求。例如,要達到12級,工程師必須在三個知識領域

達到“帶頭人”水準。例如要到達“工程管理(知識領域)的熟練(能力)”水準,工程師必須要做到以下幾點。閱讀: 4—6個經典文獻的深入分析和閱讀工作經驗: 要參與并完成6個具體的項目課程: 要參加3個專門的課程有些級别"

到目前為止我看過的關于程式設計的書屈指可數,在學習新技術的時候我偏向于在網絡上學習,畢竟網絡上的技術文章是最新的,那麼,閱讀經典文獻的必要性在哪呢?

5.2.1 主治醫生模式 "在一些學校裡,軟體工程的團隊模式往往從這一模式退化為“一個學生幹活,其餘學生跟着打醬油”"

這種情況的确很常見,但是如果在其他學生的水準都較低,對于那個水準高的學生來說,自己完成比教會他們再與他們合作效率不是高多了嗎? 但這種模式也是合理的吧,特别是對于高年級學生來說,如果參加競賽,對于隊伍中的新生,不就應該帶他們嗎?

11.5.1 閉門造車 "小飛:我今天真失敗!在辦公室裡坐了10個小時,但是真正能花在開發工作上的

時間可能隻有3個

小時,然後我的工作進展大概隻有兩個小時!

阿超:那你的時間都花到哪裡去了?"

對于這個問題我深有體會,在完成個人項目的過程中,我常常一坐就是一整天,對着一個bug能改一個下午,但其實隻是一個很小的錯誤,就很容易陷入這樣的迷惑中,不獨處呢,很難進入狀态工作,一個人呢,又會發散了思維,那我以後去公司裡工作該怎麼辦呢

16.1 創新的迷思 "最近幾年,我們整個社會似乎對創新很感興趣,媒體上充斥了創新型的人才、創新型的學校、創新型的公司、創新型的城市、創新型的社會,等等名詞。有些城市還把“創新”當作城市的精神之一,還有城市要批量生産上千名頂級創新人才。"

一直有聽說前輩創業的事迹,但在進入專業學習了三年,我發現創新并不是那麼容易的,你想實作的功能早就有人實作了并且已經失敗了,甚至找不到創新的方向,那些自稱創新型的事物,是否誇大其詞了。還有就是對于那些熱門的新技術方向,真正接觸了發現你能夠聽到的新技術,其實已經有許多先行者了。

​ 項目位址

Personal Software Process Stages 預估耗時(分鐘) 實際耗時(分鐘)
計劃
預估這個任務需要多少時間 20 30
開發
需求分析(包括學習新技術) 240 200
生成設計文檔 30min 20min
設計複審 15min
代碼規範 40min
具體設計 60min
具體編碼 1000min 1200min
代碼複審 120min 600min
測試
測試報告
計算工作量
事後總結,并提出過程改進計劃 10min
總和 1665min 2215min

​ codestyle

  • 第一階段:複習git,複習java文法,編寫了我的代碼規範
這一階段的任務由于在寒假就有複習,是以進行的比較快。同時還根據《碼出高效_阿裡巴巴Java開發手冊》結合我本人習慣,編寫我的代碼規範。由于之前未使用過GithubDesktop,在這個階段也安裝下載下傳,并學習了如何使用。
           
  • 第二階段:
    • 程式需求分析:
      • 擷取檔案輸入
      • 統計檔案ascii碼字元數
      • 統計符合規則的單詞數
      • 統計檔案的有效行數
      • 統計出現次數最多的單詞及出現次數(輸出前十)
      • 輸出結果到檔案
  • 我的類結構:
    • WordCount
      • main
    • Lib
      • readTxt
    • outputToTxt
      • countChar
      • countLine
      • sortHashMap
      • findLegal
      • countWordNum
  • 解題思路:
  1. 檔案輸入
    最開始我打算用BufferedReader去處理檔案輸入,通過readLine()方法一次讀取一行,然後将讀取的字元串用換行符"\n"拼接起來。但在測試中發現,讀出的文本的ASCII碼比預期的少,又回去仔細閱讀了題目,發現文本中的換行并不是簡單的"\n",還有"\r"、"\r\n"這種情況,是以如果用readLine()可能會使得文本字元變少。通過百度查找資料後,我決定采用BufferedInputStream的read()函數來讀取,一次緩沖10m的文本。關鍵代碼如下:
    byte[] bytes = new byte[BUFF_SIZE];
      int len;
      while((len=bufferedInputStream.read(bytes)) != -1){
          stringBuffer.append(new String(bytes, 0,len,StandardCharsets.UTF_8));
      }
      ...         
      }
      catch (IOException e){
          ...
      }
      return stringBuffer.toString();
               
  2. 統計檔案ASCII碼字元數
    一開始沒認真審題,将問題複雜化了,通過正則比對去統計。
    String regex = "\\p{ASCII}";
    Pattern pattern = Pattern.compile(regex);
    Matcher matcher = pattern.matcher(text);
    while (matcher.find()){
        num++;
    }
               
    後來發現由于題目說明給定的文本都是ASCII字元,是以隻需傳回讀取檔案的字元串的長度即可。
    • 單詞的規則:至少以4個英文字母開頭,跟上字母數字元号,不區分大小寫
    • 我想到的辦法是先将文本用split方法分隔開,分隔用的正規表達式為:

      [^ A-Za-z0-9_]|\\s+

      ,得到一個不含分隔符的字元串數組。再用一次循環用正則'[1]{4,}.*'去判斷是否為合法單詞。

      将文本分割:

    public static String[] splitWord(String text){
        String[] words;
        String regexForSplit = "[^ A-Za-z0-9_]|\\s+";       
        words = text.split(regexForSplit);
        return words;
    }		
               
    • 判斷是否合法單詞
    public static  List<String> splitLegalWord(String[] words){
    	List<String> legalWords = new ArrayList<String>();
    	String regexForMatch = "^[a-zA-Z]{4,}.*";
    	for(int i=0 ; i<words.length; i++){
    		if(words[i].matches(regexForMatch)){
    			legalWords.add(words[i].toLowerCase());
      		}
    	}
    	return legalWords;
    }
               
  3. 用正規表達式去比對空白字元(三種),然後用split将文本分割,就得到了每個字元串為一行的字元串數組,然後再周遊過程中判斷是否空行,這裡用trim是為了防止含有空格或tab制表符的無效行被視作有效行算入行數中。
    String[] lines = text.split("\r\n|\r|\n");
    for(int i=0; i<lines.length; i++){
    	if (!lines[i].trim().isEmpty()){
    		num++;
    	}
    }
               
将存放單詞和單詞出現次數的hashMap轉換為list,然後對list進行排序,重寫compare方法使得排序依據:從大到小排序,頻率相同的單詞,優先輸出字典序靠前的單詞,選取前十條記錄。
public static List<Map.Entry<String, Integer>> sortHashMap(HashMap<String, Integer> hashMap){
    Set<Map.Entry<String, Integer>> entry = hashMap.entrySet();
    List<Map.Entry<String, Integer>> list = new ArrayList<Map.Entry<String, Integer>>(entry);
    Collections.sort(list, new Comparator<Map.Entry<String, Integer>>() {
        @Override
        public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
            if(o1.getValue().equals(o2.getValue())){
                return o1.getKey().compareTo(o2.getKey());
            }
            return o2.getValue()-o1.getValue();
        }
    });
    //最多隻取10條
    if(list.size()>10) {
        return list.subList(0,10);
    }
    else{
        return list;
    }
}
           
簡單地用BufferedWriteer按行寫入到檔案中。
bufferedWriter.write("characters:"+num1+"\r\n");
  bufferedWriter.write("words:"+num2+"\r\n");
  bufferedWriter.write("lines:"+line+"\r\n");
  for(int i=0; i<list.size()&&i<10; i++){
      String key = list.get(i).getKey();
      Integer value = list.get(i).getValue();
      bufferedWriter.write(key+":"+value+"\r\n");
  }  
           

  • 初次性能測試:

    用于測試的文本大小為:95.3mb(100,000,000 位元組,用來測試的檔案由該檔案[GenerateText][]随機生成) 需要的運作時間為:54834ms 這個數字着實吓了我一跳,因為其他人的運作時間是遠遠低于我的。 使用了緩沖區後 改進時間為50212ms。

    軟工實踐寒假作業 (2/2)
  • 算法優化

    在對執行各個子產品的時間分析後,我發現耗時最高的是計算單詞數以及統計詞頻,由于算法不當以及一開始對各個方法獨立性的錯誤追求,在拆分單詞處進行了重複計算,在和洋藝同學交流後發現其實計算單詞的時候就可以用hashMap記錄詞頻的

    • 改進前:先用split方法提取出獨立的字元串,存放在字元串數組中,然後再用 "[2]{4,}.*" 去比對每一個字元串,用List 存放合法的單詞。用的是matches方法。據星源同學所說這兩個方法效率極低,否則我也沒意識到在這個地方有什麼可以改進的地方,非常感謝他555。
    • 改進後:直接對文本字元串進行比對,通過Matcher的find方法取出符合規則的單詞,并且統計單詞的出現次數,存放到HashMap<String, Integer>裡。不過這樣的正規表達式比較複雜,而且需要在文本字元串開頭添加一個空白字元。正規表達式:"([^ a-z0-9])([a-z]{4}[a-z0-9]*)"。關鍵代碼:
      Pattern pattern = Pattern.compile(regex);
          Matcher matcher = pattern.matcher(text);
          HashMap<String, Integer> legalWords = new HashMap<String, Integer>();
          
          //直接把單詞和出現頻率一起做了,放到hashMap裡
          while(matcher.find()){
              wordNum++;
              String tmp = matcher.group(2).trim();
              if(!legalWords.containsKey(tmp)){
                  legalWords.put(tmp,1);
              }
              else{
                  legalWords.put(tmp, legalWords.get(tmp)+1);
              }
          }
            
            return legalWords;
          }
                 
      • 對CountLine進行了優化,畢竟用split方法實在是太耗時且占用記憶體了,我用大小為476 MB (500,000,000 位元組)的文本去跑,程式直接崩潰了,原因是堆溢出。在經過百度以及和鄒洋藝同學探讨過後,決定采用正則比對來計算行數。
        public static int countLine(String text){
            int num=0;
            String regex = "(.*)(\\s)";
            Matcher matcher = regexUtils(regex, text);
            while (matcher.find()){
                String tmp = matcher.group(1);
                if(!tmp.trim().isEmpty()){
                    num++;
                }
            }
            return num;
        }
                   
  • 多線程執行:

    ​ 發現統計單詞詞頻和計算行數這兩個工作并不重複,而且耗時也都比較長,是以想到是否可以通過多線程來執行這兩個任務。由于兩個方法都需要傳回值,是以實作的是Callabel接口

    Callable callable1 = new Callable() {
        HashMap<String, Integer> legalWords;
        @Override
        public HashMap<String, Integer> call() {
            long startTime1 = System.currentTimeMillis();
            try {
                Thread.sleep(5);
            }catch (Exception e){
                e.printStackTrace();
            }
            this.legalWords = SplitWord.findLegal(content);
            countDownLatch.countDown();
            long endTime1 = System.currentTimeMillis();
            System.out.println("線程1運作時間:"+(endTime1-startTime1)+"ms");
    
            return legalWords;
        }
    };
    futureTask1 = new FutureTask<HashMap<String, Integer>>(callable1);
    new Thread(futureTask1).start();     //執行線程
               
    Callable callable2 = new Callable() {
                Integer line;
                @Override
                public Integer call() {
                    long startTime2 = System.currentTimeMillis();
                    line = CountLine.countLine(content);
                    countDownLatch.countDown();
                    long endTime2 = System.currentTimeMillis();
                    System.out.println("線程2運作時間:"+(endTime2-startTime2)+"ms");
                    return line;
                }
            };
            futureTask2 = new FutureTask<Integer>(callable2);
            new Thread(futureTask2).start();
               
    coutDownLatch使主線程等待兩個子線程都完成後才能繼續執行。
    軟工實踐寒假作業 (2/2)
    軟工實踐寒假作業 (2/2)

    可以看到,兩個線程是并行的且主線程等兩個線程都執行完畢才繼續執行。

    性能改進後,測試95.3mb(100,000,000 位元組)的檔案,所需要的時間為:7053ms,相對于優化之前的50000多ms有了相當大的改進。

  • 最開始的單元測試是很笨的通過main方法,寫好方法後在main中調用,并對方法的運作時間進行記錄。後來得知可以使用JUnit插件來進行單元測試,單元測試就變得簡單友善多了,也更加有針對性。在程式設計過程中,我進行了多次的單元測試,在確定功能正常後才進行下一步。

    ​ 1.字元統計

    @Test
    public void countChar() {
        String content = ReadTxt.readTxt(path+"input7.txt");
        long startTime1 = System.currentTimeMillis();
        System.out.println(CountAsciiChar.countChar(content));
        long endTime1 = System.currentTimeMillis();
        System.out.println("計算ASCII時間:"+(endTime1-startTime1)+"ms");
    }
               
    1. 單詞計算
    @org.junit.Test
    public void countWordNum() {
        num = CountWord.countWordNum(text);
        System.out.println(num);
    }
               
    1. 統計頻率
    @Test
        public void sortHashMap() {
            list =  CountFrequency.sortHashMap(legalWords);
            for(int i=0; i<list.size()&&i<10; i++){
                String key = list.get(i).getKey();
              Integer value = list.get(i).getValue();
                System.out.println(key+":"+value+"\r\n");
            }
        }
               
  • 代碼覆寫率測試

    我的覆寫率情況為:類的覆寫率為100%,方法的覆寫率為100%,代碼行覆寫率為88%。

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

程式中主要會出現的異常是檔案操作以及指令行輸入指令的錯誤。
在檔案操作的相應代碼中都添加了try catch結構來捕獲異常
           

  • 心路曆程

​ 早就聽聞軟體工程實踐的大名,真正上這門課的時候确實有害怕,怕自己程式設計能力太差,就和我一直對參加競賽有着莫名的恐懼一樣,害怕自己能力不足,無法在規定的時間内完成任務,特别是在規定時間内要完成新技術的學習,然後馬上投入應用。但是既然開始了,那也就隻能克服恐懼了。

​ 本次作業是個人程式設計任務,看到題目的時候我有點欣喜又有點迷茫,欣喜是因為這和我們以前編過的程式功能沒什麼本質差別,迷茫則是作業要求裡又有許多我沒接觸過的東西,什麼 單元測試、性能改進,什麼叫單元測試?又怎麼去分析程式的性能呢?

剛開始的時候手忙腳亂的,雖然以前也用過git,但是并不熟練,隻會簡單的pull和push,于是我的commit記錄裡就多了一條送出測試hhhhh,這還導緻我最後多commit了2次來删去之前多送出的檔案。

但随着一個功能一個功能的實作我開始進入狀态了,連續好幾天都是從早上坐到晚上,有些代碼寫了改,改了删,還會有些莫名其妙的bug,記得最深刻的是BufferedStream的read函數的一個用法錯誤,導緻我讀取的文本内容産生了差錯,然後那個bug我從早上改到晚上,就是沒有改出來,最後發現是沒有指定每次從緩沖區讀取的長度,改出來的時候差點從椅子上跳起來哈哈哈哈哈,興奮又懊惱,懊惱自己怎麼會犯這麼低級的錯誤,而且效率還這麼低。 我反思了一下,是因為缺少合理的休息,一直坐在電腦前是效率底下的,coding期間必須合理地休息。

在初次實作了功能後,我以為自己的程式很可以了...直到運作了大檔案,我發現我的程式崩潰了...于是接下來就是各種百度,還有和同學交流探讨,針對計算單詞這個功能,我甚至和洋藝同學連着麥,我倆邊交流改了一下午才改出來。看見其他同學的方法效率比我的高我就忍不住想問問如何實作的,但又怕被算作抄襲,同時時間也來不及了,就沒有更深的改進。

最後完成的時候感覺心裡放下了一塊大石頭

  • 收獲&不足
  1. 提高了自己的程式設計水準,抗壓力能力也變高了不少,畢竟這幾天頭發沒少掉,熬夜也是每天。
  2. 熟練使用git和gitdesktop,為以後的團隊合作打下基礎
  3. 效率過低,一個小bug由于思維誤區改了一下午
  4. 沒做好充分的準備就開始編寫代碼,導緻代碼複審和性能分析的時候發現自己的算法過于差勁。其實應該在動手編寫代碼之前先找到最佳的算法,然後再動手實作。
  1. a-zA-Z ↩︎