軟工實踐寒假作業2/2



作業基本信息

這個作業屬於哪個課程 2021春軟件工程實踐|W班(福州大學)
這個作業要求在哪里 寒假作業2/2
這個作業的目標 學習《構建之法》、邊完成詞頻統計作業邊學習相關知識
其他參考文獻 《構建之法》

目錄:


一、我對《構建之法》的幾個疑問

1.關於代碼規范的一個疑問

對於第四章4.2中的4.2.4 “斷行與空白的{}行” 中提到的標准我表示不贊同。

雖然代碼規范因人而異,書中提到格式C不夠清晰,進而選擇更清晰的格式D。
格式C
格式D

本人以前也常用格式D,但是后面改成了格式C,因為很多的語言書中都采用了格式C,並且格式C也屬於現在大家比較公認規范的一個標准,以下兩個例子一個是網上找的規范的例子,另一個是我用IDEA自動生成的代碼,兩種都用的是格式C,如果格式C不好的話,為什么IDEA的自動生成要用格式C而不是格式D呢?所以我認為格式C才是更好的標准,格式D過於發散。
示例1
示例2

2.‘從用戶的角度考慮問題’具體可以有什么角度?

第十二章12.1中的12.1.2 “從用戶的角度考慮問題” 中提到了 “設計不同於傳統的數學題,是沒有唯一的標准答案的”。后又舉了郵箱地址、翻譯等例子,但是看完后這些內容我只知道要從用戶的角度考慮問題,我還是不知道具體要怎么考慮。

在搜索資料的同時,我發現書后12.3評價標准中作者的總結解決了我的大部分疑惑。作者列舉了“盡快提供可感觸的反饋系統狀態”、“用戶有控制權”、“一致性和標准化”等原則,讓我對這個問題有了比較清楚的認識。
其中兩個原則

除了作者自身總結,我還通過網上搜索,發現了更多的考慮角度。簡單來說,就是控制感、歸屬感、驚喜感、沉浸感。
控制感:給予用戶控制感,讓用戶做用戶想做的事情就是好的用戶體驗。
歸屬感:抽象上來說是一種意識形態上的認同感。舉例就是“母校是一個自己天天罵三百遍,但別人罵一句就能拼命的地方”。
驚喜感:產品能在不經意的某一步超出用戶的心理預期,觸達用戶心理最柔軟的那片地方。
沉浸感:(我自己總結起來就是)傻瓜式操作+及時反饋+無其他信息干擾=上癮/沉浸

3.關於書本第十三章“驗收測試”中“可用”→“預覽版”的疑問

在書本13.2“各種測試方法”的“驗收測試”中提到了——如果所有場景都能通過,就是“可用”的,這種版本也就是“社區預覽版”和“技術預覽版”的由來。那么,既然已經“可用”了,怎么還是“預覽版”,而不是“正式版”。如果這樣都不能達到“正式版”的要求,那我們得達到什么要求才能把版本當作“正式版”?
問題3

通過查詢網上資料,我了解到了“預覽版”和“正式版”的定義。
預覽版:尚未穩定的測試版。主要用於軟件未來版本的改善與修正。
正式版:總結了之前預覽版的BUG並修復完善后的版本。
通過這個定義,大概可以推出一個流程:經過測試確定“可用”→發布“預覽版”供用戶使用→通過反饋收集測試過程沒有發現的BUG問題→修復收集到的BUG信息→修復完畢后發布更加完善的“正式版”。
所以我們在軟件測試過后得到的版本只能稱之為“預覽版”,畢竟實踐出真知,還沒投放市場之前,就算所有功能都是可用的,實際上仍存在很多問題,必須經過“預覽版”到“正式版”之間的過渡,同時“預覽版”也不是我之前認為的功能不齊全的次品,“預覽版”其實已經屬於接近完善的版本,功能基本實現才能稱之為測試版,只是測試版還需進一步考驗才能晉升為正式版。

4.對於16章創新16.1.5“迷思之五”的解釋,我有其他見解

"要成為領域的專家,才能夠創新"這句話確實我也不贊同,但是我有其他的看法。

“事實上在WWW/HyperText協議剛出現時,一些計算機專家非常看不起這個玩意,專家們認為,一個文本文件上有一些文字,有些是藍色的,用鼠標一點,就能打開另一個文件,網頁上都不記錄狀態,這算什么難度,這又是什么創新呢?”這是書本中原話,單從這段話看,創新者不是專家的理由似乎是專家對於一些創新不屑一顧,認為其是微不足道的。

我覺得要創新與是不是領域的專家並沒有必然聯系。你不是該領域的專家,就能更容易在該領域創新,這句話也顯然是錯誤的。透過現象看本質,你會發現,創新成功的人,最重要的一點是打破了思維定勢,只是對該領域了解越深的人,就越容易陷入思維定勢罷了,因為你對這個領域太過於了解。所以這個迷思的本質我認為應該是:誰能打破思維定勢,誰才有可能能夠創新。
還有另一種解釋方法,就像書中提到過的“認知阻力”,正是因為專家對於自己領域內的東西過於了解,專家看到的東西與普通人是截然不同的,看問題的角度便會不同。

5.關於17章提到的團隊合作的幾個階段,作為團隊的一個普通成員(而不是領導者),要如何順利的完成過渡?

書本17.5提到了團隊合作的幾個階段,萌芽階段→磨合階段→規范階段→創造階段。書本中對於這四個階段的特征做了說明,以及對領導在幾個階段要做的事做了詳細的舉例。但是對於一個普通的團隊成員要做到什么沒有詳細的描述。

反復讀了幾遍書本內容后,我自己總結了各個階段,一名普通成員應該做的(純屬個人看法)
萌芽階段:盡快適應新的團隊環境,嘗試去了解其他成員,並弄清自己的定位,積極配合領導開始最初的工作。
磨合階段:如果自身屬於技術能力較強的人員,可以適當發揮自己的技術領導能力;注意與隊友共事、交流的方式是否存在不妥;不要懼怕團隊合作,加強自己的自信心和熱情;碰到確實無法解決的困難,敢於尋求幫助。
規范階段:團隊的規矩已經定下,盡量不要試圖打破規矩;時刻牢記團隊的目標和決心;承認成員之間的差異性,並且要學習尊重成員。
創造階段:(這個不清楚)

附加題:冒泡、快速排序的起源

1960年代,霍爾正在主攻計算機翻譯,當有一段俄文句子需要翻譯時,第一步是把這個句子的詞按照同樣的順序排列。於是他意識到,他必須找出一種能在計算機上實現的排序的算法來。他想到的第一個算法是后人稱作“冒泡排序 (bubble sort)”的算法。雖然他沒有聲明這個算法是他發明的,但他顯然是獨自得到這個算法的。他很快放棄了這個算法,因為它的速度比較慢。用計算復雜度理論 (Computational complexity theory) 來說,它平均需要 O(n2) 次運算。快速排序 (Quicksort) 是霍爾想到的第二個算法。這個算法的計算復雜度是 O(nlogn) 次運算。當 n 特別大的時候,顯然步驟要少很多。
原文鏈接:快速排序算法的發明者霍爾
在了解這個故事之前,我一直認為霍爾主攻的是計算機算法,沒想到著名的冒泡排序、快速排序都是由霍爾思考出來的,更沒想到的是,霍爾主攻的是計算機翻譯。同時這也印證了上面的問題四,創新的不一定要是該領域的專家。在我們看來,這2種排序算法也許很簡單,但是在當時那個年代,這也算是一種創新,技術上的創新,為計算機翻譯工作帶來了極大的便利。

二、WordCount編程

1.Github項目地址

PersonalProject-Java

2.PSP表格

PSP2.1 Personal Software Process Stages 預估耗時(分鍾) 實際耗時(分鍾)
Planning 計划
• Estimate • 估計這個任務需要多少時間 3day 3.5day
Development 開發
• Analysis • 需求分析 (包括學習新技術) 180 210
• Design Spec • 生成設計文檔 120 140
• Design Review • 設計復審 20 30
• Coding Standard • 代碼規范 60 60
• Design • 具體設計 120 120
• Coding • 具體編碼 300 450
• Code Review • 代碼復審 30 30
• Test • 測試(自我測試,修改代碼,提交修改) 120 150
Reporting 報告
• Test Repor • 測試報告 60 90
• Size Measurement • 計算工作量 30 30
• Postmortem & Process Improvement Plan • 事后總結, 並提出過程改進計划 105 120
合計 1145 1430

3.解題思路描述

  1. IO部分
    讀取文件,用流讀取,一開始看到要讀取字符,於是就決定分別使用FileReader、FilerWriter,改進后換成BufferReader、BufferWriter並封裝成專門的函數獲取。
  1. 統計字符數
    統計ASCII碼,用read()讀取字符,讀到一個字符,字符數加1,后面發現可以先把字符全部讀出並拼接到StringBuffer中,再獲取字符串長度length即可,改進后將StringBuffer替換成了StringBuilder。
  1. 統計單詞數
    先編寫簡單的判斷類isAlpha()、isNum()函數,分別用於判斷是否同時出現四個連續的英文字母,並且這四個英文字母前面也必須是分隔符,用StringBuffer不斷拼接直到分隔符為止。把獲得的單詞填充到Map<String,Integer>中,value為出現次數(這個部分耗費時間最久,設計邏輯耗時,改進結構時也耗費較長時間)
  1. 統計有效行數
    一開始審題有誤,認為只要有字符(除了換行符)都算是有效行數,后面發現非空白字符行才算是有效行數,因此,寫了一個簡單的判斷函數isBlank()用於判斷空白符。(不過由於自己的失誤,一個if忘記接else導致判斷語句一直無法成功跳轉,卡了很久。)
  1. 打印詞頻最高的十個單詞
    從第3步獲得的Map中獲取詞頻,自定義一個比較器,詞頻越高排序越前,詞頻相同則按照字典序,轉換成一個List,再按順序輸出前十個單詞及其詞頻。

4.代碼規范制定鏈接

codestyle.md

5.設計與實現過程

總共有2個類.
Lib類擁有13個自定義函數,IO封裝getReader()、getWriter()函數,標准輸出到文件的writeToFile()函數,獲取流中字符串的getStr()函數、字符串切割成單詞的handleWords()函數等等;
WordCount類擁有一個主函數,一個run函數(用於組織函數邏輯)。
以下是兩個類中的函數名及其注釋,包括他們之間調用的流程圖。

public class Lib {
    //獲得輸入流
    public static BufferedReader getReader(String inputFile) throws FileNotFoundException {…}
    
     //獲得輸出流
    public static BufferedWriter getWriter(String outputFile) throws IOException {…}
    
    //標准化輸出到文件
    public static String writeToFile(…) throws IOException {…}
    
    //獲取流中字符串
    public static String getStr(String inputFile) throws IOException {…}
    
    //統計字符數
    public static int countChars(String str) {…}
    
    //統計單詞並填充入map
    public static Map<String, Integer> handleWords(String str) {…}
    
    //判斷單詞前是否為分隔符或者空格(因為要復用所以提取出來),是則填充map
    public static void insertMap(…) {…}
    
    //從map提取數據計算並返回單詞數
    public static int countWords(Map<String, Integer> map) {…}
    
    //統計有效行數
    public static int countLines(String inputFile) throws IOException {…}
    
    //從map提取詞頻最多的十個單詞並返回字符串
    public static String printWords(Map<String, Integer> map) {…}
    
    //判斷是否是字母
    public static boolean isAlpha(char ch) {…}
    
    //判斷是否是數字
    public static boolean isNum(char ch) {…}
    
    //判斷是否是空白符
    public static boolean isBlank(char ch) {…}
}

public class WordCount {

    public static void main(String[] args) throws IOException {…}
    
    //用於組織Lib類中函數的調用順序
    public static void run(…) throws IOException {…}
}

(因為博客園流程圖顯示不出來,只能截圖作業部落的預覽流程圖過來)
1
2
3
4
5
6

下面是核心函數handleWords的片段

public static Map<String, Integer> handleWords(String str) {
        Map<String, Integer> map = new HashMap<>();
        StringBuilder chars = new StringBuilder();
        int i = 0;
        int ch;//每次讀取到的字符
        int countAlpha = 0;//字母數
        int wordLength = 3;//單詞長度
        boolean wordFlag = false;//是否成單詞
        while(i < str.length()){
            ch = str.charAt(i++);
            chars.append((char) ch);//每一次拼接一個字符
            if(isAlpha((char) ch))
                countAlpha++;
            else{
                if(countAlpha < 4)
                    countAlpha = 0;//如果沒有連續四個英文字母,計數清零
            }
            if(countAlpha >= 4){//有連續四個英文字母
                wordFlag = true;//單詞出現
                int len = chars.length();
                if(isAlpha((char) ch) || isNum((char) ch))
                    wordLength++;//單詞長度增加
                else{//遇到分隔符
                    wordFlag = false;//單詞截取結束
                    insertMap(map, chars, wordLength, len);//填充map
                    countAlpha = 0;
                    wordLength = 3;
                }
            }
        }
        if(wordFlag){//防止讀到結束時正在截取的單詞的丟失
            int len = chars.length() + 1;
            insertMap(map, chars, wordLength, len);
        }
        return map;
    }

填充map的過程中還有一次判斷

public static void insertMap(Map<String, Integer> map, StringBuilder chars, int wordLength, int len){
        String word = chars.substring(len - wordLength - 1, len - 1).toLowerCase(Locale.ROOT);
        if(word.length() < len - 1 && isNum(chars.charAt(len - wordLength - 2))){
            //單詞前有分隔符或無字符才算是單詞
        }else if(map.containsKey(word)){
            int value = map.get(word);
            map.put(word, value + 1);
        }else
            map.put(word, 1);
    }

下面是map轉換成list過程自定義比較器的實現

    Comparator<Map.Entry<String, Integer>> valCmp = (o1, o2) -> {
        if(o1.getValue().equals(o2.getValue())){
            return o1.getKey().compareTo(o2.getKey());//詞頻相同按照字典序排序
        }else
            return o2.getValue() - o1.getValue();//詞頻高的在前
    };

6.性能改進

  1. IO次數的減少
    原先,四種數據的輸出,都要重新讀一次輸入文件(一共四次),封裝IO后,簡化成了兩次;利用writeToFile()函數把四次的輸出,統一到一次,從四次簡化成了一次。
public static String writeToFile(String outputFile, int characters, int words, int lines, String freq) throws IOException {
        BufferedWriter writer = getWriter(outputFile);
        StringBuilder str = new StringBuilder();
        str.append("characters: ").append(characters).append("\n")//字符數
                .append("words: ").append(words).append("\n")//單詞數
                .append("lines: ").append(lines).append("\n")//有效行數
                .append(freq);//詞頻最高前十個的單詞及其詞頻
        writer.write(String.valueOf(str));
        writer.close();
        return String.valueOf(str);
    }
  1. 單次IO速度的提高
    采用BufferReader、BufferWriter代替FileReader、FileWriter類
    //獲得輸入流
    public static BufferedReader getReader(String inputFile) throws FileNotFoundException {
        return new BufferedReader(new FileReader(inputFile));
    }

    //獲得輸出流
    public static BufferedWriter getWriter(String outputFile) throws IOException {
        return new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outputFile), StandardCharsets.UTF_8));
    }
  1. 流中獲取字符串加快
    使用StringBuilder代替StringBuffer類
//StringBuffer str = new StringBuffer();
StringBuilder str = new StringBuilder();
  1. 字符串轉換成單詞(字符串掃描)的次數減少
    將統計單詞數和打印詞頻之前的字符串掃描提前到原本的函數之前,分離出handleWords()函數,減少了一次掃描時間。
    Map<String, Integer> map = Lib.handleWords(str);
    int words = Lib.countWords(map);
    String freq = Lib.printWords(map);//減少了一次handleWords的時間
  1. 性能測試
    測試一
    250萬字符,50萬單詞(較規則),50萬行,讀了0.245s(以下均是反復測后取的穩定數據)
    大數據3
    大數據4
    測試二
    5000萬字符,1千萬單詞(較為規則),1千萬行,讀了2.2s
    大數據7
    大數據8
    測試三
    下面這個例子的輸入文件由李宇琨同學友情贊助。
    1.47億字符,1千萬+單詞(極不規則),1千萬行,讀了9.5s
    大1
    2
    大2

7.單元測試

  1. 單元覆蓋率截圖
    分別為LibTest中測試和WordCount中測試的截圖
    覆蓋率1
    覆蓋率2
    覆蓋

下面是其中兩個測試代碼,分別為打印詞頻測試、整體運行測試

void printWords() {
   try {
       String str = Lib.getStr("221801304/src/input.txt");
       Map<String, Integer> map = Lib.handleWords(str);
       String freq = Lib.printWords(map);
       System.out.println(freq);
   } catch (IOException e) {
       e.printStackTrace();
   }
}
void mainTest() throws IOException {
   String str = Lib.getStr("221801304/src/input.txt");
   int characters = Lib.countChars(str);
   int lines = Lib.countLines("221801304/src/input.txt");
   Map<String, Integer> map = Lib.handleWords(str);
   int words = Lib.countWords(map);
   String freq = Lib.printWords(map);
   String result = Lib.writeToFile("221801304/src/output.txt", characters, words, lines, freq);
   System.out.println(result);
}
  1. 覆蓋率未滿原因分析
  • try\catch塊中的Exception異常沒有覆蓋到
  • if\else塊中的另一分支只有在極少數的條件下才會觸發(僅當只有一行,且這一行所有字符合成恰好是個單詞,才會觸發)
    wordFlag
  1. 正確性測試
  • 如果能正確的讀到空白符,則以空白間隔的字母不會合成一個單詞
    是否能讀到空白
    1

  • 如果超過十個單詞,是否會輸出超過十個詞頻,不超過10個則正確
    詞頻
    1

  • 如果只有一行,一種字母,一個單詞的情況(這個例子可以解決上面測試的時候單元測試沒覆蓋到的哪個if分支)
    一行
    1

  • 如果兩個詞頻率相同,按字典序輸出,則正確
    字典序
    1

  • 單詞前后必須是分隔符才算是單詞,且必須至少是四個字母開頭
    分
    1

  • 連續按出三個制表符\t,如果是三個字符,則正確
    制表符
    1

  • 所有行都是n個空格,如果有效行數是0,才正確
    有效
    1

  • 包含多種特殊情況,綜合測試。
    綜合
    1

  • 幾種重復單詞多次大量出現,且要正確忽略大小寫

for(int i = 0; i < 2000000; i++){//添加到輸入文件中
    stB.append("agaa").append("\n");
}
for(int i = 0; i < 2000000; i++){
    stB.append("AGaa").append("\n");
}
for(int i = 0; i < 2000000; i++){
    stB.append("dSSd").append("\n");
}
for(int i = 0; i < 2000000; i++){
    stB.append("dssd").append("\n");
}
for(int i = 0; i < 2000000; i++){
    stB.append("epee").append("\n");
}

q

  • 大量的不規則或規則數據,要正確統計
    2
    1

8.異常處理說明

只有利用現有的Exception:IOException和FileNotFoundException,如果文件沒有找到,或者命令行參數輸入參數個數不足兩個,就會拋出異常。
測試過程中在try/catch塊中使用了Exception,沒有自定義特殊異常類。
代碼中存在處理特殊情況的代碼,但是屬於正常的輸入內容,沒有納入異常處理范圍(只是較為特殊,並非不合法)

9.心路歷程與收獲

  • 養成良好的代碼規范習慣十分重要,通過編寫codestyle.md並且規范自己的代碼,我覺得代碼的可讀性更高,而且代碼寫起來也更優美,不會雜亂無章

  • Git作為一個版本控制系統,在項目開發過程中,對我的幫助很大,在用Github Desktop進行commit的過程中還能知道自己的代碼到底是怎么樣發生了變化,整體變化會更加清晰,同時我也推薦大家使用Github Desktop,真的很好用

  • 往后要加強邏輯的思考,在項目開發的過程中,出現好多次條件判斷錯誤,致使項目開發受阻。對於這點,我覺得可以采用設計前,先拿個紙筆過來動手寫一下要考慮的要點,再實際編寫代碼,避免疏漏過多。

  • 單元測試很重要,以前寫程序沒有足夠重視這一點,單元測試在開發過程中不可缺少,要善於使用單元測試來驗證程序的合理性、正確性。

  • PSP表格預估的時間與自己的實際使用時間有較大的出入,對自己的評估還不夠准確。


免責聲明!

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



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