軟件工程實踐項目-WC(Java實現)
軟件工程實踐項目-WC(Java實現)
本文項目Github地址:https://github.com/MeiJinpen/wc
要求
- 基本要求
- [x] -c 統計文件字符數 (實現)
- [x] -w 統計文件詞數 (實現)
- [x] -l 統計文件行數(實現)
- 擴展功能
- [ ] -s 遞歸處理目錄下符合條件得文件(實現)
- [ ] -a 返回文件代碼行 / 空行 / 註釋行(實現)
- [x] 支持各種文件的通配符(*,?)(實現)
- 高級功能
- [ ] -x 圖形化界面(未實現)
PSP
PSP2.1 | Personal Software Process Stages | 預估耗時(分鐘) | 實際耗時(分鐘) |
---|---|---|---|
Planning | 計劃 | 30 | 60 |
· Estimate | · 估計這個任務需要多少時間 | 30 | 60 |
Development | 開發 | 855 | 1110 |
· Analysis | · 需求分析 (包括學習新技術) | 60 | 60 |
· Design Spec | · 生成設計文檔 | 30 | 60 |
· Design Review | · 設計復審 (和同事審核設計文檔) | 30 | 30 |
· Coding Standard | · 代碼規範 (為目前的開發制定合適的規範) | 30 | 30 |
· Design | · 具體設計 | 45 | 30 |
· Coding | · 具體編碼 | 360 | 480 |
· Code Review | · 代碼復審 | 60 | 60 |
· Test | · 測試(自我測試,修改代碼,提交修改) | 240 | 360 |
Reporting | 報告 | 90 | 150 |
· Test Report | · 測試報告 | 30 | 60 |
· Size Measurement | · 計算工作量 | 30 | 60 |
· Postmortem & Process Improvement Plan | · 事後總結, 並提出過程改進計劃 | 30 | 30 |
合計 | 975 | 1320 |
解題思路
剛剛拿到這個項目時,想到的是先分析該項目的需求,了解清楚具體需求後,再思考項目的架構該如何規劃,才能使得項目可讀性可維護性強。比如這個項目,就可以按照功能模塊分開,如每一個參數代表著一個功能,可以把每個功能解耦,使得後面修改代碼時更加清晰易懂。然後架構分析完成後,再進一步分析每個功能的具體需求實現,如果碰到一些需求暫時無法想出解決方案時,可以善於利用搜索引擎,找到合適的解決方案,明白如何去實現某個功能,這些都可以記錄下來,做成流程圖。對於一些功能,可能會接觸到新的技術,比如此項目中的正則表達式,可以先去學習一下使用方法,熟悉使用後在運用在項目中,此時就可以動手開發了。開發過程中,可能會遇到各種問題,有些問題如果思考一段時間後還不能解決,可以上網查找解決方案,因為大部分我們遇到的問題,網上都會有人遇到並解決後記錄下來。
設計實現過程
本項目可以簡單的分為兩個模塊:1.對參數指令的解析操作;2.對指定文件或匹配到的文件內容進行統計操作
第一個模塊:對參數指令的解析操作。由於本項目用到的指令其實不多,所以我統一使用了一個Constant
的常量類存放指令操作符,並在Main函數執行後對輸入的參數進行處理,轉換為常量表示,這樣更利於對每個操作的判斷。根據使用到的參數會統一記錄到WordCount的成員變量中存儲,以便後續對文件統計操作時使用。
第二個模塊:對指定文件或匹配到的文件內容進行統計操作。由於可以通過指令“-s”去指定通配符文件查詢,這裏我分開處理。當不指定“-s”參數時,也就是對指定文件進行統計處理,就直接使用了IO去操作文件讀取並統計;當指定“-s”參數時,由於可能是匹配到多文件操作,使用到了線程池去優化IO操作,並發處理不同文件的統計。然後IO讀取文件時,通過參數的指定,去判斷需要執行哪些統計,比如制定了“-w -a”,則會去統計單詞數和代碼行、空行和註釋行。這樣就可以避免未指定參數卻全部統計導致效率變慢的問題。對於統計功能的實現,分功能實現在不同方法中,使用了正則表達式處理字符串。
類關系圖如下:
主要流程圖:
代碼說明
程序啟動Main.java:
public class Main {
/**
* 程序入口
* @param args 第一參數:{-c, -w, -l, -s, -a};第二參數:[fileName]
*/
public static void main(String[] args) {
WordCount wordCount = new WordCount();
if (args == null || args.length == 0) {
System.out.println("需要參數{-c, -w, -l, -s, -a} [fileName],請重新輸入");
} else {
wordCount.count(args[args.length - 1], args);
}
}
}
WordCount.java
/**
* 統計文件的字符數、單詞數和行數等
* @param fileName 文件名
* @param strs 參數數組
*/
public void count(String fileName, String... strs) {
checkParams(strs);
if(isFuzzyQuery) {
//遞歸處理文件
countMultiFile(fileName);
} else {
//處理單文件
countSingleFile(fileName, new CountResult(this));
}
}
統計文件的總入口,通過“-s”去調用不同的功能。
/**
* 統計單文件
* @param fileName
* @param countCallback
*/
private void countSingleFile(String fileName, Callback countCallback) {
Count count = new Count();
File file = new File(fileName);
if(!file.exists()) {
countCallback.onError("文件不存在,請重試");
return;
}
BufferedReader reader = null;
FileReader fileReader = null;
try {
fileReader = new FileReader(file);
reader = new BufferedReader(fileReader);
String line;
while ( null != (line = reader.readLine())){
if(isCountLine)
count.lineCount = count.lineCount + addLineCount();
if(isCountWord)
count.wordCount = count.wordCount + getWorkCount(line);
if(isCountChar)
count.charCount = count.charCount + getCharCount(line);
if(isCountComplexLine) {
count.codeLineCount = count.codeLineCount + addCodeLine(line);
count.blankLineCount = count.blankLineCount + addBlankLine(line);
count.commentLineCount = count.commentLineCount + addCommentLine(line);
}
}
countCallback.onSuccess(count, fileName);
} catch (IOException e) {
countCallback.onError("文件讀取出錯,請重試");
} finally {
FileUtil.closeIOs(fileReader, reader);
}
}
單文件統計,IO打開文件並逐行讀取並統計數據。
/**
* 並發統計多文件
* @param fileName
*/
private void countMultiFile(String fileName) {
File directory = new File(""); //設定為當前文件夾
List<String> files = new ArrayList<>();
FileUtil.findFiles(directory.getAbsolutePath(), fileName, files);
//得到文件集合後,並發處理,提高效率
if(files.size() == 0) {
System.out.println("無法匹配到適合的文件");
return;
}
for (String name: files) {
executor.execute(() -> countSingleFile(name, new CountResult(this)));
}
//開啟線程池執行任務後,關閉線程池釋放資源
executor.shutdown();
try {
boolean loop = true;
while (loop) {
loop = !executor.awaitTermination(2, TimeUnit.SECONDS); //超時等待阻塞,直到線程池裏所有任務結束
} //等待所有任務完成
} catch (InterruptedException e) {
e.printStackTrace();
}
}
為了提高效率,並發處理多文件統計,把每個文件統計放入線程池的線程中執行,最後等待文件統計結束後Main線程等待防止子線程未完成就結束了。
功能實現:
-c 統計文件字符數
private int getCharCount(String line) { //空行算一個字符:“\n” if(line.isEmpty()) { return 1; } return line.length() + System.lineSeparator().length(); }
-l 統計文件行數
private int addLineCount() { return 1; }
-w 統計文件單詞數
private int getWorkCount(String line) { //把所有除了字母以外的字符都去掉 line = line.replaceAll("[^a-zA-Z]", " "); //把多於兩個以上的空格全部轉化為一個空格 line = line.replaceAll("\\s+"," "); //去掉首部和尾隨的空格 line = line.trim(); //用空格分隔單詞 String[] words = line.split("[\\s+,.]"); //如果為空行,則返回0 if(words[0].equals("")) { return 0; } return words.length; }
-a 統計文件空行,代碼行,註釋行
/** * 判斷是否是代碼行,是則返回1 */ private int addCodeLine(String line) { if(addBlankLine(line) == 0 && addCommentLine(line) == 0) { return 1; } return 0; } /** * 判斷是否是空白行,如果包括代碼,則只有不超過一個可顯示的字符,例如“{”。 */ private int addBlankLine(String line) { if(line.isEmpty()) return 1; if(!line.matches("[a-zA-Z]") && (line.trim().equals("{") || line.trim().equals("}"))) { return 1; } return 0; } /** * 判斷是否是註釋行,是則返回1 */ private int addCommentLine(String line) { line = line.trim(); //匹配“//”單行註釋或“} //”情況 if(line.matches("}*\\s+//?.+")) { return 1; } //匹配“/**/”和“/** * */”的情況 if(line.matches("((//?.+)|(/\\*+)|((^\\s)*\\*.+)|((^\\s)*\\*)|((^\\s)*\\*/))+")) { return 1; } return 0; }
遞歸文件匹配查詢(通配符查詢)
/** * 遞歸查找匹配的文件 * @param dirName 某個目錄下 * @param fileName 待匹配的文件(可帶通配符) * @param files 存放匹配成功的文件名 */ public static void findFiles(String dirName, String fileName, List<String> files) { String tempName; //判斷目錄是否存在 File baseDir = new File(dirName); if (!baseDir.exists() || !baseDir.isDirectory()){ System.out.println("找不到該目錄:" + dirName ); } else { String[] fileList = baseDir.list(); if(fileList == null) return; for (String s : fileList) { File readFile = new File(dirName + "\\" + s); if (!readFile.isDirectory()) { tempName = readFile.getName(); if (wildcardMatch(fileName, tempName)) { //匹配成功,將文件名添加到文件列表中 files.add(readFile.getAbsoluteFile().getAbsolutePath()); } } else if (readFile.isDirectory()) { findFiles(dirName + "\\" + s, fileName, files); } } } } /** * 通配符匹配(參考網上) * @param pattern 通配符模式 * @param str 待匹配的字符串 * @return 匹配成功則返回true,否則返回false */ private static boolean wildcardMatch(String pattern, String str) { int patternLength = pattern.length(); int strLength = str.length(); int strIndex = 0; char ch; for (int patternIndex = 0; patternIndex < patternLength; patternIndex++) { ch = pattern.charAt(patternIndex); if (ch == ‘*‘) { //通配符*表示可以匹配任意多個字符 while (strIndex < strLength) { if (wildcardMatch(pattern.substring(patternIndex + 1), str.substring(strIndex))) { return true; } strIndex++; } } else if (ch == ‘?‘) { //通配符?表示匹配任意一個字符 strIndex++; if (strIndex > strLength) { return false; } } else { if ((strIndex >= strLength) || (ch != str.charAt(strIndex))) { return false; } strIndex++; } } return (strIndex == strLength); }
測試
程序測試
測試-w , -l -c 功能截圖:
測試-a 功能截圖:
測試全功能截圖:
代碼覆蓋率測試
使用測試-w,-l,-c,-a,-s 測試項目下的所有.java文件,測試代碼覆蓋率如下:
總結
總體來說,該項目受益良多,之前也做過類似的項目,但並沒有按照軟件工程的操作來做,急於完成需求,導致做出來的項目需要不停的測試與修改,花費的時間更多,但這次合理的規劃時間使得該項目能夠更快的完成。而且此項目也讓我學習到了挺多新東西,比如正則表達式的使用,線程池的使用等,這些都說明要使得技術不斷進步,就必須實踐多點項目,總結多點項目經驗。雖然和計劃的對比還是效率不夠好,但這是剛剛開始實踐這種模式的項目開發,後續可以更快更好的去分配時間開發項目。
軟件工程實踐項目-WC(Java實現)