软工实践寒假作业(2/2)
这个作业属于哪个课程 | 2021春软件工程实践S班 (福州大学) |
---|---|
这个作业要求在哪里 | 软工实践寒假作业(2/2) |
这个作业的目标 | 1、阅读《构建之法》并提出问题 2、学习使用git以及github 3、制定自己的代码规范 4、实现WordCount命令行程序 |
其他参考文献 | CSDN、博客园 |
part1:阅读《构建之法》并提问
问题1 两人合作-结对编程
书中说结对编程让两个人所写的代码不断地处于“复审”的过程,这样可以提高设计和编码质量的过程,也能及时地发现问题和解决问题,避免把问题拖到后面的阶段。但是结对编程会降低编程效率,做项目的时候一定需要结对编程吗?
结对编程需要看程序员的意愿,有些人可能喜欢自己写自己的代码,书中也说了结对编程中,编码不再是私人的工作,而是一种公开的“表演”。所以结对编程需要两个人较高的配合程度,对于比较简单的项目,可以不使用结对编程的方法。
问题2 团队中的角色与合作-团队合作的阶段
书中把团队合作分为萌芽阶段、磨合阶段、散伙阶段、规范阶段、创造阶段,团队合作中每个成员都有自己的职责,如果在后期工作阶段,有成员对分配到的任务不满,或者他不想担任这个职责,该怎么办?
我觉得在团队萌芽阶段就要把人员分工做好,每个成员都要坚守自己的职责,在后期工作得到自己不会或者不满的任务时,应该与整个团员商讨重新分配任务。团队尽量不要因为个人原因达到散货阶段,个人有问题也应该及时和团队反映。
问题3 需求-项目需求分析和建议
现在的大部分软件都可以满足人们的日常生活需求,我们还能从哪些方面来了解用户的需求?而且要怎么知道这些需求是不是大部分人所需要的?
书中提到了刚性需求和辅助性需求,我觉得了解用户的需求可以从生活中的某个细节来找到。比如现在的送货上门服务,研发团队知道有的人比较懒,不爱出门买东西,就提供给用户一个送货上门服务的应用平台。
问题4 设计和开发-用户界面,用户体验的设计
书中“你姥姥的遥控器”讲到了设计应该做到简洁明了,只要能让用户明白产品怎么使用就好了,但是有时候一个产品的功能就是需要较多的设计才能展现出来,这时候是要考虑用户的体验,还是要做全产品的功能呢?
书中提到短期刺激和长期刺激的好处/坏处,一个产品最先考虑的应该是用户的体验,如果用户能够完完全全的使用一个产品的所有功能需要短期刺激的话,那这个产品就算可以了,但是有些用户使用过多少次产品也无法完全了解产品的功能,这时候产品的完整性和用户体验就需要折中了,就像偏智能化的老人机一样,它是为了能方便老年人使用。
问题5 需求- 场景/典型人物
书中给出了典型用户的定义方法,包括年龄、收入、生活/工作情况等等,那程序员根据这些典型用户的需求写好程序后,发现这些用户只有在特定场景才会使用到这个应用,那程序员需不需要修改程序使得用户在大部分情况都能使用?
像某种特定的功能,我觉得可以写个特定版本的程序,提供给特殊用户,程序不一定是只能大部分人使用的,只要在原来的基础上作出修改,特殊用户也能使用。
part2:WordCount编程
Github项目地址
PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
• Estimate | • 估计这个任务需要多少时间 | 4days | 4.5days |
Development | 开发 | ||
• Analysis | • 需求分析 (包括学习新技术) | 120 | 200 |
• Design Spec | • 生成设计文档 | 60 | 80 |
• Design Review | • 设计复审 | 20 | 10 |
• Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 30 | 50 |
• Design | • 具体设计 | 60 | 100 |
• Coding | • 具体编码 | 360 | 480 |
• Code Review | • 代码复审 | 30 | 30 |
• Test | • 测试(自我测试,修改代码,提交修改) | 120 | 180 |
Reporting | 报告 | ||
• Test Repor | • 测试报告 | 60 | 40 |
• Size Measurement | • 计算工作量 | 30 | 30 |
• Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 60 | 120 |
合计 | 950 | 1320 |
解题思路描述
需求:
1、统计文件的字符数(对应输出第一行):
- 只需要统计Ascii码,汉字不需考虑
- 空格,水平制表符,换行符,均算字符
2、统计文件的单词总数(对应输出第二行),单词:至少以4个英文字母开头,跟上字母数字符号,单词以分隔符分割,不区分大小写。
- 英文字母: A-Z,a-z
- 字母数字符号:A-Z, a-z,0-9
- 分割符:空格,非字母数字符号
- 例:file123是一个单词, 123file不是一个单词。file,File和FILE是同一个单词
3、统计文件的有效行数(对应输出第三行):任何包含非空白字符的行,都需要统计。
4、统计文件中各单词的出现次数(对应输出接下来10行),最终只输出频率最高的10个。
- 频率相同的单词,优先输出字典序靠前的单词。
- 例如,windows95,windows98和windows2000同时出现时,则先输出windows2000
- 输出的单词统一为小写格式
然后将统计结果输出到output.txt,输出的格式如下;其中word1和word2 对应具体的单词,number为统计出的个数;换行使用'\n',编码统一使用UTF-8。
characters: number
words: number
lines: number
word1: number
word2: number
...
题目的基本需求是实现一个命令行程序,输入文件和输出文件以命令行参数传入,然后统计输入文件的字符数、单词总数、有效行数、单词的出现次数,最后将统计结果输出到输出文件。
题目的要求很明确,就是实现对文件内容的统计功能,一个功能可以写成一个大类,然后在各个类中编写各自的实现函数。这个题目涉及到了对文件的读写、以及用,所以需要复习一下Java文件操作的相关知识。
代码规范制定链接
设计与实现过程
需求包含4个大的功能,所以需要4个类,分别是字符数统计类CharactersCount、单词总数统计类WordsCount、有效行数统计类LinesCount、频词个数统计类FrequentWordsCount。
一、CharactersCount(字符数统计类):只需要一个字符数统计函数,函数的功能是对文件进行读取操作,然后一个一个地读取字符并统计字符个数。
public static int charactersCount(String filename) {
File file = new File(filename);
int cnt = 0;//总行数
try {
BufferedReader br = new BufferedReader(new FileReader(file));
int temp;
while ((temp = br.read()) != -1) {
cnt ++;
}
br.close();
} catch(Exception e) {
System.out.println("文件不存在!");
}
return cnt;
}
二、WordsCount(单词总数统计类):因为需要提取出文件中的单词,所以需要写isLetterDigit函数(判断仅包含字母和数字的字符串)和isWord函数(判断字符串是否为单词)。单词统计函数的功能是先对文件进行读取操作,一行一行的读取,然后判断行是否为空白字符的行,不是则将行以空白字符为分隔符,分离出一个个没有空白字符的字符串,再用isLetterDigit函数判断筛选出仅包含字母和数字的字符串,最后再用isWord判断筛选出单词。
public static String[] words = new String[1000];//单词数组
public static int wordsCount(String filename) {
File file = new File(filename);
String[] str = new String[1000];//仅包含字母和数字的字符串数组
int cnt = 0;//筛选出的字符串个数
int num = 0;//单词数
try {
BufferedReader br = new BufferedReader(new FileReader(file));
String tempLine;
while ((tempLine = br.readLine()) != null) {
if (!tempLine.equals("")) {
Pattern pattern = Pattern.compile("\\s+|\\W+");
String[] newStr = pattern.split(tempLine);
for(String ss : newStr) {
if (isLetterDigit(ss)) {
str[cnt++] = ss;
}
}
}
}
for (String ss : str) {
if (isWord(ss)) {
words[num++] = ss;
}
}
br.close();
} catch(IOException e) {
e.printStackTrace();//System.out.println("文件不存在!");
}
return num;
}
isLetterDigit函数
private static boolean isLetterDigit(String str) {//判断仅包含字母和数字的字符串
String regex = "^[a-z0-9A-Z]+$";
return str.matches(regex);
}
isWord函数
private static boolean isWord(String str) {//判断是否为单词
if (str != null&&str.length() >= 4) {//此处第一次写因为不知道要判断参数是否为空导致空指针异常,找了许多资料才明白
char ch1 = str.charAt(0);
char ch2 = str.charAt(1);
char ch3 = str.charAt(2);
char ch4 = str.charAt(3);
if (((ch1 >= 'A' && ch1 <= 'Z')||(ch1 >= 'a' && ch1 <= 'z'))
&&((ch2 >= 'A' && ch2 <= 'Z')||(ch2 >= 'a' && ch2 <= 'z'))
&&((ch3 >= 'A' && ch3 <= 'Z')||(ch3 >= 'a' && ch3 <= 'z'))
&&((ch4 >= 'A' && ch4 <= 'Z')||(ch4 >= 'a' && ch4 <= 'z')))
return true;
else return false;
}
else return false;
}
}
三、LinesCount(有效行数统计类):只需要一个有效行数统计函数,函数的功能是对文件进行读取操作,一行一行的读取,然后判断行是否为空白字符的行,不是则计入有效行数。
public static int linesCount(String filename) {
File file = new File(filename);
int cnt = 0;//总行数
try {
BufferedReader br = new BufferedReader(new FileReader(file));
String tempLine;
while ((tempLine = br.readLine()) != null) {
cnt++;
if (tempLine.equals(""))//判断行为空白字符的行
cnt--;
}
br.close();
} catch(Exception e) {
System.out.println("文件不存在!");
}
return cnt;
}
四、FrequentWordsCount(频词个数统计类):需要一个sortByValueDescending函数(对map以Value的值按降序排序),频词个数统计包括频词的统计以及对频词相应个数的统计,函数的功能是将WordsCount类中筛选出的单词放入HashMap中,然后用sortByValueDescending函数排序map,再将map的键值对对应存放到frequentWords数组和num数组。
public static void frequentWordsCount() {
int size = WordsCount.words.length;
Map<String,Integer> map = new HashMap<String,Integer>();//创建map,key保存字符串,value保存出现的次数
int cnt = 0;
String[] words = new String[size];//待统计的单词数组
System.arraycopy(WordsCount.words, 0, words, 0, size);
for (int i = 0; i < size; i++) {
if (words[i] != null)
words[i] = words[i].toLowerCase();
}
for (int i = 0; i < size; i++) {//遍历words数组
if (map.containsKey(words[i])) {
map.put(words[i],map.get(words[i])+1);
}
else
map.put(words[i],1);
}
map = sortByValueDescending(map);
for(Entry<String, Integer> vo : map.entrySet()) {
if (vo.getKey() !=null) {
frequentWords[cnt] = vo.getKey();
num[cnt++] = vo.getValue();
}
}
}
sortByValueDescending函数
public static <K, V extends Comparable<? super V>> Map<K, V> sortByValueDescending(Map<K, V> map) {
List<Map.Entry<K, V>> list = new LinkedList<Map.Entry<K, V>>(map.entrySet());
Collections.sort(list, new Comparator<Map.Entry<K, V>>() {
@Override
public int compare(Map.Entry<K, V> o1, Map.Entry<K, V> o2) {
int compare = o1.getValue().compareTo(o2.getValue());
return -compare;
}
});
Map<K, V> result = new LinkedHashMap<K, V>();
for (Map.Entry<K, V> entry : list) {
result.put(entry.getKey(), entry.getValue());
}
return result;
}
性能改进
上网查找了split函数源码,发现每次使用split函数都需要新建了一个Pattern对象,说明在分词的时候调用了多少次split函数就等于新建了多少个Pattern对象,这样程序的运行自然会收到影响。所以在分割前先创建一个Pattern对象,再调用split函数。
单元测试
- 字符数统计测试函数
public static void main(String[] args) {
String filename = args[0];
File file = new File(filename);
String str = "file 123 \n\t\r";
String content = "";
for (int i=0;i < 10;i++) {
content += str;
}
try {
FileWriter fw = new FileWriter(file);
fw.write(content);
fw.flush();
fw.close();
if (CharactersCount.charactersCount(filename) == 120) {
System.out.print("功能测试通过");
}
} catch(IOException e) {
e.printStackTrace();
}
}
- 单词统计测试函数
public static void main(String[] args) {
String filename = args[0];
File file = new File(filename);
String str = "file123 123file FILE File file\n";
String content = "";
for (int i=0;i < 10;i++) {
content += str;
}
try {
FileWriter fw = new FileWriter(file);
fw.write(content);
fw.flush();
fw.close();
if (WordsCount.wordsCount(filename) == 40) {
System.out.print("功能测试通过");
}
} catch(IOException e) {
e.printStackTrace();
}
}
- 有效行数统计测试函数
public static void main(String[] args) {
String filename = args[0];
File file = new File(args[0]);
String str = "kk \n\r";
String content = "";
for (int i=0;i < 10;i++) {
content += str;
}
try {
FileWriter fw = new FileWriter(file);
fw.write(content);
fw.flush();
fw.close();
if (LinesCount.linesCount(filename) == 10) {
System.out.print("功能测试通过");
}
} catch(IOException e) {
e.printStackTrace();
}
}
异常处理说明
本次程序仅涉及到文件的读写,所以异常处理比较简单,仅做了判断文件是否存在和文件输入个数(命令行参数)的异常处理。
心路历程与收获
这次的编程作业是我第一次有规划地完成,我发现不管一个项目多大多小,有规划地完成可以让人收获更多,可以记录下你在这次项目中遇到的问题,以及如何解决问题的思路。《构建之法》的项目建议NABCD中说到一个项目的需求可以是明确的, 公开的 (例如: 希望能上网玩三国杀)。也可能是说不清道不明的, 例如 - 以前没人说: 嗯, 如果我能找到这样一个网站, 我可以去偷菜, 就好了…当我们明确分析了项目的需求时,我们才能更好的做到后面的ABCD。这次作业也让我学习了许多新的技能和工具使用,git、GitHub是很好的资源分享工具,刚开始看作业要求的时候,觉得很头疼,感觉git很难用,后面同学给我讲解了GitHub Desktop的使用方法之后,才发现可以更简单地来使用git这个工具。还有JProfiler这个对程序性能分析的工具也很好使用。尽管这次的作业只是一个小小的程序,我也要考虑到怎么写程序可以更简单些,让别人看到我的代码时能很好的理解。