作業基本信息
這個作業屬於哪個課程 | 2021春軟件工程實踐|W班(福州大學) |
---|---|
這個作業要求在哪里 | 寒假作業2/2 |
這個作業的目標 | 學習《構建之法》、邊完成詞頻統計作業邊學習相關知識 |
其他參考文獻 | 《構建之法》 |
目錄:
- 我對《構建之法》的幾個疑問
1.關於書本第四章4.1代碼規范的一個疑問
2.‘從用戶的角度考慮問題’具體可以有什么角度?
3.關於書本第十三章“驗收測試”中提到的“可用”→“預覽版”的疑問
4.對於16章創新16.1.5“迷思之五”的解釋,我有其他見解
5.關於17章提到的團隊合作的幾個階段,作為團隊的一個普通成員(而不是領導者),要如何順利的完成過渡?
6.附加題:冒泡、快速排序的起源 - WordCount編程
1.Github項目地址
2.PSP表格
3.解題思路描述
4.代碼規范制定鏈接
5.設計與實現過程
6.性能改進
7.單元測試
8.異常處理說明
9.心路歷程與收獲
一、我對《構建之法》的幾個疑問
1.關於代碼規范的一個疑問
對於第四章4.2中的4.2.4 “斷行與空白的{}行” 中提到的標准我表示不贊同。
雖然代碼規范因人而異,書中提到格式C不夠清晰,進而選擇更清晰的格式D。
本人以前也常用格式D,但是后面改成了格式C,因為很多的語言書中都采用了格式C,並且格式C也屬於現在大家比較公認規范的一個標准,以下兩個例子一個是網上找的規范的例子,另一個是我用IDEA自動生成的代碼,兩種都用的是格式C,如果格式C不好的話,為什么IDEA的自動生成要用格式C而不是格式D呢?所以我認為格式C才是更好的標准,格式D過於發散。
2.‘從用戶的角度考慮問題’具體可以有什么角度?
第十二章12.1中的12.1.2 “從用戶的角度考慮問題” 中提到了 “設計不同於傳統的數學題,是沒有唯一的標准答案的”。后又舉了郵箱地址、翻譯等例子,但是看完后這些內容我只知道要從用戶的角度考慮問題,我還是不知道具體要怎么考慮。
在搜索資料的同時,我發現書后12.3評價標准中作者的總結解決了我的大部分疑惑。作者列舉了“盡快提供可感觸的反饋系統狀態”、“用戶有控制權”、“一致性和標准化”等原則,讓我對這個問題有了比較清楚的認識。
除了作者自身總結,我還通過網上搜索,發現了更多的考慮角度。簡單來說,就是控制感、歸屬感、驚喜感、沉浸感。
控制感:給予用戶控制感,讓用戶做用戶想做的事情就是好的用戶體驗。
歸屬感:抽象上來說是一種意識形態上的認同感。舉例就是“母校是一個自己天天罵三百遍,但別人罵一句就能拼命的地方”。
驚喜感:產品能在不經意的某一步超出用戶的心理預期,觸達用戶心理最柔軟的那片地方。
沉浸感:(我自己總結起來就是)傻瓜式操作+及時反饋+無其他信息干擾=上癮/沉浸
3.關於書本第十三章“驗收測試”中“可用”→“預覽版”的疑問
在書本13.2“各種測試方法”的“驗收測試”中提到了——如果所有場景都能通過,就是“可用”的,這種版本也就是“社區預覽版”和“技術預覽版”的由來。那么,既然已經“可用”了,怎么還是“預覽版”,而不是“正式版”。如果這樣都不能達到“正式版”的要求,那我們得達到什么要求才能把版本當作“正式版”?
通過查詢網上資料,我了解到了“預覽版”和“正式版”的定義。
預覽版:尚未穩定的測試版。主要用於軟件未來版本的改善與修正。
正式版:總結了之前預覽版的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項目地址
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.解題思路描述
- IO部分
讀取文件,用流讀取,一開始看到要讀取字符,於是就決定分別使用FileReader、FilerWriter,改進后換成BufferReader、BufferWriter並封裝成專門的函數獲取。
- 統計字符數
統計ASCII碼,用read()讀取字符,讀到一個字符,字符數加1,后面發現可以先把字符全部讀出並拼接到StringBuffer中,再獲取字符串長度length即可,改進后將StringBuffer替換成了StringBuilder。
- 統計單詞數
先編寫簡單的判斷類isAlpha()、isNum()函數,分別用於判斷是否同時出現四個連續的英文字母,並且這四個英文字母前面也必須是分隔符,用StringBuffer不斷拼接直到分隔符為止。把獲得的單詞填充到Map<String,Integer>中,value為出現次數(這個部分耗費時間最久,設計邏輯耗時,改進結構時也耗費較長時間)
- 統計有效行數
一開始審題有誤,認為只要有字符(除了換行符)都算是有效行數,后面發現非空白字符行才算是有效行數,因此,寫了一個簡單的判斷函數isBlank()用於判斷空白符。(不過由於自己的失誤,一個if忘記接else導致判斷語句一直無法成功跳轉,卡了很久。)
- 打印詞頻最高的十個單詞
從第3步獲得的Map中獲取詞頻,自定義一個比較器,詞頻越高排序越前,詞頻相同則按照字典序,轉換成一個List,再按順序輸出前十個單詞及其詞頻。
4.代碼規范制定鏈接
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 {…}
}
(因為博客園流程圖顯示不出來,只能截圖作業部落的預覽流程圖過來)
下面是核心函數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.性能改進
- 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);
}
- 單次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));
}
- 流中獲取字符串加快
使用StringBuilder代替StringBuffer類
//StringBuffer str = new StringBuffer();
StringBuilder str = new StringBuilder();
- 字符串轉換成單詞(字符串掃描)的次數減少
將統計單詞數和打印詞頻之前的字符串掃描提前到原本的函數之前,分離出handleWords()函數,減少了一次掃描時間。
Map<String, Integer> map = Lib.handleWords(str);
int words = Lib.countWords(map);
String freq = Lib.printWords(map);//減少了一次handleWords的時間
- 性能測試
測試一
250萬字符,50萬單詞(較規則),50萬行,讀了0.245s(以下均是反復測后取的穩定數據)
測試二
5000萬字符,1千萬單詞(較為規則),1千萬行,讀了2.2s
測試三
下面這個例子的輸入文件由李宇琨同學友情贊助。
1.47億字符,1千萬+單詞(極不規則),1千萬行,讀了9.5s
7.單元測試
- 單元覆蓋率截圖
分別為LibTest中測試和WordCount中測試的截圖
下面是其中兩個測試代碼,分別為打印詞頻測試、整體運行測試
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);
}
- 覆蓋率未滿原因分析
- try\catch塊中的Exception異常沒有覆蓋到
- if\else塊中的另一分支只有在極少數的條件下才會觸發(僅當只有一行,且這一行所有字符合成恰好是個單詞,才會觸發)
- 正確性測試
如果能正確的讀到空白符,則以空白間隔的字母不會合成一個單詞
如果超過十個單詞,是否會輸出超過十個詞頻,不超過10個則正確
如果只有一行,一種字母,一個單詞的情況(這個例子可以解決上面測試的時候單元測試沒覆蓋到的哪個if分支)
如果兩個詞頻率相同,按字典序輸出,則正確
單詞前后必須是分隔符才算是單詞,且必須至少是四個字母開頭
連續按出三個制表符\t,如果是三個字符,則正確
所有行都是n個空格,如果有效行數是0,才正確
包含多種特殊情況,綜合測試。
幾種重復單詞多次大量出現,且要正確忽略大小寫
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");
}
- 大量的不規則或規則數據,要正確統計
8.異常處理說明
只有利用現有的Exception:IOException和FileNotFoundException,如果文件沒有找到,或者命令行參數輸入參數個數不足兩個,就會拋出異常。
測試過程中在try/catch塊中使用了Exception,沒有自定義特殊異常類。
代碼中存在處理特殊情況的代碼,但是屬於正常的輸入內容,沒有納入異常處理范圍(只是較為特殊,並非不合法)
9.心路歷程與收獲
養成良好的代碼規范習慣十分重要,通過編寫codestyle.md並且規范自己的代碼,我覺得代碼的可讀性更高,而且代碼寫起來也更優美,不會雜亂無章
Git作為一個版本控制系統,在項目開發過程中,對我的幫助很大,在用Github Desktop進行commit的過程中還能知道自己的代碼到底是怎么樣發生了變化,整體變化會更加清晰,同時我也推薦大家使用Github Desktop,真的很好用
往后要加強邏輯的思考,在項目開發的過程中,出現好多次條件判斷錯誤,致使項目開發受阻。對於這點,我覺得可以采用設計前,先拿個紙筆過來動手寫一下要考慮的要點,再實際編寫代碼,避免疏漏過多。
單元測試很重要,以前寫程序沒有足夠重視這一點,單元測試在開發過程中不可缺少,要善於使用單元測試來驗證程序的合理性、正確性。
PSP表格預估的時間與自己的實際使用時間有較大的出入,對自己的評估還不夠准確。