系统分析与设计课程项目 WordCount
Posted mxhkkk
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了系统分析与设计课程项目 WordCount相关的知识,希望对你有一定的参考价值。
系统分析与设计课程项目 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 |
合计 | 470 | 660 |
概述
这个项目按照作业要求来说,分为三个阶段,基础功能、扩展功能和高级功能。虽然这一周只需要完成基础功能,但在分析设计阶段也要考虑到扩展功能和高级功能,以免日后添加功能使程序“伤筋动骨”。一开始我只是想设计出可以实现所有功能的程序结构,但实际上在这周内我几乎完成了所有功能的编码工作。不得不说程序难在设计,而不是编码。
题目分析
简单描述
一个可执行的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 != ‘
‘ && (char) tempchar != ‘
‘) {
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 != ‘
‘ && (char) tempchar != ‘
‘ && (char) tempchar != ‘ ‘
&& (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的主要内容,如果未能解决你的问题,请参考以下文章