寒假作業2/2
這個作業屬於哪個課程 | 2021春軟件工程實踐|W班 (福州大學) |
---|---|
這個作業要求在哪里 | 寒假作業2/2 |
這個作業的目標 | 閱讀《構建之法》提出問題,實現WordCount程序,記錄PSP表格 |
其它參考文獻 |
結對編程 技能的反面 PSP |
GitHub項目地址 | https://github.com/NOS-AE/PersonalProject-Java |
閱讀《構建之法》並提問
-
書中第二章第三小節介紹了PSP 的特點,其中一點是
PSP不依賴於考試,而主要靠工程師自己收集數據,然后分析,提高。
我有個這個問題:對於一開始自己十分不熟悉的項目或者技術,或者說未知領域的探索,這本身是一件復雜的事情,如同軟件開發的“沒有銀彈”一樣,預估耗時也沒有公式可用,PSP的預估耗時一欄可能會出現很大偏差,偏差太大的話對於之后的總結分析會有什么樣的影響,應該如何正確預估耗時。其中還說到了可以根據PSP數據提高自身,我查了資料,其中說到
- 穩定、成熟的PSP可以使你
- 估計和計划自己的工作
- 滿足自己的承諾
- 拒絕不合理的承諾
- PSP提供了
- 一個得到證明的用於開發的基礎框架
- 告訴你怎么來改進自己個體過程
- 持續改進工作效率、工作質量、工作可預測性的相關數據
但對於具體該怎么根據PSP來提高工程師自身能力還是不太懂
- 穩定、成熟的PSP可以使你
-
書中第三章第一節說到了團隊對個人的期望
交流、收到做到、全力投入、積極討論、理性地工作....
做項目是為了自身收益,組成團隊是為了更快更好地完成一個好的項目,這門課程的組隊項目開發總有人處於類似“臨時的寄托或工作(Temporary Work)”,處於低動力、低技能的狀態,這與團隊對個人的期望相悖,不利於整個項目的完成,該如何正確應對。
-
書中在第三章第三節說專和精的關系的時候提到
有人說一個人就可以快速成長為一名全棧工程師,這讓我想起街頭賣藝的單人樂隊,他們什么都會一些,可以很快地演奏一些曲子...
當我們談論“全棧工程師”的時候,我們說的究竟是“交響樂作曲家寫各個樂器的樂譜”,還是“演奏家滿場奔走,操作各種樂器”呢?
在談論“技能的反面”的時候,說其是“解決問題”。
我又了解到運維工程師在軟件產品的整個生命周期中運維工程師都需要適時地參與並發揮不同的作用,因此運維工程師的工作內容和方向非常多,那么運維工程師究竟是什么都會一點的藝人還是譜寫樂章的作曲者呢?運維需要對產品上線期間出現的各種問題進行解決,所以運維是屬於“解決問題”也就是“技能的反面”嗎,但是網上說運維可以說是越老越吃香,所以我又想到他是在將手上的技能不斷打磨直至精通,而且對產品的各個方面都有所了解與掌控,屬於“技能”嗎。我覺得這個職業屬於運用自己的技能去解決問題,屬於技能的正面,雖然技能沒有在某個領域十分深入,比如維護linux服務器,不需要對linux如何運行起來、源碼等全面掌握,但運維工程師做好了本職工作——維護服務器運行,利用了維護服務器的知識,並隨着不斷地工作而越發熟練與精通。
-
書中第四章第五節說到了結對編程,可以做到邊開發邊復審,提高代碼質量,運用得當還可以取得更高的投入產出比。
但是網上資料顯示國內很少人實施結對編程,很大一部分取決於結對伙伴的性格、編碼能力,以及來源於上司的壓力
CEO:那個A和B最近走的很近,每天上班在一起很大聲不知道說些什么,你要提醒下,對別的部門影響不好。
技術總監:那個是我們正在采用的結對編程的實驗,可以大幅提高工作效率的。
CEO:哦,反正你注意點,不要出亂子。CEO:這個么簡單個項目為什么要兩個人去做,你不是說這個很簡單么?
技術總監:不是的,我們現在在做敏捷,所有的項目都兩個人一起做,這樣效率高。
CEO:那個敏捷我不太懂哦,但是這么簡單個項目要兩個人一起搞,我覺得有點問題,你重新安排下。
技術總監邊擦汗邊說:好的,我們一定重新安排。結對編程適用范圍看起來比書上說的更窄。我認為一個好的公司,應該充分考慮到員工的意見,將適合結對編程的人組合在一起,其他人則使用別的模式,使得效率最大化,或者實行少數服從多數的規則。
-
書上第三章第二節說到了過早泛化的問題。面對軟件開發中日后各種變化以及新增的需求,或者是技術上的抽象需求,應該提早作出預測而在早期就作出大量抽象,還是應該面對新需求見招拆招,局部性地逐步拓展和抽象代碼邏輯,如何控制“度”,使得不過度泛化/過早泛化
冷知識和故事
史上第一款電腦病毒,竟然是由防御技術專家Fred Cohen親手設計出來的。他創造電腦病毒的目的僅僅是為了證明程序對電腦感染的可行性,從未希望借此對電腦造成任何危害。但這款程序卻能夠對電腦進行感染,並且能通過軟盤等移動介質在不同計算機之間進行傳播,因而命名為病毒。后來,他又創造出一種主動式電腦病毒,主要目的是幫助電腦用戶找到未受感染可執行文件。https://zhuanlan.zhihu.com/p/59565938
PSP
PSP2.1 | Personal Software Process Stages | 預估耗時(分鍾) | 實際耗時(分鍾) |
---|---|---|---|
Planning | 計划 | ||
Estimate | 估計這個任務需要多少時間 | ||
Development | 開發 | 400 | 608 |
Analysis | 需求分析 (包括學習新技術) | 30 | 48 |
Design Spec | 生成設計文檔 | 30 | 30 |
Design Review | 設計復審 | 10 | 10 |
Coding Standard | 代碼規范 (為目前的開發制定合適的規范) | 10 | 20 |
Design | 具體設計 | 60 | 150 |
Coding | 具體編碼 | 180 | 280 |
Code Review | 代碼復審 | 20 | 10 |
Test | 測試(自我測試,修改代碼,提交修改) | 60 | 60 |
Reporting | 報告 | 40 | 40 |
Test Report | 測試報告 | 10 | 20 |
Size Measurement | 計算工作量 | 10 | 10 |
Postmortem & Process Improvement Plan | 事后總結, 並提出過程改進計划 | 20 | 10 |
合計 | 440 | 648 |
解題思路
代碼運行流程:
- 用字符讀取流打開輸入文件並讀取數據
- 統計字符數
- 統計單詞數
- 統計有效行數
- 統計出現最高頻率單詞top10
- 用字符輸出流打開輸出文件並輸出數據
其中只需要查找API就能解決的有
-
用字符讀取流打開輸入文件並讀取數據
為了效能,使用BufferedReader
-
用字符輸出流打開輸出文件並輸出數據
為了效能,使用BufferedWriter
-
統計字符數
因為只需要考慮ascii碼,每個字符長度都是1字節,讀取數據后直接獲取數據長度即可
其中需要思考的是:
- 統計單詞數
- 按順序讀取全文
- 使用正則表達式匹配並統計單詞
- 統計有效行數
- 按順序讀取全文
- 使用正則表達式匹配並統計空行
- 統計出現最高頻率單詞top10
- 按順序讀取全文
- 使用 1.統計單詞數 中的正則匹配單詞,用Map<String, Integer>存放單詞個數,按value遞減排序
代碼規范
設計實現過程
Note: 只給出核心代碼,完整代碼見Github
-
主要方法的及其功能的設計
為了便於Lib使用者的使用,經過不斷改動,設計了如下公有方法
/** * 構造Lib * @param inFile 需要從中讀取數據的文件 * @param outFile 需要將計算的數據輸出的文件 */ public Lib(String inFile, String outFile) /** * 相當於對下列所有processXXX的一次性調用 * 但只讀取一次文件,提高效率 */ public void process() /** * 計算單詞數 * 計算top10出現次數的單詞 */ public void processWord() /** * 計算有效行數 */ public void processLineNum() /** * 計算字符數 */ public void processCharNum() /** * 將以上計算好的數據寫入到輸出文件中 */ public void output()
以上設計的好處是,當使用者只需要某部分數據的時候,只需要調用對應的處理方法即可,另外output的公開使得對於數據的生成可以由調用者決定
我將具體的算法放在processXXX對應的processXXXInternal私有方法中,比如下面的統計單詞
private void processWordInternal(String str) { // ... }
該接收待處理的字符串的算法,不需要關心字符串從哪里來(字符串的獲取放在了processXXX中)
以上設計的好處是,隱藏和封裝處理(process)的算法細節,提高了公有API的穩定性,以及方便對算法本身進行測試,另外方便將processXXXInternal方法組合起來放到其它函數中,只讀取一次文件,提高效率,比如process函數。
-
I/O
對於讀取文件和讀入文件,為了簡化外界對WordCount的使用,故對外界隱藏I/O的細節
讀文件
- 讀入文件不需要外界控制時機,設為private
- 使用
BufferedReader
,利用緩存的特性,提高讀取效率 - 使用
StringBuilder
提高性能,避免對需要多次拼接的字符串用+
運算符導致多次構造String - 使用
BufferedReader
的read
函數逐字符讀取
/** * @return the content of the file */ private String readFile() throws IOException { while ((c = reader.read()) != -1) { if (c != 13) { builder.append((char) c); } } return builder.toString(); }
寫文件
-
寫文件即輸出結果,輸出格式固定,故直接硬編碼寫在方法內
-
使用
BufferedWriter
提高寫入效率
/** * write data to file in a hard-encoding format */ public void output() throws IOException { // 此處瘋狂調用BufferedWriter的write方法,就不展示了 }
-
處理單詞
統計單詞出現頻率需要遍歷所有單詞,與統計單詞數有重疊部分,故將統計單詞數與統計單詞出現頻率合並在一個函數中
處理單詞部分較為復雜,做流程圖以輔助
- Java正則API的組匹配:
find()
返回true表明找到一處匹配,group(2)
表示從正則提取單詞,其中組匹配的第1組為整個匹配的字符串,剩余的組為括號對應的項,比如此處要提取的單詞為第3組,故使用group(2)
- 提取單詞轉為小寫后,出現次數記錄在map中
- 全部單詞都提取完並存到map后,使用
Stream API
將map排序
private void processWordInternal(String str) { while (matcher.find()) { // regex:(^|[^A-Za-z0-9])([A-Za-z]{4}[A-Za-z0-9]*) // 正則的組匹配,從符合正則表達式的串中提取單詞 String word = matcher.group(2).toLowerCase(); Integer count = topWord.get(word); if (count == null) { count = 0; } topWord.put(word, count + 1); wordNum++; } // 將結果排序 topWord = topWord.entrySet().stream() .sorted( Map.Entry.<String, Integer>comparingByValue() .reversed() .thenComparing(Map.Entry.comparingByKey())) .limit(10) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); }
- Java正則API的組匹配:
-
統計有效行數
注意到有效行指的是包含非空白字符的行,故也用了正則匹配
- 此處正則直接匹配有效行
private void processLineNumInternal(String str) throws IOException { // regex: (^|\n)\\s*\\S+ Matcher matcher = linePattern.matcher(str); while (matcher.find()) { lineNum++; } }
-
統計字符數
將讀取文件得到的字符串獲取長度即可
private void processCharNumInternal(String str) { charNum = str.length(); }
性能改進
- I/O使用緩沖流,提高讀寫效率
- 對於不同類型數據的統計,只讀取一次文件,提高效率
單元測試
單元測試使用JUnit5測試框架進行測試(步驟:構建測試對象 -> 測試函數正確性 -> 測試函數性能)
構建測試對象
構造Lib
對象供測試使用,由於Lib
在每次調用處理文件的時候都重新打開文件並初始化參數,故放在@BeforeAll
中構造
private static Lib lib;
@BeforeAll
static void setUp() {
lib = new Lib(inFile, outFile);
}
測試函數正確性
-
測試統計字符數正確性,其中包括了可能出現的:字母、數字、空白字符(h,e]l8\n)
- 使用
assertEquals
測試函數比對正確行數與結果單行數
/** * 測試統計字符數 */ @Test void testProcessCharNum() throws IOException { assertEquals( testString.length() * loopCount, lib.getCharNum() ); }
- 使用
-
測試統計單詞數正確性,其中包含了可能出現的:開頭不足四個英文字母的詞語、被分隔符分割的詞語( h,e]l8 wordne[ss1\n fqsq1a \n\n)
- 使用
assertEquals
測試函數比對正確單詞數與結果單詞數
/** * 測試統計單詞數 */ @Test void testProcessWordNum() throws IOException { assertEquals( wordNum * loopCount, lib.getWordNum() ); }
- 使用
-
測試統計詞頻前十正確性,其中設置了11個單詞,每個單詞的詞頻、字典順序都不同
- 使用
assertEquals
測試函數對比正確單詞排序順序與結果單詞排序順序
/** * 測試統計單詞頻率前10 */ @Test void testProcessWordRank() throws IOException { Map<String, Integer> topWord = lib.getTopWord(); topWord.forEach(new BiConsumer<String, Integer>() { int index = words.length - 1; @Override public void accept(String s, Integer integer) { assertEquals(index + 1, integer); assertEquals(words[index--], s); } }); }
- 使用
-
測試統計單詞時出現大寫的情況
- 使用
assertEquals
測試函數對比單詞正確數目與結果數目
/** * 大小寫測試 */ @Test void testProcessCapital() throws IOException { assertEquals( words.length * loopCount, lib.getWordNum() ); assertEquals(words.length, lib.getTopWord().keySet().size()); }
- 使用
-
測試統計詞頻前十的排序正確性
- 使用
assertTrue
測試函數對比單詞字典順序、出現頻率
/** * 測試統計單詞頻率前10的排序正確性 */ @Test void testProcessWordRankSort() throws IOException { Map<String, Integer> topWord = lib.getTopWord(); topWord.forEach(new BiConsumer<String, Integer>() { int lastVal = 11; String lastKey = ""; @Override public void accept(String s, Integer integer) { assertTrue(lastVal >= integer); if (lastVal == integer) { assertTrue(lastKey.compareTo(s) < 0); } lastKey = s; lastVal = integer; } }); }
- 使用
-
測試統計有效行數,其中包括只有空白字符的空行、無字符空行(h,e]l8 wordne[ss1\n fqsq1a \n \t \n)
- 使用
assertEquals
測試有效行數正確性
/** * 測試統計有效行數 */ @Test void testProcessLineNum() throws IOException { assertEquals( wordNum * loopCount, lib.getLineNum() ); }
- 使用
測試函數性能
- 主要測試process處理全過程的用時(ms),比如
@Test
void testProcessPerformance0() throws IOException {
long time = System.currentTimeMillis();
lib.process();
lib.output();
System.out.println("use:" + (System.currentTimeMillis() - time) + "ms");
}
- 構造字符串為
h,e]l8 wordne[ss1\n fqsq1a \n \t \n
- 以下全部使用該寫法,故下方省略代碼
-
測試2w個6字符長單詞的處理全過程運行時長(另外有其它不能組成單詞的英文、數字、空白字符,下同)
用時
use:278ms
運行結果文件
characters: 340000 words: 20000 lines: 20000 fqsq1a: 10000 wordne: 10000
-
測試20w個6字符長單詞的處理全過程運行時長
用時
use:957ms
運行結果
characters: 3400000 words: 200000 lines: 200000 fqsq1a: 100000 wordne: 100000
-
測試20w個12字符長單詞的處理全過程運行時長
用時
use:1002ms
運行結果
characters: 4600000 words: 200000 lines: 200000 fqsq1afqsq1a: 100000 wordnewordne: 100000
-
測試200w個6字符長單詞的處理全過程運行時長
用時
use:3504ms
運行結果
characters: 34000000 words: 2000000 lines: 2000000 fqsq1a: 1000000 wordne: 1000000
測試總結
200w行運行性能檢測
覆蓋率
Lib
類中沒有測試到的方法/行
- 一些無需測試的getter/setter
- I/O異常catch塊
異常處理
Lib
出現並需要開發者處理的異常只有I/O異常
Lib
對於文件的處理中可能出現的異常,采用先catch異常,正常關閉文件,再將異常拋出到外層,給開發者處理
心路歷程與收獲
收獲
- 學會了如何使用JUnit進行單元測試
- 了解了白盒測試
- 進一步掌握了正則表達式處理的方法
- 了解並初步使用了語言新特性來更方便處理
Map
的排序
歷程
- 前期分析設計不得當,還有技術上對
readline
函數不夠熟悉,符合不了我的需求,導致后面重新修改了一次架構 - 因為開發中修改了架構,以及各種小地方修補,導致測試與開發的同步未能如約進行
- PSP的估計耗時與實踐耗時偏差大
可改進的地方
- 前期應該花更多的時間進行架構設計,還要充分了解框架提供的API后再進行對應編碼
- 應該增加更多測試來驗證正確性