天天看點

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

這個作業屬于哪個課程 2021春軟體工程實踐S班
這個作業要求在哪裡 作業要求
這個作業的目标 進一步閱讀《建構之法》提出問題,學習使用github,制定自己的代碼規範,編寫詞頻統計程式
其他參考文獻 CSDN、部落格園...

建構之法提問

問題1——如何避免過早地優化?

書53頁:

過早優化:一個工程師在寫程式的時候,經常容易在某一個局部問題上陷進去,花大量時間對其進行優化;無視這個子產品對全局的重要性,甚至還不知道這個“全局”是怎麼樣的。這個毛病早就被歸納為“過早的優化是一切罪惡的根源”。

在網上搜尋資料後得到一段話:優化之前先問三個問題,優化後代碼量會減少嗎?優化的是系統瓶頸嗎?優化方案中,是否增加了隐藏條件和系統限制?

在這次的實際編碼中,我也出現了這種過早優化的情況。當我在編寫統計字元的時候,我發現我的讀入檔案非常緩慢,于是我去修改了IO流,然後IO流中的讀入方法因為正确性又修改了一次,導緻統計字元的功能的完成被拖後了很多。我認為在之後的優化過程中,先問自己這三個問題能夠避免一部分的過早優化。

問題2——是否應該使用goto?

書75頁:

goto

函數最好有單一的出口,為了達到這一目的,可以使用goto。隻要有助于程式邏輯的清晰展現,什麼方法都可以使用,包括goto。

我認為不應該使用goto,因為現在的許多進階語言都是結構化,子產品化,如果使用goto在其中,必然破壞這種結構,導緻程式更加難讀且危險。

網上支援不支援的人都有,支援的人認為goto可以設計一個統一出口,簡化代碼。不支援的人認為goto容易把邏輯弄亂且難以了解。我個人還是認為不該使用goto,一旦代碼量增多,維護這個統一出口也容易出現疏漏,進而導緻的邏輯混亂則一個大項目是更加不可承受的。

問題3——結對程式設計兩人水準相差較大應該怎麼協調?

書85頁

在結對程式設計模式下,一對程式員肩并肩、平等地、互補地進行開發工作。

我認為在學校這種環境下,有一個能跟你互補的程式員同學是一件比較小機率的事件,但兩人水準差距大确實一個會出現的情況,這樣兩人在分工和交流上就會産生一定的隔閡。此前的一些合作項目,我都是在同學的帶領之下學習相應知識然後完成配置設定給我的代碼,感覺偏向任務驅動型,而展現不出結對程式設計的優勢。

查閱了網絡的資料發現,在這種強弱結對的情況下,通常類似于公司進階開發人員帶新人,這種情況下,需要進階開發人員培養新人的信心和士氣,讓新人快速融入團隊。是以我認為在學校的場景下,較強的同學帶着較弱的同學一起學習一起共同完成工作能比較好地适應結對和團隊程式設計。

問題4——如何提升使用者填問卷的積極性和真實性?

書163頁

使用者調查問卷

這種方式是向使用者提供事先設計好的問題,讓使用者回答。有時候使用者在浏覽某個網站時,一個彈窗會跳出來,打斷使用者的思路,不客氣地要求使用者回答幾個問題。使用者在回答這類問題時,是否會心不在焉,亂點一氣?

在大學的日常生活中,經常有其他同學在朋友圈或者空間發填寫調查問卷的連結,來号召朋友幫忙填寫問卷,但去除朋友關系的作用,會有什麼可以吸引我們的調查對象來填寫問卷而且保證問卷真實呢?

查閱網絡資料得到如下建議:

  1. 給予被調查者一定獎勵激發參加的積極性。
  2. 強調調研的重要性,讓使用者有一種參與感和成就感。
  3. 明确調查目的和内容,以此為基礎設計問卷。

問題5——為什麼好的設計不一定赢?

書350頁

如果使用QWERTY鍵盤,那麼隻有10%的英語單詞能在手指不離開鍵盤中間行的情況下敲出來。但是如果使用Dvorak鍵盤,你可以在鍵盤中間行打出60%的常用單詞!這樣會減輕手指和相關肌肉的負擔,減少勞損,同時加快打字速度。

人類在許多方面都在追求更高更好的性能,但在鍵盤上,高頻字母更集中,效率更高的d鍵盤卻在市場表現上遠不如大衆使用的q鍵盤,為什麼會出現這樣的情況?

查詢網際網路可知,d鍵盤在q鍵盤盛行的時候的學習成本太高,或是因為人的惰性,但我疑惑的是如果d鍵盤效率真有那麼好為什麼沒有後發優勢進而替代q鍵盤?

附加題——曆史上第一個病毒

我是爬行者

爬行者的程式(Creeper),每一次把它讀出時,它便自己複制一個副本。此外,它也會從一部電腦“爬”到另一部與其連網的電腦。很快地電腦中原有資料便被這些爬行者擠掉了。爬行者的唯一生存目地是繁殖。

爬行者的目标

Creeper由BBN Technologies 的開發人員Bob Thomas 建立,BBN Technologies處于新興技術行業的前沿。托馬斯程式設計爬行器測試是否可以建立一個程式在計算機之間移動。換句話說,他的想法并不是破壞個人電腦,實際上,僅僅幾年之後,Creeper才認為是病毒,因為直到80年代這個概念才被應用到電腦上。

爬行者通過ARPANET (美國國防部使用的第一個計算機網絡之一)傳播并複制到系統中,在那裡顯示我在本文開頭寫的資訊。一旦顯示,它就開始列印檔案,但随後停止并切換到另一台PC,并進行相同的處理。

盡管它在第一次出現時感染了電腦,但效果并沒有持續太久:跳到下一台電腦,它從前一台電腦中消失,依此類推。

今天,這可能看起來像一個失敗,但在1971年,鮑勃托馬斯的實驗得到了很多關注,因為在沒有人為幹預的情況下,獲得程式在PC之間跳轉的壯舉是不可能實作的。他的成功是建立了一個從個人電腦到個人電腦快速運作的程式...... 并且是建立第一個防病毒 程式的原因!

來源于世界上公認的第一個電腦病毒爬行者Creeper

WordCount程式

Github項目位址

Github

PSP表格

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

解題思路描述

詞頻統計可以大緻分為下列子產品

  • 檔案讀取
    傳入輸入檔案路徑的String,傳回BufferedReader或者Stream
  • 處理文檔
    将檔案按行讀取,存在字元串數組中,可以減少後續多次讀檔案的時間。
    • 統計檔案字元

      周遊字元串數組,然後判斷每個字元是否在ASCII碼的範圍内,累加符合ASCII碼的字元數

      但後續Q&A中看到不會出現非法字元,就采用累加字元串長度的方式。

    • 統計單詞總數
      切分字元串,分離出每個詞,再判斷詞語的格式,符合格式要求則累加單詞總數。
    • 統計有效行數
      周遊字元串數組中的每一項 (每一項都是一行),再判斷是否空行,累加非空行數。
    • 統計單詞出現次數
      采用

      Map<String, Integer>

      存儲單詞和頻數,後續排序首先按照頻數再按字典序排即可符合要求

      最後取出排序後的前10個單詞和頻數。

  • 結果輸出到檔案
    傳入輸出檔案的路徑和要輸出的資訊字元串,将字元串輸出到指定路徑。

代碼規範連結

程式設計與實作過程

類設計

FileIOUtil--檔案IO操作類

public static class FileIOUtil {
    //讀入檔案
    public static BufferedReader readFile(String filePath)
    //寫入檔案
    public static void writeFile(String targetPath, String msg)
}
           

TextEditor--檔案String處理類

public static class TextEditor {
    //帶參構造函數
    public TextEditor(BufferedReader reader)
    //檔案讀入字元串
    public void readString()
    //統計字元數
    public int countAscii()
    //判斷有效單詞
    public static boolean validate(String word)
    //統計單詞數
    public int countWords()
    //統計top10單詞
    public String countTopWords()
    //統計行數
    public int countLines()
}
           

Core--核心接口類

public static class Core {
    //統計獲得字元數
    public static int getCharsNum(TextEditor te)
    //統計獲得單詞數
    public static int getWordsNum(TextEditor te)
    //統計獲得top10單詞的字元串
    public static String getTopWords(TextEditor te)
    //統計獲得行數
    public static int getLinesNum(TextEditor te)
}
           

WordCount

設定一些變量用來存儲統計的結果

public class WordCount {
    int charsNum, wordsNum, linesNum;
    String topsStr = "";
    
    // 輸入輸出檔案路徑參數 采用args[]傳入讀取
    public static void main(String args[])
}
           

IO流選擇與實作

選擇:

因為是字元檔案處理,是以選擇了

BufferedReader

起初認為采用按行讀就能較好地完成讀入檔案的功能,選取了readline()方法,但在之後的字元串進行中發現readline()不能讀到每行最後的換行符。是以改用了它的read()方法,改為按字元讀入,雖然效率低與readline(),但是解決了換行符讀入的問題。

實作:

​ reader為TextEditor建立時傳入的BufferedReader對象

​ 當讀到字元為

\n

時把目前字元串加入List,再清空。

​ reader讀到末尾時退出循環,由于檔案末尾沒有

\n

換行符,是以判斷目前字元串是否為空,若非空則加入List,再清空stringBuilder。

public static BufferedReader readFile(String filePath) {
    File file = new File(filePath);
    FileInputStream inputStream = null;
    InputStreamReader inputStreamReader = null;
    BufferedReader bufferedReader = null;
    try {
        inputStream = new FileInputStream(file);
        inputStreamReader = new InputStreamReader(inputStream);
        bufferedReader = new BufferedReader(inputStreamReader);
    } catch (FileNotFoundException e) {
        System.out.println("找不到檔案路徑");
        System.out.println("目前路徑"+System.getProperty("user.dir"));
        e.printStackTrace();
    }
    return bufferedReader;
}
           

資料結構選擇

從檔案讀入的字元串,按照換行符區分按行存入

List<String>

而單詞的存儲,由于要進行詞頻統計,那麼map是一個非常不錯的選擇,可以友善地進行詞頻統計,于是采用了

HashMap<String, Integer>

資料處理實作

  1. 統計字元數

    由于輸入中不會出現非法字元,是以将已經讀入的strings的每行長度 (等于字元數) 累加。

    public int countAscii() {
        int sum = 0;
        for (int i = 0; i < strings.size(); i++) {
            sum += strings.get(i).length();
        }
        return sum;
    }
               
  2. 統計單詞數&行數

    因為沒有獨立出統計行數的需求,是以在統計單詞的同時也統計非空行數,可以少周遊一次strings數組。

    統計單詞時将每一行,使用正規表達式

    \W

    比對非單詞數字的字元,再使用spilt函數切分字元串得到待驗證的單詞存入arr,再依次驗證arr中的各項是否是有效單詞 (使用

    validate(String)

    函數),驗證通過累加單詞數。統計行數時,先用

    trim()

    去除其中空格,再判斷字元串是否為空。即可累加得到總行數。
    //統計單詞數&行數
    public int countWords() {
        String word;
        int linesSum = 0;
        int wordsSum = 0;
        for (int i = 0; i < strings.size(); i++) {
            //\W :比對任何非單詞數字字元,等價于 [^A-Z a-z 0-9_]
            String str = strings.get(i);
            String[] arr = str.split("\\W");
            for (String s : arr) {
                if (validate(s)) {
                    wordsSum++;
                }
            }
            if (!str.trim().isEmpty())
                linesSum++;
        }
        lines = linesSum;
        return wordsSum;
    }
               
    驗證單詞有效性,首先先判斷長度,在滿足長度要求情況下截取前四個字元,再利用正規表達式判斷前四個字元中不含數字,若都滿足則單詞驗證通過。
    //驗證單詞有效性
    public static boolean validate(String word) {
        boolean flag = true;
        final int MIN_LENGTH = 4;
        if (word.length() < MIN_LENGTH) {
            //                System.out.println("單詞長度小于" + MIN_LENGTH);
            return false;
        }
        String str = word.substring(0,4);
        Pattern p = Pattern.compile(".*\\d+.*");
        Matcher m = p.matcher(str);
        if (m.matches()) {
            flag = false;   //若有數字則是無效單詞
        }
        //            System.out.println(str + "是否是有效單詞" + flag);
        return flag;
    }
               
  3. 統計Top10單詞

    統計詞頻時,先建構單詞和詞頻的map,再把map中的entrySet加入list,然後利用list的sort函數重載比較函數進行自定義排序。

    之後利用StringBuilder将list的前十個單詞和詞頻轉成字元串并傳回。

    1. 将單詞轉成小寫,然後檢測是否為合法單詞,若合法再判斷map是否已經包含該單詞,若不含該單詞則放入map并設頻數為1;若已存在則将頻數加1
    2. 将map放入List,以便調用sort函數。調用sort時重寫比較方法。最後建構需要的Top10字元串
    HashMap<String, Integer> words = new HashMap<String, Integer>();
    List<Map.Entry<String, Integer>> list = new ArrayList<>();
    public String countTopWords() {
        String word;
        int sum = 0;
        //将單詞以及頻數記錄進map
        for (int i = 0; i < strings.size(); i++) {
            // \w :比對包括下劃線的任何單詞字元,等價于 [A-Z a-z 0-9_]
            // \W :比對任何非單詞字元,等價于 [^A-Z a-z 0-9_]
            String[] arr = strings.get(i).split("\\W");
            for (String str : arr) {
                word = str.toLowerCase();   //轉小寫
                if (validate(word)) {
                    if (!words.containsKey(word)) {
                        words.put(word, 1);
                    } else {
                        int times = words.get(word) + 1;
                        words.remove(word);
                        words.put(word, times);
                    }
                }
            }
        }
        //将map内容存入List等待排序
        for (Map.Entry<String,Integer> entry : words.entrySet()) {
            list.add(entry);
        }
        //執行List的sort方法,并重寫比較函數
        list.sort(new Comparator<Map.Entry<String, Integer>>() {
            @Override
            public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
                if (o2.getValue().compareTo(o1.getValue()) == 0) {
                    return o2.getKey().compareTo(o1.getKey()) * -1;
                } else  {
                    return o2.getValue() - o1.getValue();
                }
            }
        });
        //輸出TOP10字元串的過程
        stringBuilder.delete(0, stringBuilder.length());
        for (int i = 0; i < Math.min(TOP_NUM, list.size()); i++) {
            stringBuilder.append(list.get(i).getKey()).append(": ").append(list.get(i).getValue()).append('\n');
        }
        return stringBuilder.toString();
    }
               

性能改進

采用帶緩沖的BufferedReader,BufferedWriter

帶有緩沖的BufferedReader會從實體流中一次性讀取8k個字元,會盡量提取比目前操作更多的字元。

使用StringBuilder替換部分String

參考資料 String與StringBuilder的差別

此前在readString()方法中大量使用String,又不斷對String進行指派和修改操作。導緻後來的程式運作異常緩慢,後續查找資料了解到String和StringBuilder的差別。

  • String是字元串常量,是以值是不可變的,導緻每次對String操作效率低下同時也耗費額外的記憶體空間。

    例如,初值為"hello"的字元串str,在進行

    str += " world";

    時,會建立一個新的String值為"hello world",就把一個記憶體空間變成了三倍于原來的記憶體。
  • StringBuilder是字元串變量,該對象能夠被多次修改,而不需要額外的記憶體空間。

是以可以把String用StringBuilder替換能大大提高代碼性能。

public void readString() {
    stringBuilder = new StringBuilder();
    int value;
    try {
        while ((value = reader.read()) != -1) {
            char ch = (char)value;
            stringBuilder.append(ch);	//原先采用 string += ch;的方式進行字元串拼接操作
            if (ch == '\n') {
                strings.add(stringBuilder.toString());
                stringBuilder.delete(0, stringBuilder.length());	//替換了原先的string指派操作
            }
        }
        if (stringBuilder != null) {
            strings.add(stringBuilder.toString());
            stringBuilder.delete(0, stringBuilder.length());	//替換了原先的string指派操作
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}
           

多線程并行計算

将字元統計、單詞統計、詞頻統計分為三個線程進行,充分利用性能。

charThread.start();
wordsThread.start();
topThread.start();
           

測試資料:字元數

5e8

行數

1e6

大小

523MB

  1. 未使用多線程

耗時: 1分34秒

  1. 啟用多線程
軟工實踐寒假作業(2/2)

耗時: 1分5秒

單元測試

單元測試采用JUnit4進行,單元測試中檔案路徑為友善調試采用寫死,主程式檔案路徑是采用args參數讀入。

Core類測試

  1. 測試統計字元數

    要求:能正确統計所有ASCII碼範圍内的字元。

    樣例包含: 大小寫英文、數字、空格、換行符以及部分符号。

    @Test
    public void testGetCharsNum() throws Exception {
        String path = "MyTest1.txt";
        String str = "hjkl123ACVjkl^^, ;abcd\nsz\r";
        Lib.FileIOUtil.writeFile(path,str);
        BufferedReader br = new BufferedReader(new FileReader(path));
        Lib.TextEditor te = new Lib.TextEditor(br);
        te.readString();
        assertEquals(str.length(), Lib.Core.getCharsNum(te));
    }
               
  2. 測試統計單詞數

    要求:統計所有長度4個字元以上且前四個字元不含數字的單詞。

    樣例包含:相同單詞、長度小于4的字元串、單詞的大小寫不同形式、前四個字元中含數字的字元串和穿插其中的換行符和符号。

    @Test
    public void testGetWordsNum() throws Exception {
        String path = "MyTest2.txt";
        BufferedWriter bw = new BufferedWriter(new FileWriter(path));
        StringBuilder builder = new StringBuilder();
        //包含相同單詞、大小寫形式單詞以及數字穿插在單詞中 以及換行
        builder.append("abandon ").append("abandon ");
        builder.append("abc12|3 ");
        builder.append("abcd123 ");
        builder.append("abc) \n");
        builder.append("123abc 1234565 ");
        builder.append("FILE file File ");
        bw.write(builder.toString());
        bw.close();
        BufferedReader br = new BufferedReader(new FileReader(path));
        Lib.TextEditor te = new Lib.TextEditor(br);
        te.readString();
        int result = Lib.Core.getWordsNum(te);
        assertEquals(6, result);
    }
               
  3. 測試統計詞頻

    要求:能正确将合法單詞按詞頻和字典序排序。

    樣例包含:非法單詞、數字、不同字典序單詞、大小寫混合單詞。

    @Test
    public void testGetTopWordsNum() throws Exception {
        String path = "MyTest3.txt";
        BufferedWriter bw = new BufferedWriter(new FileWriter(path));
        //包含相同單詞、大小寫形式單詞 同時混亂順序
        String str = "halo yep 123 StacK raW onetwothree afk PrOGrAm JavA jAVa pyThon premier Thor StarBucks Zofia ARUNI";
        bw.write(str);
        bw.close();
        BufferedReader br = new BufferedReader(new FileReader(path));
        Lib.TextEditor te = new Lib.TextEditor(br);
        te.readString();
        String result = Lib.Core.getTopWords(te);
        String ans = "java: 2\naruni: 1\nhalo: 1\nonetwothree: 1\npremier: 1\nprogram: 1\npython: 1\nstack: 1\nstarbucks: 1\nthor: 1\n";
        assertEquals(ans, result);
    }
               
  4. 其他測試
    軟工實踐寒假作業(2/2)
  5. 單元測試覆寫率截圖
    軟工實踐寒假作業(2/2)
    對于93%的行都有覆寫到,剩餘的一小部分為一些異常的catch塊。

計算子產品部分異常處理

一些基礎IO流的try..catch..,對catch塊進行了一些輸出,友善之後确定位置。

心路曆程與收獲

  • 這次的作業相比以往的大作業有許多的不同之處,讓我從更多的角度更深入地了解了軟體工程這門學科。
  • 首先是通過使用github來進行代碼同步以及版本管理,此前的大作業中,基本沒有進行版本管理,就導緻再出現一些故障需要版本回退的時候變得非常棘手,但使用git之後便可以靈活的進行版本控制,解決了此前的問題。
  • 其次,這次的程式在性能調試上也花費了比以往更多的精力,優化前統計一個大檔案經常卡死,沒法順利執行,但經過自己和查詢資料的一些分析之後,修改了相應位置後提升性能,讓程式能執行更大的檔案,也感受到了不一樣的成就感。
  • 單元測試也是第一次使用,通過這種測試代碼來檢查自己代碼的備援程度和正确性确實是一種非常高效且實用的手段,能夠更好地保證我們代碼的品質。
  • 在之後的作業中,也希望自己能進一步提升自己的代碼水準和相應工具 (git、單元測試...)的使用水準