1. 程式人生 > >軟件工程實踐項目-WC(Java實現)

軟件工程實踐項目-WC(Java實現)

ddb 使用 odin ace rup find sys tca sub

軟件工程實踐項目-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實現)