软工实践寒假作业(2/2)
软工实践寒假作业(2/2)
标签 : 软工实践作业
这个作业属于哪个课程 | 2021春软件工程实践|S班 (福州大学) |
---|---|
这个作业要求在哪里 | 软工实践寒假作业(2/2) |
这个作业的目标 | 继续阅读《构建之法》、学会git的基本操作、学习代码性能分析改进 |
作业正文 | 正文位置 |
其他参考文献 | 《Pro Git》、《猴子也能看懂的Git入门》、维基百科 |
GitHub个人地址:https://github.com/koopi0123
GitHub项目地址:https://github.com/koopi0123/PersonalProject-Java
作业主要内容:1. 阅读《构建之法》并提问。2. 熟悉GitHub和软件测试,完成WordCount项目。
目录:
部分一:阅读与思考
1.重新阅读《构建之法》提出的问题
1.第1章 1.2:
“Build To Show:为了突出地展现某个技术的作用,开发一些以演示为目的软件,这些项目很吸引眼球,经常获得新闻报道,但是功能未必全面或实用。”
这个部分让我了解到还有为了演示技术而开发得软件,而我有一个疑问:一个软件如果仅仅用于一时演示而没有投入全面应用。甚至在演示之后基本就不会再继续使用了,那么这些程序在开发的过程中还有必要严格按照常规的开发流程吗?
2.第10章 10.2:
“讲故事是最有效的人与人交流信息的途径,通过讲故事(Use Case),团队成员能对需求有统一的了解。当我们用自然语言故事讲的时候,我们不自觉地会把复杂的系统当作一个黑盒子把重点放在用户的愿望,行动上面,这种做法非常有利于我们找到用户的需求和软件的功能点”
简单说明故事是否会缺少用户体验的细节,然而如果要让故事说明得完整又细节,是否会将故事写的缺少简明性,那么要如何把握故事的细节?在之前设计用例时,我使用UML图形来分析的次数比较多,而文字内容较少,故事有哪些图形不具备的优势吗?
3.第12章 12.1:
“大家平时都说要向某某大师学习,把最重要的功能做好交给客户,把那些无关紧要的功能藏起来,做减法……”
只把最重要的功能醒目地展示出来,的确可以让产品看起来一目了然,用户上手的难度也降低了。我的疑问是那些介于无关紧要和最重要之间的,没那么重要的功能应该放在哪里比较合适呢?
4.第16章 16.3:
“在产品达到引爆点之前,不宜过早考虑变现,同时,不宜受产品现有变现模式的束缚,而要把重点放在用户满意度和用户增长率上。”
看了这句话之后,我对其中提到的“引爆点”概念的定义并不清楚。我在维基百科中,查阅到关于引爆点(Tipping Point)的定义:“系统发展到一定程度会产生飞跃性的变化,但转捩点更隐含意思于一系列大型事件、大型系统的发展,这种改变在表面或外在方面来看可能明显也不一定很明显。”查阅百科后我对“引爆点”的概念还是有些模糊。一个产品在什么时候到达引爆点,又可以通过哪些指标判断呢?
5.第16章 16.3:
“在用户中招募粉丝,让粉丝有参与感并整合到市场推广中。在这一阶段要首先培养用户的忠诚度,然后再考虑品牌的知名度。”
我也十分赞同这个观点,我认为先着手用户的忠诚度可以通过粉丝的用户体验来完善产品,此外粉丝用户也能在产品发布之初缺少宣传的时候对各自的亲友宣传推荐,在产品有不足的时候及时反馈。可是在软件开发之初就可以确定自己潜在的忠实用户吗,实际市场中有什么策略可以有效地招募粉丝、培养忠实用户并提高用户忠诚度呢?
2.软件工程发展冷知识
Unix系统在上世纪曾经风靡一时,而它的发明却来自肯·汤普森的一次贪玩。
肯·汤普森是当时著名的贝尔实验室的一名研究员,当时贝尔实验室接了一个开发MULTICS系统的项目,肯·汤普森为MULTICS这个操作系统写了一个游戏,游戏名叫“Space Travel”。但是,他发现这个游戏在MULTICS操作系统上运行速度很慢而且耗费昂贵 —— 每次运行会花费75美元。而后来,由于MULTICS操作系统这个项目过于庞大和复杂,贝尔实验室决定退出这个项目。退出这个项目以后,肯·汤普森为了可以继续玩这个游戏,就计划自己写一个操作系统。然后他找来丹尼斯·里奇为这个游戏开发一个极其简单的操作系统,这就是Unix系统原型。
部分二:WordCount编程
1.作业描述
“ 在大数据环境下,搜索引擎,电商系统,服务平台,社交软件等,都会根据用户的输入来判断最近搜索最多的词语,从而分析当前热点,优化自己的服务。首先当然是统计出哪些词语被搜索的频率最高啦,请设计一个程序,能够满足一些词频统计的需求。”
2.项目地址
3.PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 20 | 25 |
• Estimate | • 估计这个任务需要多少时间 | 20 | 25 |
Development | 开发 | 1065 | 980 |
• Analysis | • 需求分析 (包括学习新技术) | 200 | 170 |
• Design Spec | • 生成设计文档 | 40 | 25 |
• Design Review | • 设计复审 | 15 | 30 |
• Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 15 | 40 |
• Design | • 具体设计 | 240 | 120 |
• Coding | • 具体编码 | 300 | 350 |
• Code Review | • 代码复审 | 15 | 25 |
• Test | • 测试(自我测试,修改代码,提交修改) | 240 | 220 |
Reporting | 报告 | 255 | 175 |
• Test Report | • 测试报告 | 150 | 140 |
• Size Measurement | • 计算工作量 | 15 | 20 |
• Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 90 | 15 |
合计 | 1340 | 1180 |
4.解题思路
基本需求分析:
- 读取文件数据
- 以文件形式输出结果,读取输出编码统一使用UTF-8
- 输入文件和输出文件以命令行参数传入
- 统计文件的字符数(对应输出第一行)
- 统计文件的单词总数(对应输出第二行)
- 统计文件的有效行数(对应输出第三行)
- 统计最多的10个单词及其词频(对应输出接下来10行)
初步思考:
- 命令行参数应当考虑输入参数格式不正确的情况。
- JAVA读写文件有多种方法,用何种方式效率会更好些?
- 统计字符总数较其他统计功能来说容易实现,不考虑中文的情况下,直接读取文本长度应该就能完成。
- 统计单词总数如何实现还没具体想好,大概的思路应该就是将每两个空白字符间的字符串与正则表达式匹配,判断是否具有单词的条件(条件:至少以4个英文字母开头,跟上字母数字符号,单词以分隔符分割,不区分大小写)。
- 统计有效行数可以通过正则表达式,匹配有非空白字符的文本行。
- 统计词频最高的10个单词,我的大致思路是使用map结构,key为单词,value为词频,针对map的value进行降序排列。这方法是第一反应想到的,但我觉得在不同单词数量多的情况下容易造成大量的空间浪费。
5.我的代码规范
6.模块接口的设计与实现
程序共计两个类:负责基本功能的Lib类,和实现将最终结果输出到文件的WordCount类。
其中Lib类中包含5个静态函数,分别为:txtToString(读取txt文件内容以字符串形式返回)、countChar(统计字符串的字符数)、countLine(统计字符串有效行数)、countWord(统计字符串有效单词个数)、staticFrequency(按频率排序单词)。
txtToString代码实现
通过打开File类对象实例化FileInputStream流对象,接着使用FileInputStream的read()方法读取文本文件。
//txtToString部分关键代码
FileInputStream inputStream = new FileInputStream(file);
int length = inputStream.available();
byte[] buffer = new byte[length];
inputStream.read(buffer);
inputStream.close();
countLine代码实现
使用正则表达式匹配字符串,并统计非空行的个数。
//countLine部分代码
int count = 0;
Pattern nonblankPattern = Pattern.compile("(\n|^)\\s*\\S+");
Matcher matcher = nonblankPattern.matcher(context);
while (matcher.find()){
count++;
}
countChar代码实现
根据字符串的字节数统计字符数。
context.getBytes().length;
countChar代码实现
实现方式与countLine类似,即通过正则表达式匹配来统计单词数。
staticFrequency代码实现
使用HashMap存储单词和对应的出现频率,首先通过正则提取出符合条件的单词,若HashMap中已存在对应Key,则将其value值加1,反之在HashMap中增加对应键,其Value置1。
HashMap值是没有顺序的,因此想要排序可以借助其他有序集合:把HashMap中全部键值对插入List,使用Collections的sort帮助排序。
//排序部分代码
List<Map.Entry<String,Integer>> list = new ArrayList<>();
list.addAll(frequency.entrySet());
Collections.sort(list, new Comparator<Map.Entry<String, Integer>>() {
@Override
public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
if(!o2.getValue().equals(o1.getValue())) {
return o2.getValue().compareTo(o1.getValue());
} else {
return o1.getKey().compareTo(o2.getKey());
}
}
7.性能改进
读取文件性能
为了避免频繁对文件的访问操作导致时间上的浪费,使用大小为文件字节数的byte数组作为缓冲区,一次读取全部数据,从而加快程序整体的运行速度。
单词排序性能
因为单词在频率相同的情况下要按照字典序排列,一开始的想法是排序两次,先按字典序排序,最后再按照频率排序就能得到结果。后来请教了朋友发现修改只要比较器的内容就可以做到一次排序就能得到结果,能省一次排序的时间,代码看起来也更简洁明了。
8.计算模块部分单元测试
使用到的工具:JUnit插件
测试countChar:
测试思路:随机生成一段特定长度字符串多次检验。
测试代码:
@org.junit.Test
public void countChar() {
//从str随机选取字符产生特定长度字符串
String str = "abAB1234 \t\n\r\f.!,?";
String testStr = "";
int location;
int length = 900;
int time = 10000;
for(int t = 0; t<time; t++) {
for (int i = 0; i < length; i++) {
location = (int) (Math.random() * str.length());
testStr += str.charAt(location);
}
int result = Lib.countChar(testStr);
assertEquals(length, result);
System.out.println(t);
testStr = "";
}
}
测试countLine:
测试思路:使用特定文本检验。
测试countWord:
测试思路:随机生成包含特定个数合法单词的字符串,检验是否符合预期。
部分代码:
int countLegal = 300;
//合法单词与不合法单词
String[] wordStr = {"word", "c", "1"};
String testStr = "";
String str = "";
int i = 0;
while (i < countLegal){
//每个子字符串随机以0-99结尾
int location = (int)(Math.random()*wordStr.length);
str = " " + wordStr[location] + (int)(Math.random()*99);
testStr = testStr.concat(str);
//子字符串为合法单词
if(location == 0){
i++;
}
}
测试staticFrequency:
测试思路:使用指定字符串作为参数检验结果是否正确。
部分代码:
List<Map.Entry<String,Integer>> resultList = Lib.staticFrequency(testStr);
for(int i = 0; i < resultList.size(); i++){
System.out.println(resultList.get(i).getKey() + ": " + resultList.get(i).getValue());
}
int maxFrequency = 50;
int resultFrequency = resultList.get(0).getValue();
assertEquals(maxFrequency, resultFrequency);
一个使用特定字符串测试的用例:
//方便查看结果,频率写在字符串名最后1-2位
String word1 = "word1";
String hello10 = "hello10";
String halo10 = "halo10";
String sad20 = "sad20";
String work1 = "work1";
String wolf1 = "wolf1";
String think29 = "think29";
String sam21 = "sad21";
String roof50 = "roof50";
String ram14 = "ram14";
String[] str = {word1, hello10, halo10, sad20, work1, wolf1, think29, sam21, roof50, ram14};
//产生待测字符串
String testStr = "";
for(int i = 0; i < str.length; i++){
int frequency;
frequency = Integer.parseInt( str[i].replaceAll(".*[^\\d](?=(\\d+))",""));
for(int k = 0; k<frequency; k++){
testStr = testStr.concat(str[i] + " ");
}
List<Map.Entry<String,Integer>> resultList = Lib.staticFrequency(testStr);
for(int i = 0; i < resultList.size(); i++){
System.out.println(resultList.get(i).getKey() + ": " + resultList.get(i).getValue());
}
int maxFrequency = 50;
int resultFrequency = resultList.get(0).getValue();
assertEquals(maxFrequency, resultFrequency);
使用该字符串输出的结果符合预期:
roof50: 50
think29: 29
halo10: 10
hello10: 10
wolf1: 1
word1: 1
work1: 1
测试txtToString:
测试思路:读取不存在的文件检验是否异常、读取存在的文件检验读取后的字符串是否完整。
部分代码:
@org.junit.Test
public void txtToString() {
String fileName = "input.txt";
System.out.println(Lib.txtToString(fileName));
fileName = "xx";
Lib.txtToString(fileName);
}
测试main函数
测试点:命令行参数不符合情况是否会符合预期报错。
代码:
@org.junit.Test
public void Main(){
String[] str1 = {"input.txt", "output.txt"};
WordCount.main(str1);
// 参数个数不对
String[] str2 = {"input.txt"};
WordCount.main(str2);
// 参数后缀不对
String[] str3 = {"input.exe", "input.txt"};
WordCount.main(str3);
}
软件执行速度
待测试文件:字节数分别为10万左右、100万左右的英文小说、随机生成文本的txt文件。
执行时间:经过统计,处理10万左右字节的文件平均需要124ms,处理100万左右字节的文件平均需要209ms。
模块部分测试截图
9.异常处理说明
主要考虑到的异常有:命令行参数异常、文件读取异常
点击查看 命令行参数异常测试
//命令行参数异常处理代码
if(args.length!=2||!args[0].endsWith(".txt")||!args[1].endsWith(".txt")){
System.out.println("输入非法参数,程序结束!");
return;
}
//文件读取异常代码
(Exception e){
e.printStackTrace();
System.out.println("读取文件失败!");
return null;
}
10.心路历程与收获
心路历程
刚看到作业要求的时候,我觉得作业的要求比较简单看起来容易完成。等到我开始做的时候我才发现,要考虑到的细节还是很多的,不仅在编程的时候要思考有没有效率更高的方法,还要考虑编写出的函数是否足够可靠,能禁得起多次测试。做到后面又觉得其实只要把这个任务划分成多个小任务一块块完成的话,其实也没有那么复杂,对软件单元测试让我了解到,测试一个程序的可靠性,需要花心思考虑各种包括异常在内的所有可能的情况。
之前使用GitHub都是下载其他人的代码,还没怎么上传自己的代码到上面过,这次使用GitHub委托自己的代码感觉十分方便,做到哪一步觉得开始乱了可以回退到之前之前没有问题的一个版本,中间也经历了误操作把自己写好的代码全部回退的不好回忆,但还是很快就通过git找回了,git版本控制的确可以带来很大便利。
收获
- 学会了通过git控制代码版本的基本操作
- 加深了对软件测试的了解与应用
- 熟悉如何通过IDEA插件工具进行单元测试
- 重温了很久没有接触的HashMap数据结构
- 通过PSP表格提前规划好要做的工作