1. 程式人生 > >系統分析與設計課程項目 WordCount

系統分析與設計課程項目 WordCount

方法 get 為我 wid des 多處理器 行數 多條 pub

系統分析與設計課程項目 WordCount

項目代碼地址:https://gitee.com/mxhkkk/Wc

PSP2.1表格

PSP2.1

PSP階段

預估耗時

(分鐘)

實際耗時

(分鐘)

Planning 計劃 30 30
Estimate 估計這個任務需要多少時間 30 20
Development 開發 0 0
Analysis 需求分析 (包括學習新技術) 30 20
Design Spec 生成設計文檔 0 0
Design Review 設計復審 (和同事審核設計文檔) 0 0
Coding Standard 代碼規範 (為目前的開發制定合適的規範) 20 20
Design 具體設計 60 180
Coding 具體編碼 60 120
Code Review 代碼復審 30 90
Test 測試(自我測試,修改代碼,提交修改) 90 30
Reporting 報告 90 120
Test Report 測試報告 0 0
Size Measurement 計算工作量 0 0
Postmortem & Process Improvement Plan 事後總結, 並提出過程改進計劃 30 30
合計470660

概述

  這個項目按照作業要求來說,分為三個階段,基礎功能、擴展功能和高級功能。雖然這一周只需要完成基礎功能,但在分析設計階段也要考慮到擴展功能和高級功能,以免日後添加功能使程序“傷筋動骨”。一開始我只是想設計出可以實現所有功能的程序結構,但實際上在這周內我幾乎完成了所有功能的編碼工作。不得不說程序難在設計,而不是編碼。

題目分析

簡單描述

  一個可執行的exe文件,我們在命令行啟動它並傳入參數(我把它看成是命令),若命令不要求啟動GUI,則讀取指定文件(可以使用通配符,可以是一個目錄下的多個文件),進行統計字符數、單詞數(可以過濾掉不需要統計的單詞)、行數、代碼行數、註釋行數和空行數,然後輸出到指定文件(若無指定文件,則輸出到默認文件);若命令要求啟動GUI,則啟動一個窗口,用戶可以通過鼠標操作選取文件(只能是單個文件),不能使用過濾單詞表,點擊OK,程序就會顯示文件的字符數、單詞數、行數等全部統計信息。

 無論是啟動GUI還是不啟動,程序的入口都是命令行,輸出則有不同。

 在我看來,對於GUI的要求要比命令行簡單的多,有點不理解為什麽把它當作高級功能。

詳細需求

  • 對程序設計語言源文件統計字符數、單詞數、行數,統計結果以指定格式輸出到默認文件中。
  • 可執行程序命名為:wc.exe,該程序處理用戶需求的模式為:
  • wc.exe [parameter] [input_file_name]
  • 存儲統計結果的文件默認為result.txt,放在與wc.exe相同的目錄下。
  • 輸出結果需要按照一定的順序,按照字符-->單詞-->行數-->代碼行數/空行數/註釋行的順序,依次分行顯示。顯示順序與輸入參數的次序無關

基本功能:

  • -c 統計字符數,空格,水平制表符,換行符,均算字符
  • -w 統計單詞數,由空格或逗號分割開的都視為單詞,且不做單詞的有效性校驗。
  • -l 統計總行數
  • -o 結果輸出文件,必須與文件名同時使用,且輸出文件必須緊跟在-o參數後面。

擴展功能:

  • wc.exe -s //遞歸處理目錄下符合條件的文件
  • wc.exe -a file.c //返回更復雜的數據(代碼行 / 空行 / 註釋行)
  • wc.exe -e stopList.txt // 停用詞表,統計文件單詞總數時,不統計該表中的單詞,
  • stopList.txt中停用詞可以多於1個,單詞之間以空格分割,不區分大小寫
  • -e 必須與停用詞文件名同時使用,且停用詞文件必須緊跟在-e參數後面

高級功能:

  • wc.exe -x //該參數單獨使用,如果命令行有該參數,則程序會顯示圖形界面,
  • 用戶可以通過界面選取單個文件,程序就會顯示文件的字符數、單詞數、行數等全部統計信息。

分析與設計

 程序的功能是不限於編程語言的,Java、C#、C++、C和Python等主流的編程語言都可以實現。因為我對Java比較熟悉,所以決定選擇Java(純Java)作為開發語言。

 IDE則選用流行的Eclipse,並使用Maven項目管理工具。還要使用一些UML畫圖工具。

 使用Java語言,自然是采用面向對象的設計方法。

 作業的要求是不能使用測試框架,那怎麽做測試呢?不使用框架意味著要額外做大量的工作,首先我寫了一些必要的assert方法,把他們放在一個類中,聲明為靜態,方便使用。在測試過程中還要寫許多模擬的對象,每個測試類都要有一個main函數來執行。

使用命令模式將請求的發送者和接收者解耦

  分析需求時,我首先想到的是使用命令模式,將操作的請求者與執行者之間解耦,先要解析命令行傳過來的參數,將參數封裝成命令,還要有一個Invoker的角色來執行命令,命令的執行則是調用handler來處理。

  命令行傳入的有關操作的參數都可以抽象成命令,如-c,統計字符數,封裝成CharCountCommand命令,通過調用CharCountHandler來完成操作;啟動GUI的參數也可以封裝成一條命令,ShowFrameCommand,將啟動GUI的代碼寫在ShowFrameHandler中來完成操作。

  這樣程序就可以按照提取命令,處理命令,收集結果、輸出結果這樣的流程來執行了。

類圖如下所示:

技術分享圖片

使用組合模式來統一對待結果單項和結果集

下面在來分析如何接收handler的處理結果,一個handler只處理一類問題,如CharCountHandler統計字符數並返回,BlackCountHandler統計空行數並返回,因此我確定一個handler返回一個ResultItem。一個command可能要調用對個handler,如-a參數的command需要統計代碼行數、空行數和註釋行數,這是三個問題,按照先前的設計需要調用三個handler來處理,因此我認為command應該返回ResultItem的一個集合ResultItems。Invoker可能要調用多個Command來完成多條參數指令的操作,它應該返回的是結果的結果集的結果集,這使我和惱火。這時,我想到了組合模式,將葉子和集合同一對待。

類圖如下所示:

技術分享圖片

使用策略模式來實現不同的調用策略

Invoker角色是比較復雜的,因為它涉及到多種不同的調用策略,如命令行任務,啟動GUI任務和啟動GUI後任務調用等,還有任務串行,還是並行的問題。因此我決定使用策略模式來設計。

類圖如下所示:

技術分享圖片

程序運行序列圖大致如下所示

技術分享圖片

命令行參數如何解析

命令行參數的解析一個難點,Apache的CLI是一個很好地命令行參數解析框架,可我並不打算使用它,自己動手寫parse類。

任務如何並行,提高執行效率

統計算法都封裝在handler中,我把統計任務細粒度化,一個handler只處理一種統計任務。每進行一項任務,handler都要讀一次文件,這會影響效率。如果只讀一次文件,就可以完成所有的統計工作,那麽效率會大大提升,有四種方案,一種是把統計任務的方法放進一個handler中,甚至是放入一個方法中,這樣做雖然會提升效率,但代碼復雜度上升了,面向對象設計原則被破壞了;另外一種方案是緩存文件,這對於小文件來說是可行的,但對於大文件則有可能會耗盡內存,程序被終止;還有一種方案是通過任務並行來解決,啟動多個線程(需要多處理器,現代計算機幾乎都是多處理器的),這樣效率就提升了,可是還有一個問題,多個線程讀取一個文件,肯定是要出問題的,多個線程使用多個文件連接則不允許,多個線程使用同一個文件連接則讀到的內容不全。最後的方案是一個線程讀取文件,其它多個線程訪問該線程讀取的內容並進行統計,這比較復雜,未能實現。
最後我選擇的方案是:用戶選取一個輸入文件時,則並行運行;選取多個輸入文件時,則為每一個文件分配一個線程,並行運行,可惜也還沒有實現。

程序運行效果

命令行界面

文件目錄結構

技術分享圖片

啟動命令

技術分享圖片

執行結果(輸出文件內容)

技術分享圖片

GUI界面

啟動命令

技術分享圖片

執行結果

技術分享圖片

關鍵代碼

統計字符數:

public Result execute() throws IOException {
    int countNum = 0;
    int tempchar;
    InputStream inputStream = new FileStream().getFileInputStream(super.getFileName());
    while ((tempchar = inputStream.read()) != -1) {
        if ((char) tempchar != ‘\r‘ && (char) tempchar != ‘\n‘) {
            countNum++;
        }
    }
    return new ResultItem(super.getFileName(), Args.CHAR, countNum);
}

使用單詞停用表後統計單詞數

private void initStopList() throws IOException {
    stopList = new ArrayList<>();
    String stopListFileName = FileName.getStopListFileName();
    InputStream inputStream = new FileStream().getFileInputStream(stopListFileName);
    StringBuffer buffer = new StringBuffer();
    byte[] b = new byte[10];
    while (inputStream.read(b) != -1) {
        buffer.append(new String(b));
        b = new byte[10];
    }
    String[] words = buffer.toString().trim().split("\\s+");
    stopList = Arrays.asList(words);
}

@Override
public Result execute() throws IOException {
    initStopList();

    int countNum = 0;
    boolean add = true;
    int tempchar;
    int i = 0;
    char[] word = new char[100];
    InputStream inputStream = new FileStream().getFileInputStream(super.getFileName());
    while ((tempchar = inputStream.read()) != -1) {
        if ((char) tempchar != ‘\n‘ && (char) tempchar != ‘\r‘ && (char) tempchar != ‘\t‘
                && (char) tempchar != ‘ ‘) {
            word[i] = (char) tempchar;
            i++;
            if (add) {
                countNum++;
                add = false;
            }
        } else {
            add = true;
            i = 0;
            if (stopList.contains(new String(word).trim())) {
                countNum--;
            }
            word = new char[100];
        }
    }
    if (stopList.contains(new String(word).trim())) {
        countNum--;
    }
    return new ResultItem(super.getFileName(), Args.STOP, countNum);
}

遞歸讀取符合文件名通配符的文件名

private List<String> getFileNameList(String url, String match) {
    List<String> fileNameList = new ArrayList<>();

    File file = new File(url);
    if (file.isDirectory()) {
        for (File file2 : file.listFiles()) {
            fileNameList.addAll(getFileNameList(file2.getPath(), match));
        }
    } else {
        String fileName = file.getName();
        if (fileName.matches(match)) {
            String path = file.getPath();
            path = path.replace(this.getClass().getResource("/").getPath().substring(1).replace("/", "\\"), "");
            fileNameList.add(path);
        }
    }

    return fileNameList;
}

最後輸出結果的排序

@Override
public int compareTo(ResultItem item) {
    int count = charCount(this.getFileName(), ‘\\‘);
    int otherCount = charCount(item.getFileName(), ‘\\‘);
    if (count > otherCount) {
        return 1;
    } else if (count < otherCount) {
        return -1;
    } else {
        int nameCompare = this.getFileName().compareTo(item.getFileName());
        if (nameCompare > 0) {
            return 1;
        } else if (nameCompare < 0) {
            return -1;
        } else {
            int id = this.arg.getId();
            int otherId = item.arg.getId();
            if (id > otherId) {
                return 1;
            } else if (id < otherId) {
                return -1;
            } else {
                return 0;
            }
        }
    }

}

關鍵測試代碼

通用的assert方法

public static void assertEqual(String mess, Object expected, Object actual) {
    if (!expected.equals(actual)) {
        throw new AssertionError(mess);
    }
}

public static void assertSame(String mess, Object expected, Object actual) {
    if (expected != actual) {
        throw new AssertionError(mess);
    }
}
// others

測試統計字符數的方法

public class CharCountHandlerTest {
    private Reader reader;

    public void beforeCalculateCharCount() throws FileNotFoundException {
        // 讀取文件,已知字符數
        reader = new FileReader("D:\\eclipse_n_java\\Wc\\src\\test\\java\\com\\mxh\\wc\\handler\\test.txt");
    }

    public void testCalculateCharCount() throws IOException {
        int count = new CharCountHandler().calculateCharCount(reader);
        Assert.assertEqual("統計文件字符數不正確", 32, count);
    }

    public void afterCalculateCharCount() throws IOException {
        reader.close();
    }

    public static void main(String[] args) throws IOException {
        CharCountHandlerTest test = new CharCountHandlerTest();
        test.beforeCalculateCharCount();
        test.testCalculateCharCount();
        test.afterCalculateCharCount();
    }
}

測試解析參數類的測試類

public class DefaultParseTest {
    public void testParse() {
        String[] args = new String[] { "-c", "-w", "-l", "-e",
                "\"D:/eclipse_n_java/Wc/src/test/java/com/mxh/wc/handler/test.txt\"", "-a",
                "D:/eclipse_n_java/Wc/src/test/java/com/mxh/wc/handler/test.txt" };
        DefaultParse parse = new DefaultParse(args);
        List<AbstractCommand> commands = parse.parse();
        Assert.assertEqual("解析參數出錯了", args.length - 2, commands.size());
    }

    public static void main(String[] args) {
        DefaultParseTest test = new DefaultParseTest();
        test.testParse();
    }
}

程序的不足之處及完善計劃

  花一周的時間來完成這個程序還是有些吃力,有很多地方做的不夠好,程序存在許多問題,還需要花費很長的時間來完善。

  另我最痛苦的是解析參數的類。可以說,解析參數的這個類是整個項目最糟糕的類,這個類的復雜程度遠遠超過了我的預期,它要處理單參數,也要處理雙參數,還要處理文件通配符,甚至要遍歷文件目錄,太多的功能導致它很亂。我找不到一種好的通用方法來解析參數,在接下來的項目工作中,我可能會重構這個類,也可能會拋棄它,使用CLI框架來完成工作。

  統計算法的實現也是比較粗糙的,幾乎沒有考慮什麽特殊的情況,單詞統計采用的是非常簡單的策略(當然,作業也沒要求算法有多復雜)。

  對程序結構的分析還有很多不足之處,槽糕的參數解析類,以及修改了多次的數據模型。

  程序在提高代碼結構的同時,犧牲了執行效率。在執行效率方法還有很多需要優化的地方。

  因為是個人開發的原因,使用碼雲的次數比較少,其實碼雲對個人開發的幫助也是很大的,比如可以記錄每次提交的內容。

  我覺得項目最大的問題在測試上,設計的時候沒有仔細考慮這一點,以致於難以修改測試代碼,又難以重構原程序代碼。如果能夠先設計中測試用例,再來寫代碼,情況就會好很多。

系統分析與設計課程項目 WordCount