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

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

项目代码地址:https://gitee.com/mxhkkk/Wc

PSP2.1表格

PSP2.1

PSP阶段

预估耗时

(分钟)

实际耗时

(分钟)

Planning 计划 30 20
Estimate 估计这个任务需要多少时间 30 20
Development 开发 290 460
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 测试报告 60 90
Size Measurement 计算工作量 0 0
Postmortem & Process Improvement Plan 事后总结, 并提出过程改进计划 30 30
合计410600

概述

  这个项目按照作业要求来说,分为三个阶段,基础功能、扩展功能和高级功能。虽然这一周只需要完成基础功能,但在分析设计阶段也要考虑到扩展功能和高级功能,以免日后添加功能使程序“伤筋动骨”。一开始我只是想设计出可以实现所有功能的程序结构,但实际上在这周内我几乎完成了所有功能的编码工作。不得不说程序难在设计,而不是编码。

题目分析

简单描述

  一个可执行的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框架来完成工作。

  统计算法的实现也是比较粗糙的,几乎没有考虑什么特殊的情况,单词统计采用的是非常简单的策略(当然,作业也没要求算法有多复杂)。

  对程序结构的分析还有很多不足之处,槽糕的参数解析类,以及修改了多次的数据模型。

  程序在提高代码结构的同时,牺牲了执行效率。在执行效率方法还有很多需要优化的地方。

  因为是个人开发的原因,使用码云的次数比较少,其实码云对个人开发的帮助也是很大的,比如可以记录每次提交的内容。

  我觉得项目最大的问题在测试上,设计的时候没有仔细考虑这一点,以致于难以修改测试代码,又难以重构原程序代码。如果能够先设计中测试用例,再来写代码,情况就会好很多。

posted @ 2018-09-23 21:59  MXHKKK  阅读(360)  评论(1编辑  收藏  举报