這個作業屬於哪個課程 | 2021春軟件工程實踐 | W班 (福州大學) |
---|---|
這個作業要求在哪里 | 軟工實踐寒假作業(2/2) |
這個作業的目標 | 體驗軟件開發過程 |
其他參考文獻 | 無 |
閱讀《構建之法》並提問
我的問題
1. 第二章 個人技術和流程
單元測試必須有最熟悉代碼的人(程序的作者)來寫。代碼的作者最了解代碼的目的、特點和實現的局限性。所以,寫單元測試沒有比作者更適合的人選了。
一開始我是贊成這段話的,但后來上了軟件質量測試課,認識到專業的測試人員不僅有編寫代碼的能力,而且掌握測試思想,在專業程度上會比開發人員更勝一籌。當對程序的熟悉程度與測試的專業程度無法兼顧時,是否可以通過在開發人員里培養測試人員解決矛盾?
當開發人員與專業測試人員一起進行測試時,你將更清楚全面地了解與產品發布相關的業務風險。同時,在用戶遇到高風險問題之前,你將有機會去解決掉這些麻煩。這也正是測試的最終目標——它需要多個角色之間更多的協作,而不是針對開發人員 / 測試人員誰該承擔測試任務爭論不休。
以上時我在一篇文章中看到的說法。但是這又引出一個新問題,讓開發人員接手多少測試工作才會不影響他的精力,又同時保證測試的高質量?
2. 第四章 兩人合作
結對編程
對“結對編程”這個模式的可行性還是有些疑惑的,如果當兩個人的價值理念與目標期待產生強烈沖突時,這個模式還能順利進行下去嗎?如果在原則上,比如這個軟件的最終呈現形式等出現觀念的對立,我不認為通過交流技巧就可以達成一致。結對編程是否只能用在簡單的程序編寫和單元測試上呢?進行結對的時候是否需要一個兩人之外的統領者?
3. 第八章 需求分析
市場分析者:代表“典型用戶”的需求,他們或是市場部分的成員,或者是獨立的市場分析人士。
這是在“軟件產品的利益相關者”中提到的,只有寥寥幾筆,沒有多余的描述了,但我有些好奇。在網絡上搜索了一下“市場分析”,發現涵蓋的范圍比我想象中的廣很多,看了幾篇報告后,發現報告涵蓋了產品競爭格局、市場供需狀況、市場規模分析與行業政策法規等方面的內容。既然市場分析者對整個市場有較清晰的把控,能代表“典型用戶”的需求,相較於普通用戶又能夠與開發團隊進行更專業的溝通交流,那可不可以說市場分析人士的意見比普通用戶的更有建設性?
4. 第八章 需求分析 & 第九章 項目經理
PM做開發和測試之外的所有事情。
看完第八章,感嘆需求分析這么重要也這么復雜的任務要交由誰來完成,然后就看到了第九章的“項目經理”這一角色。私以為,“開發和測試之外的所有事情”都交由一個人來做有些可怕了,畢竟開發團隊不只有“程序員”,也有專職開發的與專門測試的,專業的事由專業的人來做。而PM卻要完成需求分析、項目計划、風險把控、資源統籌等多項任務,工作是否過於繁重?如果PM中也適當分職會有更高的效率和更出色的完成度嗎?
帶着這一問題去查閱資料,發現有時一個團隊也會同時又產品經理與項目經理,但如果兩人的方案產生沖突無法協調,這個產品又怎么做好呢?
5. 第十一章 軟件設計與實現
寫好代碼后,小飛對照設計文檔和代碼指南進行自我復審,重構代碼。
“重構代碼”這一概念讓我有些在意,以為我一直以為程序的基本架構在一開始就是設計好的,至少后期不能修改。我查閱了一些資料,有這樣的定義:
關於代碼重構的理解:在不改變軟件系統/模塊所具備的功能特性的前提下,遵循/利用某種規則,使其內部結構趨於完善。其在軟件生命周期中的價值體現主要在於可維護性和可擴展性。
根據一些實例來看,似乎小至“函數命名不規范,缺少注釋,尤其是函數功能、返回值及參數說明”,大到“系統技術架構無法滿足業務發展需求,如性能瓶頸頻現,無法快速進行新業務邏輯的添加/修改”都算作需要重構。前者可以理解,也讓我意識到我確實在時時進行重構,而后者的修改就顯得有些困難。當軟件出現性能、架構上的瓶頸時,該如何跨越這一難點?如果無法解決、且計划好的交付時間臨近,程序要強行在原架構上繼續擴展嗎?
軟件工程發展過程中的小故事
史上第一位程序員是名貴族小姐,且這位貴族小姐來頭不小,是19世紀英國著名詩人拜倫的女兒。她是一名數學家,也是世界上第一位程序員。她的名字是Ada Lovelace。
阿達一生做出的成就不少。她設計了巴貝奇分析機上解伯努利方程的一個程序,證明了計算機狂人巴貝奇的分析其可以用於許多問題的求解。
后來她在1843年發表的論文里提到了一個叫循環和子程序的概念,並且她相信以后創作復雜音樂、制圖和科學研究是可以通過機器來創作的,這在當時是大膽的預見,但在今天都逐漸成為了現實。
現在看來,阿達首先為計算機擬定的“算法”,以及寫作的那份“程序設計流程圖”都是極為難得和珍貴的,也是史上第一件計算機程序。
后來據說國防部花了10年時間,把所需軟件的全部功能混合在一種計算機語言里,為的是想讓它能成為軍方數千種電腦的標准。
於是在1981年,為了紀念這位程序員,這種語言被正式命名為ADA(阿達)語言,艾達·洛夫雷斯也被公認為“世界上第一位軟件工程師”。
沒想到在一個以男性工作者占比大而聞名的領域中,第一位聞名的工作者居然是一名女性。這個故事激勵着我,無論以后成功還是失敗,別將結果歸因於性別。
參考鏈接:https://blog.csdn.net/XVJINHUA954/article/details/110266902
WordCount程序
Github項目地址
PSP表格
Personal Software Process Stages | 預估耗時(分鍾) | 實際耗時(分鍾) |
---|---|---|
計划 | 7 | 7 |
估計這個任務需要多少時間 | 7 | 7 |
開發 | 620 | 535 |
需求分析 (包括學習新技術) | 30 | 20 |
生成設計文檔 | 40 | 25 |
設計復審 | 10 | 5 |
代碼規范 | 20 | 30 |
具體設計 | 30 | 25 |
具體編碼 | 300 | 210 |
代碼復審 | 40 | 40 |
測試 | 150 | 180 |
報告 | 60 | 50 |
測試報告 | 15 | 10 |
計算工作量 | 15 | 10 |
事后總結, 並提出過程改進計划 | 30 | 30 |
合計 | 687 | 592 |
解題思路描述
解題思路描述。即剛開始拿到題目后,如何思考,如何找資料的過程。
一開始拿到題目時先進行需求分析,最先關注主要實現什么功能,其次再看看是否有另外的要求,如此次要求對核心功能進行封裝。簡單查閱了Java的文件讀寫相關API后,確定了在統計字符數時逐字符讀取,統計行數時逐行讀取,在統計單詞數的同時記錄單詞與其出現次數,之后實現排序。在對比多個數據結構后,考慮到查找的方便,選擇用map來存儲單詞及其詞頻。至於封裝,決定采用一名為CoreCount的類來包裝核心功能的函數。
代碼規范制定鏈接
設計與實現過程
設計包括代碼如何組織,比如會有幾個類,幾個函數,他們之間關系如何,關鍵函數是否需要畫出流程圖?說明你的算法的關鍵(展示出項目關鍵代碼),並解釋思路,以及獨到之處。
設計
- WordCount類
該類包含一個main函數。main函數負責從命令行獲取輸入的文件名,將輸入文件的文件名傳給核心功能類,獲取返回結果后,生成格式化的內容並寫入輸出文件。 - CoreCount類
該類的數據成員有字符數charCount、單詞數wordCount、行數lineCount和一個map存儲的單詞表wordsMap,前三個成員類型為Long。該類的成員函數有上述數據成員的get方法,有三個實現核心功能的方法——countChars()統計字符數,countWordsAndLines()統計單詞數與行數,同時也生成未排序的單詞表,sortWordsMap()對單詞表進行排序。其中,sortWordsMap()在countWordsAndLines()中被最后調用。此外還有一個count()函數,集中調用了countChars()與countWordsAndLines()兩個方法,供外部程序一次性調用所有核心方法,實現全部功能。
WordCount類的main函數在獲取了輸入文件的文件名之后,通過該文件名new一個CoreCount類的實例對象,調用該對象的count()方法讓其實現統計功能。接下來,main函數再依次調用CoreCount實例對象的各個getXXX()方法,獲取統計數據,生成格式化字符串並寫入輸出文件。對於詞頻統計,調用getWordsList()方法獲取一個wordsMap排序后的entry list。遍歷該list,輸出前十個單詞及其出現次數。
實現
- 統計字符數
用BufferdReader的read()方法逐字符讀取,讀取的同時charCount加一。while (reader.read() != -1) { charCount += 1; } reader.close();
- 判斷是否為正確格式的單詞
逐個判斷讀取的字符,若字符為字母或數字,將其存入臨時字符串word中,並讀取下一個。而這個動作將在遇到一個非字母數字字符時被打斷。當遇到一個非字母數字字符時,判斷由其之前若干個字母數字字符組成的字符串word是否為一個格式正確的單詞。若是,則對該單詞進行處理,若不是,將該字符串置空,繼續讀取下一個字符。int len = line.length(); //line為用BufferdReader的readLine()方法讀取的字符串 for (int i = 0; i < len; i++) { ch = line.charAt(i); //逐個字符 if (Character.isLetterOrDigit(ch)) { //判斷是否為字母數字字符 word += ch; } else { if (!"".equals(word)) { if (isProperWord(word)) { //若格式正確,處理該單詞 } } // end if word = ""; //重置詞組 } // end if } // end for
- 生成單詞表
若是格式正確的單詞,就將其加入單詞表。首先要將該單詞轉為小寫格式,然后在單詞表wordsMap中按鍵查找該單詞,若未找到,說明這是它第一次出現,則put(word, 1L),若找到,則取出value值(即單詞出現次數),然后put(word, ++value)。if (isProperWord(word)) { wordCount += 1; word = word.toLowerCase(); if (wordsMap.get(word) == null) { wordsMap.put(word, 1L); } else { value = wordsMap.get(word); wordsMap.put(word, ++value); } }
- 對單詞表進行排序
用Collections.sort方法對wordsMap進行排序。重寫比較器中的compare方法,按照value值由大到小進行排列,若value值相同,再對比key,若字典序靠前則排位靠前,這可通過String的compareTo方法實現。wordsList = new ArrayList<Map.Entry<String, Long>>(wordsMap.entrySet()); Collections.sort(wordsList, new Comparator<Map.Entry<String, Long>>() { public int compare(Map.Entry<String, Long> o1, Map.Entry<String, Long> o2) { if (o1.getValue() == o2.getValue()) { return o1.getKey().compareTo(o2.getKey()); } else { return (int)(o2.getValue() - o1.getValue()); } } });
- 統計行數
該功能在countWordsAndLines函數中實現,BufferedReader每讀取一行,lineCount加一。while ((line = reader.readLine()) != null) { if (! line.trim().equals("")) { lineCount += 1; } }
性能改進
展示出項目性能測試截圖並描述;記錄在改進計算模塊性能上所花費的時間,描述你改進的思路。
在countWordsAndLines方法中,有一for循環逐個讀取字符串的字符。原先的代碼如下:
for (int i = 0; i < line.length(); i++) {
//……
}
這意味着每進行一次循環,都要調用一次length()方法,這將產生不必要的開銷。於是將代碼改為只調用一次length()方法:
int len = line.length();
for (int i = 0; i < len; i++) {
//……
}
性能有所提升:
修改前的運行時間
修改后的運行時間
單元測試
展示出項目部分單元測試代碼,並說明測試的函數,構造測試數據的思路。並將單元測試得到的測試覆蓋率截圖,發表在博客中;如何優化覆蓋率?
CoreCount的測試
CoreCount測試未另寫測試用例,主要通過程序輸入。
- 統計字符數功能的測試
輸入所有ASCII碼字符,測試是否能全部統計。@Test void testCountChars() throws IOException { BufferedWriter writer = new BufferedWriter(new FileWriter(file)); for (int i = 0; i < 128; i++) { writer.append((char)i); } writer.close(); coreCount.countChars(); assertEquals(128, coreCount.getCharCount()); }
- 統計行數功能的測試
在幾百條格式正確的行中,插入7條僅僅包含空白字符但字符組成方式不同的空行,測試程序是否能准確排除空白行。@Test void testCountLines() throws IOException { BufferedWriter writer = new BufferedWriter(new FileWriter(file)); for (int i = 0; i < 700; i++) { if(i == 0) writer.append("\n\n"); else if(i == 100) writer.append("\r\n"); else if(i == 200) writer.append("\t\n"); else if(i == 300) writer.append("\f\n"); else if(i == 400) writer.append("\0\n"); else if(i == 500) writer.append("\0\f\t\r\n"); else if(i == 600) writer.append(" \n"); else writer.append(" Here are some meaningful words, and this is a proper line. \n"); } writer.close(); coreCount.countWordsAndLines(); assertEquals((700 - 7), coreCount.getLineCount()); }
- 統計單詞數功能的測試
依次輸入格式不正確的詞組與正確形式的單詞。格式不正確的詞組包括以數字開頭的詞組、以小於4個英文單詞開頭的詞組,以及穿插非字母數字字符的單詞。測試程序是否能排除格式不正確的詞組,只統計正確的單詞。@Test void testCountWords() throws IOException { BufferedWriter writer = new BufferedWriter(new FileWriter(file)); //測試詞組以數字開頭的情況,如"7word" for (int i = 0; i < 11; i++) { writer.append(Integer.toString(i) + "word" + "\n"); } //測試詞組以<4個英文字母開頭的情況,如"wor7d"或"war" for (int i = 0; i < 21; i++) { writer.append("wor" + Integer.toString(i) + "d" + "\t"); } for (int i = 0; i < 31; i++) { writer.append("war" + "\r"); } //測試當正確的單詞中穿插非字母數字字符時,程序是否能正確識別 for (int i = 0; i < 41; i++) { writer.append("w[or]d" + " "); } //正確的單詞 for (int i = 0; i < 51; i++) { writer.append("word" + Integer.toString(i) + "&"); } writer.close(); coreCount.countWordsAndLines(); assertEquals(51, coreCount.getWordCount()); }
- 生成有序單詞表功能的測試
輸入多組相互對比的單詞,比如word19與word2、a與aaa,abcde與bcdef,這些單詞出現頻率相同,以此測試單詞排序的正確性。同時也輸入一些出現次數大於1的單詞,測試詞頻排序的正確性。再輸入已知次數的同一單詞,但單詞的大小寫形式不斷變化,以此測試程序在存儲單詞時是否完成小寫轉換,並對同一單詞的不同形式正確計數。
測試時,遍歷排序后的單詞表wordsMap,對比前一組與后一組的頻率與單詞。若后一組的頻率高於前一組,或頻率相同時,后一組的單詞字典序更靠前,則fail測試。@Test void testSortWordsMap() throws IOException { BufferedWriter writer = new BufferedWriter(new FileWriter(file)); //檢查是否對同一頻率的單詞按照字典序正確排序,如word19比word2排序更前 for (int i = 0; i < 100; i++) { writer.append("word" + Integer.toString(i) + " "); } //如a比aaa排序更前 String str = ""; for (int i = 0; i < 10; i++) { for (int j = i; j < 15; j++) { str += 'a'; } writer.append(str + "\r"); str = ""; } //如abcde比bcdef排序更前 char[] array = "abcde".toCharArray(); writer.append(String.copyValueOf(array) + "\t"); for (int i = 0; i < 20; i++) { for (int j = 0; j < array.length; j++) { array[j] = (char)(array[j] + 1); } writer.append(String.copyValueOf(array) + "\t"); } writer.flush(); //檢查頻率排序的正確性,隨意添加幾個出現次數>1的單詞 for (int i = 0; i < 30; i++) { writer.append(" your opal eyes areall Iwish tosee "); if (i < 1) writer.append(" opal eyes areall Iwish tosee "); if (i < 2) writer.append(" areall Iwish tosee "); if (i < 3) writer.append(" Iwish tosee "); if (i < 4) writer.append(" tosee "); } writer.append("\n"); writer.flush(); //檢查是否正確識別變換大小寫形式的同一單詞,同時測試同一單詞的計數功能 char[] word = "champagne".toCharArray(); for (int i = 0; i < word.length; i++) { word[i] = Character.toUpperCase(word[i]); writer.append(String.copyValueOf(word) + "\n"); } word = "champagne".toCharArray(); writer.close(); //開始測試,當value或key的排列順序不符合規定時,fail the test coreCount.countWordsAndLines(); Iterator<Map.Entry<String, Long>> iterator = coreCount.getWordsList().iterator(); Map.Entry<String, Long> entry = null; String key, preKey; key = preKey = ""; Long value = 0L, preValue = 0L; while (iterator.hasNext()) { entry = (Map.Entry<String, Long>) iterator.next(); key = entry.getKey(); value = entry.getValue(); if (value < 0) fail("出現錯誤!單詞 " + key + " 的頻率為 " + value + " ,小於0"); if (key.isEmpty()) fail("出現錯誤!單詞為空"); if ((preValue != 0) && (value > preValue)) fail(preKey + "-" + preValue + " 與 " + key + "-" + value + " 的頻率排序錯誤"); else if (value == preValue) { if (key.toString().compareTo(preKey.toString()) <= 0) fail(preKey + "-" + preValue + " 與 " + key + "-" + value + " 的單詞字典序排序錯誤"); } preKey = key; preValue = value; } assertEquals(word.length, coreCount.getWordsMap().get(String.copyValueOf(word))); }
- 輸入內容為空的測試
@Test void testCount() throws IOException { //測試輸入為空的情況 BufferedWriter writer = new BufferedWriter(new FileWriter(file)); writer.append(""); writer.close(); coreCount.count(); assertEquals(0, coreCount.getCharCount()); assertEquals(0, coreCount.getWordCount()); assertEquals(0, coreCount.getLineCount()); }
WordCount的測試
WordCount測試包含各類異常的測試、文件中沒有內容的測試,與一個傳入正常文件的測試。因為幾個測試大致相同,故只貼出傳入正常文件的測試的代碼:
void testMain() {
String[] files = {"input.txt", "output.txt"};
WordCount.main(files);
}
其中,測試用例input.txt的內容如下:
forever&sad&sdark2 dGIGsede| dy77dew(((finsha435 9
782* 324**de forever==56yuyu ewyuD-hu 7-daRk2
love story the STORY OF us
everything has changed yooou said forever
wonderland blank space spacE SPAce[SpAce]space@space1{space2&SPACE1&&spa\Space2021}space#forever$story
56yuyu+is+dGIGsede ewyuD-hu-ewyuD-hu
everything everything everything 1everything
alltoowell all1-too20-well
rever= =56y( ) changed
測試覆蓋率
優化覆蓋率時應充分考慮各種分支,因為分支中可能存在導致錯誤發生的邊界情況。
異常處理說明
在博客中詳細介紹每種異常的設計目標。每種異常都要選擇一個單元測試樣例發布在博客中,並指明錯誤對應的場景。
- FileNotFoundException
當輸入不規范、當前目錄中不包含輸入文件名的文件時,會拋出FileNotFoundException並打印異常信息。
測試如下:@Rule public ExpectedException expectedEx = ExpectedException.none(); @Test void testFileNotFound() { expectedEx.expect(FileNotFoundException.class); expectedEx.expectMessage("3"); String[] files = {"3", "output.txt"}; WordCount.main(files); }
- NullPointerException
main函數中有一條語句:
用於獲取單詞表wordsMap排序后的entry list。但在CoreCount中,該list的初值設為null。故此處要包裝在try-catch語句中,拋出一個NullPointerException並打印異常信息。Iterator<Map.Entry<String, Long>> iterator = coreCount.getWordsList().iterator();
- IOException
BufferedReader的讀寫異常。
心路歷程與收獲
- 體驗了軟件開發過程,結合書本知識,通過實踐,加深了對“軟件=程序+軟件工程”這一概念的理解,為后續的團隊開發奠定基礎。
- 需求分析一定不能馬虎,不能只關注核心功能而疏忽一些其他要求,要面面俱到。此次在程序基本完成后才注意到要求不是從控制台輸入而是以命令行輸入,若沒有助教提醒可能會直接提交一個錯誤的程序上去。也幸好程序不復雜,獲取輸入的模塊獨立性強,改動時不影響其他功能的性能。
- 以后可以考慮邊寫代碼邊復審,以便性能改進。這次等具體編碼完成后才統一復審並進行性能改進,本來想試試將程序中的部分String類型數據換成StringBuffer,但由於改動太大,會牽涉到其他功能,於是放棄了。
- 代碼更新的同時,注釋和測試用例也要及時更新,把多次更新積攢在一起的話,之后會很麻煩。
- 完成程序時進行詳細的總結對學習和進步有很大幫助。
- 經歷一整個開發過程的軟件與以前一拿到題目就悶頭寫代碼得到的軟件有很大不同,至少我對於程序架構的把握更清晰,也比較清楚程序的性能和正確性。
- 第一次寫測試,發現測試真的很有幫助,不僅可以實現自動化、不用一次次在控制台輸入、同時對比輸出,而且可以照亮程序的死角。
- 不太理解為什么不讓貼大段代碼,個人認為只要輔以恰當的注釋說明,代碼並不會給讀者太差的觀感,至少我還是挺樂意看到代碼的,畢竟代碼段行數多並不一定等於內容雜或結構差。
- 一定要備份!也不要把VS Code的工作區建在安裝目錄下!!!要不然就可能像我今天這樣,ddl當天VS Code更新、工作區被覆蓋,被迫成為一個無情的打字機器。