首页 > 其他 > 详细

系统分析与设计课程项目 WordCount

时间:2018-09-23 23:57:08      阅读:245      评论:0      收藏:0      [点我收藏+]

系统分析与设计课程项目 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

原文:https://www.cnblogs.com/mxhkkk/p/9693911.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!