软工实践寒假作业(2/2)
作业摘要
这个作业属于哪个课程 | 2021春软件工程实践|S班 (福州大学) |
---|---|
这个作业要求在哪里 | 软工实践寒假作业(2/2) |
这个作业的目标 | 1.开发WordCount程序 2.学习使用github,github desktop,Jprofiler软件等 |
作业正文 | 作业正文 |
其他参考文献 | CSDN、简书、知乎 |
目录:
作业摘要
Part1:《构建之法》的深入提问
Part2:WordCount编程
《构建之法》的深入提问
《构建之法》7问
Queation1:
文字引用 | 对产品功能性需求:要求产品必须实现某些功能。 综合需求:有些需求并不是单单一个软件模块就能满足,例如,“购物网站必须在24小时内把货物发送到用户手中”,这个需求牵涉到软件系统、货物派送系统、送货部门、监控系统等不同部门的功能和执行能力。 |
---|---|
问题描述 | 有定义来看,一个需求可以即归为产品的功能性需求又归为综合需求吧,这种的情况下应该归为哪一个分类?(书本P159) |
说法引用 | 网络上的定义和书本不同,百度百科中将软件需求做如下定义:需求包括三个不同的层次—业务需求、用户需求和功能需求—也包括非功能需求。 |
我的经验/困惑/观点 | 我个人想法是把重叠的部分归为综合需求,这样这个需求可以被实现的更全面。 |
Queation2:
文字引用 | 主治医师模式运用到极点,可以蜕化为明星模式,在这里,明星的光芒盖过了团队其他 |
---|---|
问题描述 | 主治医师模式可以理解为只有一个“明星”,这是不是说明一般情况下明星模式优于主治医师模式?P98 |
说法引用 | 主治医师模式缺点:在一些学校的软工课上,这种模式逐渐退化成“一个学生干活,其他学生打酱油” 明星模式缺点:团队模式强调的是团队的作用,而不是个人的独角戏,这种模式显然违背了团队模式的初衷,效率也很低 |
我的经验/困惑/观点 | 两者其实很像,但由于明星模式表现突出的人更多,我觉得只要“明星们”团结一致,大概率下会优于主治医师模式。 |
Queation3:
文字引用 | 明星也是人,也会受伤,犯错误,如何让团队的利益最大化,而不是明星利益最大化?如何让团队的价值在明星陨落之后仍然能够保持?是这个模式要解决的问题。 |
---|---|
问题描述 | 明星利益最大化是不是很能帮助团队利益的最大化,还是这个模式下明星利益化反而会降低团队的利益? |
说法引用 | 明星模式优点:对“明星”个人的成长进步可能会有所帮助。 明星模式缺点:团队模式强调的是团队的作用,而不是个人的独角戏,这种模式显然违背了团队模式的初衷,效率也很低 |
我的经验/困惑/观点 | 以我的经验,我们开展团队合作时的模式其实可以近似为“明星模式”,一些大佬来完成重要的工作,其他人来协助完成,也就是所谓的打杂。所以我认为影响这个模式的反而是”大佬“和其他人之间的关系,如果明星利益太大,会使其他人发言权大大降低,所以应该采取一种“中庸”的状态。 |
Queation4:
文字引用 | PM通常也能写代码,能玩转Excel、PPT、Visio、甘特图,会PS,有文字功底,写的博客有人爱读,反正,总得有几招绝活吧!不用说还要有大量的阅读,对IT行业、用户心理、社会都要有广泛的了解。 |
---|---|
问题描述 | PM的能力要求太广泛了,做好一个初级PM的切入点是什么?P198 |
说法引用 | 初学PM的建议: 1.基础的东西已经具备 2.要实实在在的参与一个产品的迭代 3.要多参考竞品 4.每增加一个功能,必须要问自己为什么 5.把每一个功能拆分到很细,比如登陆,账号用手机还是邮箱,为什么要用手机?为什么要用邮箱,你自己要搞清楚,虽然现在很多产品都首选手机做账号,但不是所有的都适合;密码字段,是用字符窜还是用纯数字,还是字符串+纯数字,是几个字节,需不需要特殊字符?大小写区不区分? 6.业务逻辑要清楚的列出来,你自己清楚了,才能给开发和测试提供炮弹 7.需求,大家都在讲需求,我就不强调,每个人有自己获取需求的渠道以及判断 |
我的经验/困惑/观点 | 之前我有参与西二的PM小组,PM开发项目是有一些固定的需求文档,每个公司也会有自己的一些模板,如果想当然的填补文档的内容就会偏离正确内容,所以我觉得PM这个职业的新手需要多注意细节,不能想当然。至于切入点可能要学习一些前辈的实际经验。 |
Queation5:
文字引用 | 场景怎么写?首先针对每一个场景,设计一个场景入口(描述场景如何开始)。接着描述典型用户在这个场景中所处的内部和外部环境(内部环境指心理因素等)。然后给场景划分优先级,按优先级排序写场景。 |
---|---|
问题描述 | 对场景的重要性如何排序?P219 |
说法引用 | 基本型需求>期望性需求>兴奋型需求 使用需求的金字塔法则来表达,金字塔的最底层是基本型的需求,往上是期望型需求,最上面一层是兴奋型需求。 |
我的经验/困惑/观点 | 之前写过一些需求文档,虽然也是按照规则进行书写,但是我觉得还是参杂了许多的主观看法。 |
Queation6:
文字引用 | 团队在Bug数量上上下下的过程中,要注意消灭族老的问题,让老的Bug数量为0,以防止一些问题拖而未解决,有可能掩盖深层次的设计问题,要尽早把这些问题暴露出来,一个招数是,划定一个时间期限,一定要解决在此之前发现的Bug。 |
---|---|
问题描述 | 一些bug可以被认为不予解决,会不会因此出现Bounce问题?P335 |
说法引用 | 解决bug流程: 1、找产品经理确认 2、不予解决,关闭 3、要解决,写明原因给开发 |
我的经验/困惑/观点 | 我的理解是应该要有前瞻性,可能出现Bounce问题的bug就不能不被解决,但是也不排除判断出错的可能。 |
Queation7:
文字引用 | 我们常说“软件的生命周期”——这个软件开发的周期结束了,生命也结束了。我们能不能像医学的尸体解剖一样,把这个软件的开发的流程解剖一下? |
---|---|
问题描述 | 事后诸葛亮会议是用来探究发布后产生的问题,还是总结开发过程中的问题?P339 |
说法引用 | 无 |
我的经验/困惑/观点 | 我认为既然是畅所欲言,就集思广益,想到什么讨论什么,但是各个要点需要遵循5WHY。 |
附加题
软件工程发展的过程中的故事
故事:
1986年4月11日,美国东部德克萨斯州。一位患有面部皮肤癌的男性患者正在使用Therac-25 做放射性治疗。刚刚开始治疗,一团巨大的光亮就出现在他的眼前,耳边响起了煎鸡蛋的声音,其实这是机器灼烧了他的大脑和脑干的右侧额叶。因为严重的辐射,三周后这名病人就死亡了。
因为之前的案例没有得到AECL和医院的重视,悲剧一再地发生。类似悲剧一共发生了6起,直到1987年雅基马谷医院的最后一次事故,整个悲剧事件才结束。
后期的调查表明,全部的医疗事故都是因为软件包含严重的bug,放射性治疗的机器在正常情况下只会发射低能量的电子束,旧型号的机器为了保证不出意外,使用硬件互锁的机制确保能量不会升高。而新的型号Therac-25为了降低成本,改用了软件锁机制。可该程序本身包含的一个一字节的计数器常常会溢出,如果操作员恰好在溢出位输入命令,软件锁的机制就会失效。导致了悲剧,患者受受到100倍的辐射剂量,痛苦的死亡。
个人见解:
1.该事例的发生,我们可以确定Therac-25没有没有通过充分的测试就被安装到设备中,理论上没有跳过开发的必要步骤,但是工程师的过度自信和缺少标准的质量体系,使代码的质量无法保证。
2.所有的工程不能只解决问题,还要对可能发生的错误提出解决方案。如上述故事中,Therac-25本应该有个“熔断机制”保证无论如何不能发生辐射量过大的情况,但是这在开发中没有实现。
3.软件工程师应当从Therac-25事件中吸取教训,在软件的开发中边界条件的注意,防御性编程的思考,软件文档的完善与准确和对测试工作的重视都是对自己的要求。
WordCount编程
Github项目地址
作业的主仓库: 点击进入——作业主仓库
我的Github仓库地址: 点击进入——Github项目地址
PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
• Estimate | • 估计这个任务需要多少时间 | 20 | 30 |
Development | 开发 | ||
• Analysis | • 需求分析 (包括学习新技术) | 150 | 180 |
• Design Spec | • 生成设计文档 | 20 | 20 |
• Design Review | • 设计复审 | 20 | 40 |
• Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 30 | 40 |
• Design | • 具体设计 | 80 | 100 |
• Coding | • 具体编码 | 300 | 500 |
• Code Review | • 代码复审 | 60 | 90 |
• Test | • 测试(自我测试,修改代码,提交修改) | 90 | 120 |
Reporting | 报告 | ||
• Test Repor | • 测试报告 | 15 | 30 |
• Size Measurement | • 计算工作量 | 30 | 30 |
• Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 100 | 120 |
合计 | 915 | 1300 |
解题思路描述
step1:学习git、github、复习java
step2:总结要求,确定实现的流程(详细见设计与实现过程)
step3:查找资料,深入了解Map函数的使用,并实现上所述流程
根据资料一共了解了三种map集合的遍历方式
根据资料一共了解了三种map集合的遍历方式
1.使用keySet方法便利
缺点:keySet只是返回了所有的key没有value
//KeySet()把map集合中的所有键都保存到一个Set类型的集合对象中返回
Set<String> keys = map.keySet();
Iterator<String> it = keys.iterator();
while(it.hasNext()){
String key = it.next();
System.out.println("key: "+key+"value: "+map.get());
}
2.使用values方法遍历
缺点:values方法只能返回所有的value,没有key
//values()把所有的值存储到一个Collection集合返回值
Collection<String> c = map.values();
Iterator<String> it = c.iterator();
while(it.hasNext()){
System.out.println("value: "+it.next());
}
3.使用entry方法遍历
Set<Map.Entry<String,String>> entrys = map.entrySet();
Iterator<Map.Entry<String,String>> it = entrys.iterator();
while(it.hasNext()){
Map.Entry<String,String> entry = it.next();
System.out.println("key: "+entry.getKey()+"value: "+entry.getValue());
}
显然,第三种是最符合要求的,所以最终选择第三种来实现词频统计。
step4:单元测试
要求编写十个以上的测试用例,首先将题目的要求罗列,按照要求编造例子,一一对应测试,再找几篇真正的英语文字,从字数简到繁进行测试。
代码规范制定链接
设计与实现过程
文字+代码的设计构思
1.输入需要读取的文件路径和结果的输出路径
2.读取文件,统计文件的字符数(对应输出第一行)、单词数(对应输出第二行)、有效行数(对应输出第三行)
3.将文件的所有字母转化为小写
4.通过Map集合,以键值对的方式去存储单词和出现的次数
5.创建一个BufferReader的缓冲流,将字符流对象传进去,提高读取的效率
6.创建一个spilt数组,用来分割字符串,通过调用map的key值获取value,进行单词统计
7.利用TreeMap实现Comparator接口,对Map集合进行排序
要求总结
统计文件的字符数(对应输出第一行)
- 只需要统计Ascii码,汉字不需考虑
- 空格,水平制表符,换行符,均算字符
统计文件的单词总数(对应输出第二行)
单词:至少以4个英文字母开头,跟上字母数字符号,单词以分隔符分割,不区分大小写
- 英文字母: A-Z,a-z
- 字母数字符号:A-Z, a-z,0-9
- 分割符:空格,非字母数字符号
- 例:file123是一个单词, 123file不是一个单词。file,File和FILE是同一个单词
统计文件的有效行数(对应输出第三行)
任何包含非空白字符的行,都需要统计
统计文件中各单词的出现次数(对应输出接下来10行),最终只输出频率最高的10个
- 频率相同的单词,优先输出字典序靠前的单词
- 输出的单词统一为小写格式
代码和注释
统计文件的字符数、英文单词数、行数。
int charnum= 0 ;
int wordsnum= 0;
int linenum = 0;
InputStreamReader isr = new InputStreamReader(new FileInputStream(docin));
BufferedReader br = new BufferedReader(isr);
while( br.read()!= -1){
String s = br.readLine();
charnum += s.length();
//在统计单词时,用split根据" "对每一行的单词进行统计
wordsnum += s.split(" ").length;
linenum ++;
}
isr.close();
//写入文件
out.write("characters: " + charnum);
out.newLine();
out.write("words: " + wordsnum);
out.newLine();
out.write("lines: " + linenum);
out.newLine();
按照单词的出现次数从高到低排序
List<Map.Entry<String, Integer>> list = new LinkedList<Map.Entry<String, Integer>>();
list.addAll(map.entrySet());
Collections.sort(list, new Comparator<Map.Entry<String, Integer>>() {
// 从高往低排序
public int compare(Map.Entry obj1, Map.Entry obj2) {
if (Integer.parseInt(obj1.getValue().toString()) < Integer
.parseInt(obj2.getValue().toString()))
return 1;
if (Integer.parseInt(obj1.getValue().toString()) == Integer
.parseInt(obj2.getValue().toString()))
return 0;
else
return -1;
}
});
将出现频率最高的前十个单词写入文件
int i = 0;
//如果单词数大于等于10,就输出前10个单词
if(wordsnum>=10) {
for (Iterator<Map.Entry<String, Integer>> ite = list.iterator(); i < 10; i++) {
Map.Entry<String, Integer> maps = ite.next();
out.write(maps.getKey() + ": " + maps.getValue());
out.newLine();
//System.out.println(maps.getKey() + "\t" + maps.getValue());
}
}
//如果单词数小于10,有几个单词就输出几个单词
else {
Iterator<Map.Entry<String, Integer>> ite = list.iterator();
if(ite.hasNext()) {
Map.Entry<String, Integer> maps = ite.next();
out.write(maps.getKey() + ": " + maps.getValue());
out.newLine();
//System.out.println(maps.getKey() + "\t" + maps.getValue());
}
}
把文件中所有单词转化为小写
public static void toLower(String file) throws Exception{
Reader myReader = new FileReader(file);
Reader myBufferedReader = new BufferedReader(myReader);
CharArrayWriter tempStream = new CharArrayWriter();
int i = -1;
do {
tempStream.write(i);
i = myBufferedReader.read();
if(i >= 65 && i <= 90){
i += 32;
}
}while(i != -1);
myBufferedReader.close();
Writer myWriter = new FileWriter(file);
tempStream.writeTo(myWriter);
tempStream.flush();
tempStream.close();
myWriter.close();
}
统计单词词频
public Map<String, Integer> wordCount(String fileName) throws IOException {
// 打开文件
File file = new File(fileName);
FileInputStream fis = null;
fis = new FileInputStream(file);
// 英文单词以空格为分隔符,将单词分隔,并将所有大写字母转换为小写
BufferedReader bufr = new BufferedReader(new InputStreamReader(fis));
String s = null;
while ((s = bufr.readLine()) != null) {
// 移除字符串的前导空白和后尾部空白
s = s.trim();
// 正则表达式:以非字母或者是数字为分隔符,进行分割
String[] str = s.split("(\\s+\\W+)|[\\s+\\W+]");
for (int i = 0; i < str.length; i++) {
// 如果HashMap中已有该值,将值加1
if (wordCount.containsKey(str[i])) {
wordCount.put(str[i], wordCount.get(str[i]) + 1);
} else {
// 默认初始化该单词的出现次数为1
wordCount.put(str[i], 1);
}
}
}
// 移除HashMap中的""空字符串
wordCount.remove("");
return wordCount;
}
性能改进
初始我将统计字符数、单词数与行数分开统计,代码如下:
//统计字符数和单词数
FileReader fr = new FileReader(file);
BufferedReader bfr = new BufferedReader(fr);
char ch;
char fch='A';
//字符数
int countc = 0;
//英文单词数
int countw = 0;
//按字符读取文本内容
while((ch = (char) bfr.read()) != (char)-1)
{
//统计文本中字符数
if(ch != '\n' && ch != '\r')
//累计字符数
countc++;
if(!(ch>='a'&&ch<='z')&&!(ch>='A'&&ch<='Z')&&((fch>='a'&&fch<='z')||(fch>='A'&&fch<='Z')))
{
//累计单词数
countw++;
}
fch=ch;
}
out.write("characters: "+countc);
out.newLine();
out.write("words: "+countw);
out.newLine();
//System.out.println("characters: "+countc);
//System.out.println("words: "+countw);
//统计文件的行数
FileReader fr1 = new FileReader(file);
LineNumberReader lnr = new LineNumberReader(fr1);
int linenumber = 0;
while (lnr.readLine() != null){
linenumber++;
}
out.write("lines: " + linenumber);
out.newLine();
//System.out.println("Total number of lines : " + linenumber);
lnr.close();
虽然能成功实现功能,但是这样的代码十分冗长且性能低,在查阅了相关的代码后 ,我用了如下代码代替:
int charnum= 0 ;
int wordsnum= 0;
int linenum = 0;
InputStreamReader isr = new InputStreamReader(new FileInputStream(docin));
BufferedReader br = new BufferedReader(isr);
while( br.read()!= -1) {
String s = br.readLine();
charnum += s.length();
wordsnum += s.split(" ").length;
linenum ++;
}
isr.close();
out.write("characters: " + charnum);
out.newLine();
out.write("words: " + wordsnum);
out.newLine();
out.write("lines: " + linenum);
out.newLine();
初始我没有考虑到单词小于10的情况,会产生越界错误,经过修改后将单词分为10个及以上和十个以下两种情况讨论。
int i = 0;
if(wordsnum>=10) {
for (Iterator<Map.Entry<String, Integer>> ite = list.iterator(); i < 10; i++) {
Map.Entry<String, Integer> maps = ite.next();
out.write(maps.getKey() + ": " + maps.getValue());
out.newLine();
//System.out.println(maps.getKey() + "\t" + maps.getValue());
}
}
else {
Iterator<Map.Entry<String, Integer>> ite = list.iterator();
if(ite.hasNext()) {
Map.Entry<String, Integer> maps = ite.next();
out.write(maps.getKey() + ": " + maps.getValue());
out.newLine();
//System.out.println(maps.getKey() + "\t" + maps.getValue());
}
}
在进行了以上优化后,再用JProfiler来测试性能,得到如下结果:
单元测试
单元测试我选择的是一篇简单的英语作文,其中包含大小写、按字典排序等内容,具有常规性和代表性。
单元测试我选择的是一篇简单的英语作文,其中包含大小写、按字典排序等内容,具有常规性和代表性。
词频统计结果:
单元测试结果:
总结:
由单元测试的详细内容可知,覆盖率已为100%。有一处不完全执行,功能将是大写转化为小写,因为所提供的词不全为大写,所以不完全执行为正常现象。由此可得可以暂时不用优化覆盖率。
异常处理说明
本次程序的输入输出比较简单,所以利用的Exception有IOException和FileNotFoundException两种。即如果文件没有找到,或命令行参数输入参错误,就会抛出异常。
心路历程与收获
1.对大篇幅复杂工作的理解
这两次作业的篇幅都很长而且很详细,因为是第一次接触这种布置形式,感觉很难下手。后来我总结出了阅读流程:从checklist入手,先大致知道自己该干什么,然后按照checklist的顺序慢慢扩充具体细节。还有很重要的一点是要分好part,不能心急,虽然有些环节可以同时进行,但是会造成作业效率的降低,所以以我个人角度还是应该一步一步来。
2.自学能力的提升
大学很多东西都是靠自学,自学能力的提升主要还是要靠实践,纸上谈兵是没有用的。本次的git、github学习,我学习方式是先理解每个功能的作用再跟着教程实践一遍。感觉只要动手操作了,就学得挺快的。
3.编码能力的提升
这里的编码能力也包括debug能力。本次的统计词频我用了map,详细了解后才发现之前对map的使用都是皮毛,所以在编码过程中遇到了很多错误,通过无数次的debug才能达到无误。还有一点是因为细节太多,会常常忘了某项功能的实现,所以本次作业是在完成大致框架的情况下,慢慢添加功能进去。