项目地址

https://github.com/hqweay/WordCount

已完成需求与分析

需求分析

基本功能:统计文件的字符数,单词数,行数。

扩展功能:统计代码文件的注释行数,空行数,代码行数,使用忽略词组统计单词数

将统计结果按照一定格式写入输出文件。

打包为可执行文件 wc.exe

执行方式:wc.exe 参数

注意

  • 空格,水平制表符,换行符,均算字符。
  • 由空格或逗号分割开的都视为单词,且不做单词的有效性校验,例如:thi#,that视为用- - 逗号隔开的2个单词。
  • -c, -w, -l参数可以共用同一个输入文件,形如:wc.exe –w –c file.c 。
  • -o 必须与文件名同时使用,且输出文件必须紧跟在-o参数后面,不允许单独使用-o参数。

截图效果

参数违规

bugs

正常

normal

采用 stop list文件

stoplist

PSP表格

PSP2.1 PSP阶段 预估耗时(分钟) 实际耗时(分钟)
· Planning · 计划 30 20
· Estimate · 估计这个任务需要多少时间 20 10
· Development · 开发 24*60 10*60
· Analysis · 需求分析 (包括学习新技术) 50 40
· Design Spec · 生成设计文档 10 10
· Design Review · 设计复审 (和同事审核设计文档) 10 0
· Coding · 代码规范 (为目前的开发制定合适的规范) 10 10
· Code Review · 具体设计 20 30
· Test · 具体编码 24*60 15*60
· Reporting · 代码复审 2*60 50
· Test Report · 报告 2*60 1.5*60
· Size Measurement · 测试报告 30 10
· Postmortem & Process · 计算工作量 10 15
· Improvement Plan · 事后总结, 并提出过程改进计划 10 20
· 合计 3310 1825

解题思路

统计字符与行数,第一感觉需要用到文件的输入输出知识,然后按行读取,按字符来进行一些判断,看是否满足需求。

类图

uml

程序设计与代码说明

在正式写代码之前,我先简单地组织了一下项目。

如类图所示,首先我考虑到需求分析时的基本功能,扩展功能,高级功能这三块,我注意到每一个模块要求的功能并不多,所以打算按模块建立功能类,这样扩展就只需要新建类,当我完成基本功能去实现高级功能时,就不需要修改完成的代码。

比如我在 BasicFeature.class 中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 基本功能
* 统计 字符,单词,行数
*/
public class BasicFeature {
Util util = new Util();

/**
* @param file
* @return string of countChar
* @throws IOException
*/
public String countChar(File file) throws IOException {

}

/**
* @param file
* @return string of countWord
* @throws IOException
*/
public String countWord(File file) throws IOException {

}

/**
* @param file
* @return string of coutLine
* @throws IOException
*/
public String countLine(File file) throws IOException {

}
}

对应于需求分析中基本功能的三个功能。

其次,需求分析中涉及到对某个名词的定义,比如以什么来分隔单词,什么是空行。于是我把这类定义放在一个配置类 Config.class 中,可以方便地进行修改。

比如需求中提到:

由空格或逗号分割开的都视为单词,且不做单词的有效性校验

于是我在 Config.class 写了

1
2
3
4
5
6
//统计单词的分隔符
//按需求,只需要区分 逗号,空格
//这样定义便于扩展
public final static String[] supportSplits = {
",", " ", "."
};

在编码过程中,我注意到在每个小功能中,都会涉及到文件的读取操作,于是我建立了一个配置类 Util.class ,抽象了一个用的最多的文件操作方法。(在之后,我还在此类中放了一些其他方法。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 把获取输入流独立出来
*
* @param file
* @return bufferedReader
*/
public BufferedReader getBufferedReader(File file) {
InputStreamReader inputStreamReader = null;
try {
inputStreamReader = new InputStreamReader(new FileInputStream(file));
} catch (FileNotFoundException e) {
e.printStackTrace();
//可能出错勒
}
return new BufferedReader(inputStreamReader);
}

这样,就把一个经常用到的流程抽象为了一个方法,可以方便地调用。

当我完成了各个模块,就准备将整个流程整合起来。

按需求分析所说,最终我们需要达到的是通过命令行窗口执行 wc.exe 参数列表

这个流程是一定的,所以我建立了一个操作类 Operator.class ,通过调用功能类的方法,达到了封装整个操作的效果。

根据用户的参数,我们需要判断出要执行哪些功能,所以我在此类中添加了如下 5 个属性。在解析用户输入的参数时,就解析出哪些操作是需要执行的。

然后在 excuteCountOperatorToOutput() 方法中,就开始通过调用功能类的方法执行相应功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Operator {
private boolean countChar = false;
private boolean countWord = false;
private boolean countWordByStopList = false;
private boolean countLine = false;
private boolean countCodeInfo = false;

/**
* 执行一系列的统计操作
*
* @param file
* @param stopListFile
* @return
*/
public ArrayList<String> excuteCountOperatorToOutput(File file, File stopListFile) {
ArrayList<String> outputTexts = new ArrayList<String>();
//功能类
BasicFeature basicFeature = new BasicFeature();
EXFeature exFeature = new EXFeature();
try {
if (countChar) {
outputTexts.add(basicFeature.countChar(file));
}
if (countWord) {
outputTexts.add(basicFeature.countWord(file));
}
if (countWordByStopList) {
outputTexts.add(exFeature.countWordByStopList(file, stopListFile));
}
if (countLine) {
outputTexts.add(basicFeature.countLine(file));
}
if (countCodeInfo) {
outputTexts.add(exFeature.countCodeInfo(file));
}
} catch (IOException e) {
e.printStackTrace();
}
return outputTexts;
}
}

整个流程有了,就可以进行阶段性的测试。我建立了一个测试类 Test.class,不过在实际开发过程,我都是直接在主函数中测试的…这一点做的不是很好。

最后,需要和用户交互,我建立了 Welcome.class 类,这个类主要用于解析用户输入的参数。

因为涉及到与用户的交互,我创建了一个内部类 paramException.class ,用于相关的错误提示。

1
2
3
4
5
6
7
8
9
10
11
class paramException {
private ArrayList<String> errors = new ArrayList<String>();

public ArrayList<String> getErrors() {
return errors;
}

public void addError(String error) {
this.errors.add(error);
}
}

解析用户的参数,需要解决这几个问题:

  1. 无意义的输入
  2. 参数的顺序(比如 -o 后面跟输出文件)
  3. 对用户进行提示

完成后需要封装为 exe 可执行文件,使用的工具是 exe4j ,用到了张波同学在 有关exe4j打包jar包控制台没有输出的补充 的内容。

然后涉及到了参数的解析,在命令行执行 wc.exe -a -p,’-a’,’-p’ 都会作为 args[] 参数传进来。

下面是大体流程,其中包含一些小方法,不赘叙。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public boolean paramsExecute(String[] args) {
for (int i = 0; i < args.length; i++) {
String param = args[i];
switch (param) {
case "-c":
countChar();
break;
case "-w":
countWord(args);
break;
case "-l":
countLine();
break;
case "-a":
countCodeInfo();
break;
case "-o":
if (i < args.length - 1 && args[i + 1].contains(".")) {
//输出文件 ok
} else {
String error = "没找到输出文件 " + args[i];
paramException.addError(error);
return false;
}
break;
case "-e":
if (i < args.length - 1 && args[i + 1].contains(".")) {
// count word by stop list
operator.setCountWordByStopList(true);
operator.setCountWord(false);
} else {
String error = "参数 " + args[i] + " 后没找到 stop list 文件 ";
paramException.addError(error);
return false;
}
break;
default:
//文件名或者报错
filePaths.add(param);
break;
}
}
return true;
}

测试过程

思路

整个开发流程无时无刻不在测试,系统的测试在基本功能完成后。

因为我的开发流程是:各个小功能,整体,打包 exe 。

所以测试流程也随之是先通过各个小功能,再整体流程测试,再打包为 exe 可执行文件,在命令行窗口执行。

最后一步的测试效果如 截图效果 所示。

如类图所示,我分了 BasicFeature.class,EXFeature.class 两个功能类,在这两个功能类里我通过创建 Main 方法实现了测试方法的效果,达到模块测试。

我还建立了一个 Test.class 作为后面的整体测试,不过没怎么用上,因为代码比较简单,直接在 main 函数执行比较方便。

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Welcome welcome = new Welcome();

String[] argsd = {
"-c",
"-w",
"-l",
"-a",
"text.txt",
"-e",
"stopList.txt",
"-o",
"output.txt"
};
//解析参数
welcome.start(argsd);

侧重点

因为需求分析中没做非常严格的规定,所以每个小功能还是比较好实现的。比如单词的分隔仅仅要求以空格,逗号来分隔,不做检验。一般只需要按行读取再做匹配。

我在输入文件中写了一些字符做测试。

还有就是与用户的交互,用户执行操作的时候什么参数都可能传进来,所以要多考虑。

文件用例

text.txt

1
2
3
4
5
6
7
8
9
//public void 
/**
*
**/
ddddd
ssss File is a
dd ss

ssss

/**/ 多行注释并不会被统计为注释。

执行用例

  1. wc.exe -c -w -l -a text.txt -e stopList.txt -o output.txt
  2. wc.exe -xx
  3. wc.exe -c text.txt
  4. wc.exe -e stopList.txt
  5. wc.exe text.txt -o oup.txt
  6. wc.exe fffffffffffffffffffffffffffffffffffffffffffff&&*$##%&

在 5 这个测试中,代码执行会通过,没有指定参数,但是含有输入输出文件,程序就默认不做操作。(但是还是会进入上面提到的 Operator.class 操作类的方法中)

其他

gitee 管理项目,需要 git 相关知识。基本的配置见这篇文章

从Git合作管理gitee项目浅谈

因为管理不当,我在 push 代码的时候遇到了本地分支和远程分支冲突的问题,一直没弄好,最后强制上传,git push origin master -f 把我之前的 commits 覆盖完了…