寒假作业2/2
任务一
问题1
PSP 方法,预估时间要怎么做才能有参考价值?PSP的真正作用是什么?
问题重述
|Analysis|需求分析 |
|Design Spec |生成设计文档 |
|Design Review |设计复审|
|Coding Standard|代码规范 |
|Design|具体设计 |
|Coding|具体编码 |
|Code Review|代码复审 |
|Test|测试(自我测试,修改代码,提交修改)|
|Reporting |报告 |
|Test Repor|测试报告 |
|Size Measurement |计算工作量|
都是“写程序”,学生和工作了的工程师有什么区别呢? 下表显示了笔者2011年收集的两组统计数据:
大学生: 在中科大 “现代软件工程”课程中, 每个学生记录了自己在完成个人项目时所花费的时间 (学生情况: 大学4 年级上学期, 计算机/电子/数学专业)。
工程师: 一群平均工作时间在3年左右,平均毕业学位为硕士的职业软件工程师(Software Design Engineer)的匿名调查.
SDE 比Senior Student多读了3 年书, 多工作了3年. 两类人任务的质量要求也不全一样, 我们可以看到SDE 在“需求分析”和“测试” 这两方面明显地要花更多的时间(多60% 以上);但是在具体编码上, SDE 要少花1/3 强的时间。 从这里看出, 从学生到职业程序员, 并不是更没完没了地写程序– 写程序的相对时间反而少了许多。
资料或事例
在第一次作业中,我很努力地按照PSP的方法记录我的时间,但是事实上无论是估计还是统计,我都没办法很好地进行。这还是建立在这次作业的技术我都已经使用过的基础上。
一方面,估计的时候,为了估计能更接近真实情况,就得去做一点需求的分析,进而统计的时候,估计的时候已经在进行需求分析了,需求分析的时候就已经在进行具体设计了。最后我统计的数据有很多是混在一起的。
提问原因
个人开发的时候,很难一开始就把所有的东西都在最开始定好,经常是跟着开发的推进做调整。时间的统计在很大程度上也没有很充足的可实际性。
有的技术框架互联网上的资料完整度有问题,甚至方向也是有问题的,怎么才能量化学习的时间?假设我想给一个网站增加文章搜索的功能,上网一搜大部分都是数据库,这就方向都有问题了。通过更进一步的搜索了解到全文检索技术之后,作为从没接触过这方面知识的人怎么才能预估自己能遇到什么样的问题?
上面例子给出的SDE写代码的时间比例小其实不能说明他比学生话更少的时间,他们的总时间又不一样。只能说明SDE需要花更多时间在分析和测试上,这更说明了随着项目的难度增加,这两件事是越来越难的,比起编码来说更难预测需要的时间。
自问自答
我觉得很大程度上这个方法只是一个概念,它能起到有限度的“激励”作用。因为除非项目所需的技术已经是自己有所接触的,否则一般来说预估时间是很难做到“有依据”的。
问题2
在概念上进行了创新就不算先行者么?
问题重述
迷思之四: 创新者都是一马当先
大家听了很多创新者的故事, 有些人想, 他们真了不起, 第一个想出了这些美妙的想法, 要是我早生几十年, 也第一个实现那些想法就好了。
其实, 大部分成功的创新者都不是先行者, 例如搜索引擎, Google 是很晚才进入这个领域的。 例如APPLE 的音乐播放器 iPod. 它是2001 年10 月23 日发布的, 在它之前市面上已经有很多产品了
iPod 出现后的几年时间里, 它甩开了对手
作者观点
首先研发出某种产品才算是先行。
我的观点
我认为在概念上,先行,可以理解为先人一步,可以是推出某种产品,当然也可以是对产品的认识。比如手机,苹果推出了第一个触屏手机,它是先行者,同时它推出的东西也是已经存在的,但是它的概念是全新的。
从作者给出的例子来说,谷歌最开始得到较好的声誉为后面的发展打造基础和它奇客(Geek)文化氛围、不作恶(Don’tbeevil)的理念有很大的关系,另外就是它对于页面相关性做出了优化。排除掉资本运作等其他的原因之外,它能获得成功恰是因为他是先行者,他在理念上做出了创新,对消费者有更清晰的认识,在技术层面上进行了创新。
问题3
关于复审,不可否认复审对于一个项目是有一定的好处。但是复审需要付出的代价和他带来的好处是匹配的么?
问题重述
如果开发者很厉害,那么复审者就没有什么作用,也许这些复审都是走过场?还是有一点用处,至少确保代码的作者把代码的逻辑和思想系统地表达了一遍,这样做本身就能发现不少问题
在严格规范进行开发的时候,作者也提到复审达到的效果在于作者把代码的逻辑和思想系统地表达了一遍。反过来说,是不是写代码的时候就遵循规范地注释流程逻辑,严格要求降低模块之间的耦合度,多测试几组测试样例,是否就可以很大程度上到达复审的好处同时降低时间成本?
提问原因
给别人讲解自己的代码,如果到要求对方能够对代码细节提出问题程度,那需要很多时间。除非本身就是逻辑很简单的代码。
自问自答
对于代码逻辑的复审,我认为可以通过规范开发解决,特别是在小的项目中。
对于算法效能、程序是否考虑周全等的复审,一方面我认为这和实际的场景需求有关,很大程度上是在编程之前就需要考虑的事情;另一方面现在很多的语言都有基础的算法库,像List、Map等都有提供实现,特别是JVM对性能其实做了很多优化,用点心的话也挺难把算法写的非常效率差的。//当然我说的不是框架级别的开发,我的能力还不足以我对框架开发有很清晰的认识
问题4
重大决定应该由投入最多的人来定夺,还是让技术认识最深的人来定夺?
问题重述
猪 - 他们或者辞掉了工作, 投入创业中; 或者这一门软件工程课是他们的必修课, 他们一定要拿到高分, 才能提高自己的GPA, 申请到好学校。 对他们来说, 要想项目成功, 他们要拿出自己身上的肉, 背水一战; 一旦失败, 自己的老本也赔进去了. 他们的投入级别是 - 全身心投入 (committed).
鹦鹉: 提供咨询, 它会每天阅读大量博客, 给其他团队成员提供建议, 例如最新业界趋势, 最新术语, SaaS, N-层架构, 创业明星当年的轶事, 等等。
重大决定由 “猪” 来定夺。
企业内不同的角色相互合作, 各有想法, 市场变化快, 应该听谁的呢? 是听那些在研发和市场第一线全心投入的 "猪", 还是坐办公室的“鸡”, 还是一些空降而来的 "鹦鹉"? 在软件企业培养新人, 是让他们对公司各项业务作高层次的点评, 写成漂亮的PPT (鹦鹉), 还是让他们坐办公室, 主管流程 (鸡), 还是把他们送到能听到炮声, 可能会流血的第一线 (猪)?
资料或事例
小的项目一般不会有那么多需要决定的东西。对于大的项目来说,假设考虑性能的话,比如说考虑高并发高可用等等,就需要比基础程序员更高的认识水平才可以把分布式项目完整架构起来。把项目架构起来之后,业务代码交给普通程序员就可以了。
提问原因
一方面是作者对猪
的抽象太理想化了,投入最多就等于认识最深,就等于在研发和市场第一线。
就我所学的java后端方向,一个项目有很大部分的代码是增删改查,不是说这些代码不重要,但是写多了并不能真的有多少提高,反而容易待在舒适区里出不去。
自问自答
首先猪
的定义看上下文,作者的意思是投入更多时间精力在项目里的人。但是真的应该由猪
来做决定么?猪
真的有做项目预研的视野和经验么?答案是不一定的,投入的多少和一个人的水平没有直接联系,很多时候花时间的只是业务层面的代码。架构系统更多需要的是对于技术的整体认识以及行业经验。某种程度上来说,知道的更多的鹦鹉
反而更有决定权。
问题5
通用软件的设计思想
到底是什么,怎么进步?
问题重述
通用的软件设计思想, 软件工程思想的提高
这一方面就比较虚,什么是好的软件设计思想, 什么是好的软件工程思想? 一个工程师开了博客, 转发了很多别人的文章, 这算有思想么? 另一个工程师坚持任何设计都要画 UML 图, 这算有思想么? 我个人比较重视一个程序员原创的博客, 在面试的时候, 我们别空口吹思想, 一起来看看你写的原创博客吧。
反对
作者仅提出了原创博客
,我觉得有点宽泛。原创博客表达形式罢了,真要看编程思想那还是得实际去编程。
自问自答
首先从从工程的角度上来讲,编程思想和代码的拓展性
、可读性
、架构合理性
等有比较密切的关系。所谓思想,很大程度上可以理解为指导实践的纲要。
我认为提升这方面的能力可以从两方面入手:
一个是选择自己熟悉的编程语言,阅读其有关编程思想上的书籍,如java的effective java
。这样的书籍因为有具体的编程语言作为载体,相比于通用的一些思想来说会更容易理解,以及有更详细的案例。
另一个则是阅读成熟框架的源码,从可用性、可拓展性等来看框架怎么架构是有比较大的帮助的。同时也可以练习阅读源码的能力。
任务二
项目地址
PSP
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
Estimate | 估计这个任务需要多少时 | ||
Development | 开发 | ||
Analysis | 需求分析 | 3h | 2h4min |
Design Spec | 生成设计文档 | 1h | 43min |
Design Review | 设计复审 | 30min | 5min |
Coding Standard | 代码规范 | 30min | 22min |
Design | 具体设计 | 3h | 1h23min |
Coding | 具体编码 | 2h | 4h12min |
Code Review | 代码复审 | 30min | 47min |
Test | 测试(自我测试,修改代码,提交修改) | 2h | 2h23min |
Reporting | 报告 | ||
Test Repor | 测试报告 | 1h | 0 |
Size Measurement | 计算工作量 | 30min | 0 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 2h | 20min |
合计 | 15h | 12h19min |
解题思路
需求分析
首先看完题干,这次任务可以分成几个部分
- 读取文件数据
- 处理文件数据
- 字符统计
- 单词统计
- 有效行数统计
- 按照格式输出文件数据
架构需要面临的问题有:类设计
、io流选择
,数据读取到内存后的数据结构
,数据处理算法
类设计
io工具类:IOUtil 负责读取文件以及输出文件。
int solveFileInASCII(String inputFilePath,StringBuilder stringBuffer) //读取file,把文件内容存在缓存中 返回非法字符数目
String getString(String inputFilePath) throws IOException //将文件中的所有字符转换成一个字符串返回
void writeTo(String outputFilePath,String content) //将内容写入文件
文本文件类:TextFileSolver
类说明:一个TextFileSolver与一个File相对应,通过IOUtil读取
public API:
TextFileSolver(String filePath) //构造函数
getValidLineNum //获取有效行数
getWordNum //获取单词数
getFileCharNum //获取字符数
getOrderedWordFrequencyMap(int size) //获取排好序的单词-频率 Map,通过foreach循环即可按顺序遍历,size为取出的单词-频率项条数的最大值
private:
solveString(String) //处理字符串
功能包装类: WordCount
类说明: 与TextFileSolver交互,将数据集合成答案,通过IOUtil输出。
独到之处: API的访问性比较讲究,防止用户随便调用。
public API:
WordCount(String inputPath,String outputPath) //构造函数
public void procceed() //执行WordCount
private:
initAnswerBuilder() //构造答案
writeAnswerToFile() //将答案写入到文件
io流选择
因为要处理字符文件,所以选择Reader的实现类
最开始考虑到性能,以及按行读取
的需求,选择BufferedReader来读取文件。
后来因为最后一个行的最后一个换行符没法成功统计,修改成了字节读取的输入流组合成整个字符串,利用System.getProperty("line.separator")
获取换行符来分割数据成几个数据行。//因为这个是后来才发现的,导致花了更多时间来修改架构
//然后最后又改成了"/n"来分割
数据结构选择
version1:
从文件中按行取出字符串使用List<String>
存储。
然后分别将List中的String转换成char[]进行字符统计以及基础处理,最终合并到一个统计单词的字符串
然后用String的API,以空白符为分割符分割成String List 然后通过Stream收集成Map<String,long>
version2:
从文件中按照字符取出一整个字符串
利用系统的换行符参数将字符串分割成List
然后分别将List中的String转换成char[]进行字符统计以及基础处理,最终合并到一个统计单词的字符串
然后用String的API,以空白符为分割符分割成String List 然后通过Stream收集成Map<String,long>
代码规范
数据处理算法
字符的处理:将非数字且非英文字母的字符转化成空白字符,遍历就可以了
单词处理:使用java Stream的API进行收集映射以及排序
收集相同单词:
//正则表达式切分字符串
wordFrequencyMap = Arrays.asList(solveStringBulder.toString().split("\\s+"))
.stream()
.filter(word->{ //过滤单词
if (word.length()<4) return false;
char[] chars = word.toCharArray();
for (int i = 0 ; i < 4 ; i++){
if (! Character.isLetter(chars[i])) return false;
}
return true;
})
.collect(Collectors.groupingBy(String::toLowerCase,Collectors.counting()));//按照小写字母收集单词,key:单词,value:频率
首先根据单词出现频率排序,然后根据单词字母序排序,取出前n个
public Map<String,Long> getOrderedWordFrequencyMap(int size){
return wordFrequencyMap
.entrySet() //获取set
.stream() //获取流
.sorted(Map.Entry.<String, Long> comparingByValue() //按照数值排序,默认升序
.reversed()//倒序
.thenComparing(Map.Entry.comparingByKey()))//按照key排序
.limit(size) //选择最前面的十个
.collect( //以map形式返回
Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(oldVal, newVal) -> oldVal,
LinkedHashMap::new
)
);
}
实现过程
先写了IO工具类,然后其他的基本使用StreamAPI解决。
遇到的最大的问题就是换行符处理。
- 因为经过搜索,java的换行符在不同的操作系统下是不一样的
/r Mac
/n Unix/Linux
/r/n Windows
所以我就把需要用换行符的地方改为System.getProperty("line.separator")
- java中按行读取的话,如果在最后加上换行符的话统计不到数据,然后就改成了按字符输入才解决。
- 另外则是中文处理思路,最开始的时候:这个问题可以通过把非法字符转化成空白符解决,但是这样文件就需要读取两次或者存两份。考虑到有提过不用统计中文,所以我选择直接把中文全都过滤再处理。审题后发现不太对,然后转化了思路:传入StringBuilder作为文件读取buffer,读取的时候保留非法字符,同时统计非法字符个数并返回。这样就解决了矛盾
性能改进
从这个项目的复杂度来说,性能瓶颈可能有两个地方:io、计算。
输入流的话我已经用了有缓存的BufferedInputStream
,而且文件只输入了一次。输出流用了BufferedWriter
,也是比较好的选择了。
计算的话,这次的项目计算有两项:
- 字符串处理,复杂度的话是O(n),字符串拼接我用了性能比较好的StringBuilder
- 词语的统计,这里我用了StreamAPI(上面有展示),JVM会优化操作流程。
算法性能测试
基本上时间复杂度和单词量呈线性增长,单词长度也会产生影响,不过相对小一点
测试数据:
hello,hello,hello,hello,i21
iiii1,iiii2,iiii3,iiii4,
iiii1,iiii1.iiii1
iiii5 iiii5 iiii5 iiii5 iiii5
iiii6
iiii7
iiii8
iiii9 iiii10 iiii11
java WordCount in.txt out.txt
successful,耗时76毫秒
测试数据:一共有 120,000 个单词
for (int i = 0; i < 10000; i++) {
stringBuilder.append("aaaa").append(i).append(",");
}
stringBuilder.append('\n');
for (int i = 0; i < 10000; i++) {
stringBuilder.append("bbbb").append(i).append(",");
}
stringBuilder.append('\n');
for (int i = 0; i < 10000; i++) {
for (int j = 0; j < 10; j++) {
stringBuilder.append("maxmax").append(j).append(",");
}
stringBuilder.append('\n');
}
java WordCount big.txt out.txt
successful,耗时243毫秒
测试数据: 1,200,000个单词
注:io流选择的影响非常大,在之前使用FileInputStream
的时候是九千多毫秒,换成缓存输入流后提速到一千多毫秒了
for (int i = 0; i < 100000; i++) {
stringBuilder.append("aaaa").append(i).append(",");
}
stringBuilder.append('\n');
for (int i = 0; i < 100000; i++) {
stringBuilder.append("bbbb").append(i).append(",");
}
stringBuilder.append('\n');
for (int i = 0; i < 10000; i++) {
for (int j = 0; j < 100; j++) {
stringBuilder.append("maxmax").append(j).append(",");
}
stringBuilder.append('\n');
}
java WordCount big.txt out.txt
successful,耗时1092毫秒
测试数据:120,000 个单词,单词加长到十倍
for (int i = 0; i < 10000; i++) {
for (int j = 0; j < 10; j++) {
stringBuilder.append("aaa");
}
stringBuilder.append(i).append(",");
}
stringBuilder.append('\n');
for (int i = 0; i < 10000; i++) {
for (int j = 0; j < 10; j++) {
stringBuilder.append("bbb");
}
stringBuilder.append(i).append(",");
}
stringBuilder.append('\n');
StringBuilder maxString = new StringBuilder();
for (int i = 0; i < 10; i++) {
maxString.append("maxmax");
}
for (int i = 0; i < 10000; i++) {
for (int j = 0; j < 10; j++) {
stringBuilder.append(maxString.toString());
stringBuilder.append(j).append(",");
}
stringBuilder.append('\n');
}
java WordCount long.txt out.txt
successful,耗时427毫秒
其他测试
计算模块正确性测试
单词收集, 构思思路:要能检查计算结果正确性所以用for循环构造测试数据,另一方面加入不满足要求的单词。
for (int i = 0; i < 10; i++) {
stringBuilder.append("a11 ");
}
for (int i = 0; i < 10; i++) {
for (int j = 0; j < i ; j++){
stringBuilder.append("test").append(i).append(" ");
}
}
successful,耗时10毫秒
test4: 4
test5: 5
test2: 2
test3: 3
test8: 8
test9: 9
test6: 6
test7: 7
test1: 1
单词收集,大数据测试。
测试参数:
for (int i = 0; i < 1000000; i++) {
for (int j = 0; j < 10 ; j++){
stringBuilder.append("test").append(i).append(" ");
}
}
successful,耗时2684毫秒
排序,取前10个
思路:1,000,000 个单词,同时加入value相同需要排字母序的数据
Map<String,Long> map = new HashMap<>();
for (long i = 0; i < 1000000; i++) {
map.put("text"+i,i);
}
map.put("text999998", (long) 999999);
map.put("text999997", (long) 1999999);
}
successful,耗时208毫秒
text999997: 1999999
text999998: 999999
text999999: 999999
text999996: 999996
text999995: 999995
text999994: 999994
text999993: 999993
text999992: 999992
text999991: 999991
text999990: 999990
整体正确性测试:
测试数据:
思路:有空格,有非法单词,有同频率按字母排序,只显示10个
hello,hello,hello,hello,i21
iiii1,iiii2,iiii3,iiii4,
iiii1,iiii1.iiii1
iiii5 iiii5 iiii5 iiii5 iiii5
iiii6
iiii7
iiii8
iiii9 iiii10 iiii11
结果:
characters: 140
words: 22
lines: 8
iiii5: 5
hello: 4
iiii1: 4
iiii10: 1
iiii11: 1
iiii2: 1
iiii3: 1
iiii4: 1
iiii6: 1
iiii7: 1
大数据量测试:
for (int i = 0; i < 100000; i++) {
stringBuilder.append("aaaa").append(i).append(",");
}
stringBuilder.append('\n');
for (int i = 0; i < 100000; i++) {
stringBuilder.append("bbbb").append(i).append(",");
}
stringBuilder.append('\n');
for (int i = 0; i < 10000; i++) {
for (int j = 0; j < 100; j++) {
stringBuilder.append("maxmax").append(j).append(",");
}
stringBuilder.append('\n');
}
characters: 10887782
words: 1200000
lines: 10002
maxmax0: 10000
maxmax1: 10000
maxmax10: 10000
maxmax11: 10000
maxmax12: 10000
maxmax13: 10000
maxmax14: 10000
maxmax15: 10000
maxmax16: 10000
maxmax17: 10000
大小写测试
aAaa aaaa AAAA aaAA Abbb abbb
characters: 30
words: 6
lines: 1
aaaa: 4
abbb: 2
中文测试: 中文不会被计算在字符数中,不会被统计成单词
哈哈哈哈 哈哈啊哈哈 hhhhh
characters: 7
words: 1
lines: 1
hhhhh: 1
中文测试:在四个字母后面的中文会被当成非法字符,算作非法单词,不加入统计
这里 aAaa就啊纠结啊叫啊叫
会被过滤
aAaa就啊纠结啊叫啊叫 aaaa AAAA aaAA Abbb abbb
characters: 30
words: 5
lines: 1
aaaa: 3
abbb: 2
中文测试:行数计算,中文当成是非空字符,会参与行数计算
哈哈哈哈 哈哈哈哈
哈哈哈哈哈
characters: 2
words: 0
lines: 2
中文测试:中文在中间的时候。
aaaa哈哈哈哈哈哈哈哈a
哈哈哈哈哈
characters: 6
words: 0
lines: 2
各种空白字符换行测试
哈哈哈
哈哈哈
characters: 10
words: 0
lines: 2
覆盖率截图
没有覆盖到的:
- IOUtil里有一些方法更新了输入io思路之后没有删除
- 异常块
异常处理
没有自定义异常,没有传入两个参数的话会提示错误。
基本上只有输入流异常,抛出到最外面跟参数放在同一个层级处理。
这样的话可以在跑完的时候给出一个成功提示,不然成功提示会因为异常太早被处理出现显示bug
总结
- PSP的方法实践起来效率挺低的,一方面体现在我自己统计时长的时候可能会在一段时间做其中几项,比如需求分析、设计、具体设计,这些可能在大的项目上可以分开,但是在这种小玩具项目上来说就是浪费时间。
- 多了解新特性会让代码写起来更轻松,stream编程让我节省了很多的功夫
- 这次整体架构上还行,就是
TextFileSolver
类的几个参数初始化可以更加模块化一些,这样后面我更改输入流应该会更轻松点 - 测试应该和开发同步进行
- 尽量形成“代码约束”,尽量在编译的阶段就发现问题。
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步