這個作業屬於哪個課程 | 2021春軟工實踐|W班 (福州大學) |
---|---|
這個作業的要求在哪里 | 寒假作業2/2 |
這個作業的目標 | 任務一:閱讀《構建之法》並提問 任務二:完成詞頻統計個人作業 |
其他參考文獻 | 無 |
任務一:閱讀《構建之法》並提問
一、基本要求
1.我看到這段文字(2.5 散伙階段 有些團隊過不了磨合這一道門坎, 退化到 “散伙階段” - 有些名存實亡的 “團隊” 就剩下一兩個人干活, 其他人在打醬油, 並且還抱怨其他伙伴不重視他們。 這些情況在我們學生項目中是不是很常見呢?)。有這個問題(該如何避免團隊從磨合階段進入到散伙階段?),我查了資料,有這些說法(1.談好規則,定好規則。2.包容忍讓。3.增強團隊凝聚力)。但是根據我的實踐,我得到了這些經驗(一個團隊中難免會有人划水,即使已經分配好了任務,制定好了規則,也會出現就幾個人在努力搬磚的情況)。我的困惑是(如何使一個團隊中的每個人各施其職,能融洽的交流配合,共同完成項目的構建)。
2.我看到這段文字(在《現代軟件工程》 這門課里, 同學們不能穿新鞋, 走老路 - 學習了很多新技術, 新的開發模式, 新的團隊管理方法, 卻做一個毫無新意, 沒人使用, 演示后就扔掉的東西 例如: 虛擬的學籍管理系統, 圖書館管理系統…)。 我們要做實用並且創新的項目。 那我們怎么才能想出靠譜的想法, 然后有條理地說服別人? 在宿舍里睡覺, 聚餐, 喝酒, 搞頭腦風暴?),有這個問題(我們如何想出一個具有新意,而且具有使用價值的軟件?),我查了資料,有這些說法(1.通過問卷調研用戶的需求。2.跟用戶面談,了解用戶需求)。根據我的實踐,我得到了這些經驗,雖然用過問卷可以收集到用戶的需求,但是各個用戶的需求不一樣,而且用戶的需求不一定有實現的價值。但我還是不太懂,我的困惑是(在接下來的團隊作業中,該如何設計一個具有新意而且有價值的軟件,而不是為了應付作業)。
3.我看到這段文字(1990年代, 韋爾奇注意到核磁共振機器的通道特別狹窄, 在長達幾十分鍾的檢查過程中, 病人常常有得了幽閉恐懼症的感覺。 傑克做過類似的檢查, 深有體會。他問, 能不能把通道做得大一些? 專家說那樣會降低掃描成像的質量。)有這個問題(我們在用戶的體驗和質量不能得兼的情況下,我們該如何進行權衡?)。經過我的實踐,得到的經驗是,在之前的項目中,在限時的情況下,如果花時間去改善用戶體驗,有可能會導致代碼的質量下降。我的困惑是(在接下來的項目中,如果為了提高用戶的體驗則要花更多的時間去設計界面,同時擴展新功能,但是在有限的時間中,這會影響到軟件的質量,這該如何選擇)。
4.我看到了這段文字(誰不喜歡創新呢? 然而細細想來, 創新就是做和以前不一樣的事, 並不是所有的人都喜歡“不一樣“。 當你提出一個創新的想法時, 你會得到什么回答呢? 下面是一些:This will never work、No one will want this等),有了這個問題(如果我有一個創新的想法(比如下學期的團隊作業我想做+個校園閑置物品交易平台),要如何跟我的團隊溝通使他們相信這個想法具有可行性)。根據我的實踐,大多數創新的想法總會被團隊以各種理由否決,即使想法具有可行性但是團隊也會因為怕麻煩而否決。我的困惑是(每個人都有創新的想法,但是要如何確定想法的可行性以及協調一整個團隊去嘗試實現這個想法)。
5.我看到了這段文字(那一個剛入行的初級軟件工程師如何成長呢? 我認為成長有下面幾種:1. 知識: 對具體技術的掌握, 動手能力2. 經驗: 對問題領域的知識和經驗的積累 (例如: 對於醫療行業的了解, 對於金融行業的了解)。3. 通用的軟件設計思想, 軟件工程思想的提高4. 職業技能 (區別於技術技能)職業技能包括: 自我管理的能力; 表達和交流的能力; 與人合作的能力; 把任務按質按量完成的執行力; 這些能力在IT 行業和其它行業都很重要。)我有了這個問題(作為剛入行的軟件工程師,我們將如何高效率地提高我們的能力?),根據我的實踐,我在學習各種框架的時候,通過不斷的編碼,提供了我編程的能力以及使我對框架的結構有了更深刻的理解。我的困惑是(雖然通過學習各種框架可以提高編程能力與編程思維,但是總覺得效率不太高,該采取什么方法才能高效率的進行學習,提高能力)。
6.我看了這段文字(每人在各自獨立設計、實現軟件的過程中不免要犯這樣那樣的錯誤。在結對編程中,因為有隨時的復審和交流,程序各方面的質量取決於一對程序員中各方面水平較高的那一位。這樣,程序中的錯誤就會少得多,程序的初始質量會高很多,這樣會省下很多以后修改、測試的時間。具體地說,結對編程有如下的好處:(1)在開發層次,結對編程能提供更好的設計質量和代碼質量,兩人合作能有更強的解決問題的能力。(2)對開發人員自身來說,結對工作能帶來更多的信心,高質量的產出能帶來更高的滿足感。(3)在心理上, 當有另一個人在你身邊和你緊密配合, 做同樣一件事情的時候, 你不好意思開小差, 也不好意思糊弄。(4)在企業管理層次上,結對能更有效地交流,相互學習和傳遞經驗,能更好地處理人員流動。因為一個人的知識已經被其他人共享。總之,如果運用得當,結對編程能得到更高的投入產出比),有了這個問題(在接下來的團隊編程中,采取結對的編程模式是否具有可行性,是否能減少開發時間和提高代碼質量,比如兩個人做ui,兩個人做前端,兩個人做后端,一個人架構等),根據我的實踐,在之前的上機課中,與同學討論會有效的減少bug,但是會花費更多的時間,在有限的時間里這樣反而利大於弊,所以我打算在這學期的團隊作業中嘗試采取結對編程的方法。我的困惑是(對於一個團隊來說,是單人開發的收益高還是結對開發的收益高)
二、附加題
馬化騰稱,當初創業的時候,要做到3萬用戶,於是一個一個的去學校拉用戶,但是按照這樣的方法,想要達到3萬用戶的目標至少要兩年的時間,這么久的話公司就死掉了。后來做出不同的項目就一個接一個的賣掉。不停地去網上推廣,后來用戶數量上來了,但是聊天的人很少,馬化騰就自己和別人各種聊天,有時候還要換頭像,假扮成女生在社區內聊天,就為了顯得社區內很熱鬧。馬化騰也表示當時自己不敢寫總經理的職位,因為總經理不能出來自己干活,只能寫是技術工程師。
見解:創業是一件艱苦和勞累的事,往往不但要花費大量經歷,還要投入大量時間,要有堅強的意志以及堅定的決心。馬化騰曾經想把QQ賣掉,但是還是咬牙堅持了下來,才成就了中國使用的人最多的社交軟件。
任務二:WordCount編程
Github項目地址
PSP表格
PSP2.1 | Personal Software Process Stages | 預估耗時(分鍾) | 實際耗時(分鍾) |
---|---|---|---|
Planning | 計划 | 5 | 3 |
• Estimate | • 估計這個任務需要多少時間 | 5 | 3 |
Development | 開發 | 245 | 331 |
• Analysis | • 需求分析 (包括學習新技術) | 10 | 12 |
• Design Spec | • 生成設計文檔 | 15 | 15 |
• Design Review | • 設計復審 | 5 | 3 |
• Coding Standard | • 代碼規范 (為目前的開發制定合適的規范) | 15 | 24 |
• Design | • 具體設計 | 10 | 12 |
• Coding | • 具體編碼 | 120 | 155 |
• Code Review | • 代碼復審 | 10 | 10 |
• Test | • 測試(自我測試,修改代碼,提交修改) | 60 | 100 |
Reporting | 報告 | 30 | 43 |
• Test Repor | • 測試報告 | 15 | 21 |
• Size Measurement | • 計算工作量 | 10 | 12 |
• Postmortem & Process Improvement Plan | • 事后總結, 並提出過程改進計划 | 5 | 10 |
合計 | 280 | 377 |
解題思路描述
當看到題目的第一個想法就是,這是一個命令行程序,而且要進行文件讀寫,所以要用args[ ]傳入輸入文件和輸出文件。然后,接下來就是去實現題目要求的功能,功能設計中最主要的部分應該是字符的過濾(過濾掉非法字符)和拼接(將合法字符拼接成單詞)。再者,就是選擇文件的Reader和Writer。因為主要是字符的處理,我選用能能進行單個字符處理的Reader和Writer。查詢資料的話我用的主要是CSDN和博客園。
代碼規范制定鏈接
設計與實現過程
首先,設計了Lib類和WordCount類這兩個類和10個函數,Lib類中有許多接口的實現,WordCount類是主類,調用Lib類的接口來完成所有功能。
wordsCount()和wordNum()會調用isValidChar()和isValidChars()來判斷是否為有效的單詞,isValidChars()會調用isNum()來判斷單詞前四個字母是否為數字
以下為接口的設計
//打開文件
public static Reader openInputFile(String fileName) {...}
//關閉文件
public static BufferedWriter openOutputFile(String fileName) throws IOException {...}
//統計字符數,空格,水平制表符,換行符,均算字符
public static int charactersCount(String inputFile, String outputFile) throws IOException {...}
//統計單詞總數,至少以4個英文字母開頭,跟上字母數字符號,單詞以分隔符分割,不區分大小寫
public static int wordsCount(String inputFile, String outputFile) throws IOException {...}
//統計行數,任何包含非空白字符的行,都需要統計。
public static int linesCount(String inputFile, String outputFile) throws IOException {...}
//統計單詞的出現次數(對應輸出接下來10行),最終只輸出頻率最高的10個。
public static Map wordNum(String inputFile, String outputFile) throws IOException {...}
//打印出頻率前十的單詞
public static void printWords(Map<String, Integer> map, Writer writer) throws IOException {...}
//判斷是否為有效的字符
public static boolean isValidChar(int temp) {...}
//判斷是否為有效的單詞
public static boolean isValidChars(char[] chars) {...}
//判斷是否為數字
public static boolean isNum(int temp) {...}
運行結果
characters: 148998191
words: 10031861
lines: 10000000
middle: 15624
part: 15619
color: 15528
full: 15528
card: 4184
body: 4176
shirt: 4173
style: 4170
wing: 4168
cream: 4165
這個測試的input.txt用的是宿舍群中的大文件,用於測試大文件是否會發生異常以及運行的時間,測試數據的構建思路主要是通過隨機數生成大量的單詞並輸出到input.txt中。
主要的函數有四個:
-
統計字符數:由於read會讀出每一個字符,所以用read可以方便的讀出所有字符。同時在這里沒有用openOutputFile(),是為了保證每次打開文件的時候可以先清空之前文件的內容再追加。
while ((temp = reader.read()) != -1) { num++; }
-
統計單詞總數:用了isValidChar()來判斷是否為有效字符(數字,英文字母),不是則被過濾掉。同時用isValidChars()來判斷是否為有效的單詞(至少以四個英文字母開頭)。
while ((temp = reader.read()) != -1) { while (isValidChar(temp)) {//將所有有效字符拼接成字符串 word += (char) temp; temp = reader.read(); } while (!isValidChar(temp) && temp != -1) {//去除所有空白字符和分隔符 temp = reader.read(); } char[] chars = word.toCharArray(); if (isValidChars(chars)) {//如果單詞合法,則單詞總數++ num++; } word = "" + (char) temp;//將讀取到的最后一個字符(下個單詞首字母)加到下一個單詞 }
-
統計行數:要求中只計算有效行,所以我用一個Line來保存每一行,如果這一行去除掉空白字符之后為空String,則不會被計數。
while ((temp = reader.read()) != -1) { while (temp != -1 && (char) temp != '\n') { if ((char) temp != ' ' && (char) temp != '\t' && (char) temp != '\r' && (char) temp != '\n') {//去除掉空白字符 line += (char) temp; } temp = reader.read(); } if (line != "") {//如果行非空,num++ num++; } line = "";//清空字符串 }
-
統計出現頻率前十的單詞:首先判斷出所有的單詞,然后將單詞轉成小寫,並且放入Map中,再將Map轉化為Set進行排序,最后再調用printWords()打印出頻率前十的單詞。
if (isValidChars(chars)) {//如果單詞合法,則單詞總數++ if (words.get(word) == null) {//如果map中沒有 words.put(word, Integer.valueOf(1)); } else { words.put(word, Integer.valueOf(words.get(word).intValue() + 1)); } }
Map<String, Integer> result = words.entrySet().stream() .sorted(new Comparator<Map.Entry<String, Integer>>() {//對Map進行排序 @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()); } else { return o2.getValue().compareTo(o1.getValue()); } } }) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (oldValue, newValue) -> oldValue, LinkedHashMap::new));
性能改進
仔細想了想這個程序可以優化時間的地方好像不多,我進行優化的地方是**
1.把Map轉為Set進行排序,使用了stream()節約了一點時間
Steam有以下好處:
- Stream不存儲元素
- Stream不會修改數據源,而是會產生一個修改后的Stream對象,進而鏈式調用
- Stream的執行具有延遲特性
因此可以使得對集合對象的操作更加高效和遍歷。
Map<String, Integer> result = words.entrySet().stream()
.sorted(new Comparator<Map.Entry<String, Integer>>() {//對Map進行排序
@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());
} else {
return o2.getValue().compareTo(o1.getValue());
}
}
})
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
(oldValue, newValue) -> oldValue, LinkedHashMap::new));
使用stream處理6萬個單詞花費的時間:
使用普通的Map排序處理6萬個單詞花費的時間:
2.用BufferedWriter代替FileWriter
public static BufferedWriter openOutputFile(String fileName) throws IOException {
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(fileName),true),"utf-8"));
return bw;
}
FileWriter每次調用write()方法,就會調用一次OutputStreamWriter中的write()方法
而BufferedWriter只有在緩沖區滿了才會調用OutputStreamWriter中的write()方法
在大量寫入文件的時候write()的執行次數和所花費時間會有巨大差別
但是在該案例中只輸出13行,效果不明顯
單元測試
單元測試的代碼
@org.junit.jupiter.api.Test
void wordNum() throws IOException {
String inputFile = "C:\\Users\\WWJ20\\IdeaProjects\\WordCount\\src\\input.txt";
String outputFile = "C:\\Users\\WWJ20\\IdeaProjects\\WordCount\\src\\output.txt";
Lib.wordNum(inputFile, outputFile);
}
-
單元測試的函數主要是通過固定的絕對路徑訪問文件(因為main函數中是通過命令行輸入文件路徑的),然后在單元測試函數中調用Lib類中的該方法
-
構造測試數據的思路主要想的是這幾個方面
- 無效的單詞:故意構建無效的單詞看是否會被跳過,在這里我使用了手動構建錯誤情況並輸入input.txt
- 多個分隔符:在兩個單詞之間加入大量的分隔符看是否出現異常,在這里我也是手動構建錯誤
- 大量數據:通過大文件輸入大量的數據,判斷是否會發生異常,在這里我使用了宿舍群眾142MB的input.txt測試結果
-
單元測試得到的測試覆蓋率截圖
-
charactersCount()
-
wordsCount()
-
lineCounts()
-
wordNum()
-
-
如何優化覆蓋率?
- 方法體盡量小,一入一出,不要使用變量傳遞。
- 文件數據等測試,加快速度可以一次性加入內存跑。
- 盡量避免使用三目運算符,多IF條件判斷,可以使用枚舉+工廠類來規避,減少單元測試編寫難度。
異常處理說明
-
如果輸入文件不存在
try { reader = new InputStreamReader(new FileInputStream(file)); } catch (FileNotFoundException e) { e.printStackTrace(); System.out.println("文件不存在"); }
console會打印出文件不存在
-
如果輸出文件不存在
系統會自動創建輸出文件
-
如果輸入文件中有錯誤信息
程序會過濾錯誤的信息
心路歷程與收獲
這次的作業讓我們了解了如何使用github發布項目和控制版本,同時也讓我們了解了一個代碼從無到有的規范流程。同時我們也制定了自己的代碼規范,以后寫代碼都將按照代碼規范來,使得自己的代碼清晰有條理。這次的代碼總體來說難度不高,但是幫助我們規范了寫代碼的習慣,這對我們以后的編程有着莫大的幫助。