软工实践寒假作业(2/2)

这个作业属于哪个课程 2021春软件工程实践|W班 (福州大学)
这个作业要求在哪里 软工实践寒假作业(2/2)
这个作业的目标 1.阅读《构建之法》,提出5-10个问题
2.设计一个程序,能够满足一些词频统计的需求
其他参考文献 《构建之法》

part1:阅读《构建之法》并提问

问题一

2.3 个人开发流程

工程师在“需求分析”和“测试”这两方面明显地要花更多时间(多60%以上);但是在具体编码上,工程师比学生要少花1/3的时间
显然,从学生到职业程序员,并不是更加没完没了地写程序————花在写代码上的时间反而少了许多

  我认为这个结论是有问题的。文章对比的是学生和程序员在PSP表中的百分比数据,而非绝对时长,不能说工程师在“需求分析”和“测试”中花的时间多,或者说在编码上花的时间少。只能说明学生花在编码上的时间占比更高,学生花了相对比例更多的时间在编码上,而这也是情有可原的:以我的经历而言,学生时代初期大多时间花在具体某个算法,或者某个函数上,当需要编写较为大型的项目,需要把各种算法、函数、类集合在一起实现某种功能时,不是说把这些算法、函数、类写完堆一起就完事了,而是要花很多经历编写各个部分之间的接口、调用、异常之类相对繁琐的事情上。而职业程序员对这些应该是有丰富的经验,所以他们花在编码上的时间相对更少一些。

问题二

4.5 结对编程

在结对编程模式下,一对程序员肩并肩、平等地、互补地进行开发工作。
驾驶员:控制键盘输入。领航员:起到领航、提醒的作用。
驾驶员和领航员不断轮换角色,领航员要控制时间。
只有水平上的差异,没有级别上的差异。

  这一小节让我感到十分新奇,也是第一次接触这种思想。现实生活中类似的搭档关系,如飞机驾驶员和副驾驶,战场上的狙击手和观察员,这些搭档一般不会互相轮换角色,但是结对编程中的两个人却可以不断轮换角色。但是我认为这样一对组合还是会有大体分工的,一个人更多的作为驾驶员,而另一个人更多的作为领航员。
  而我想到的问题是,如果结对编程的两个人,在水平上有高低之分,那么是高水平的人更多作为驾驶员,还是低水平的更多作为驾驶员。换句话说,驾驶员和领航员哪一个需要更高的水平?
  思考之后我认为,由于驾驶员控制键盘输出,会被此分心,而领航员全程专注于项目,需要更高水平来进行指导、检查、纠错等。

问题三

5.2 软件团队的模式

软件团队又各种形式,适用于不同的人员和需求。
包括 主治医师模式、明星模式、社区模式、业余剧团模式、秘密团队、特供团队、交响乐团模式、爵士乐模式、功能团队模式、官僚模式

  这一节介绍了各种形式的软件开发模式,而这些模式是基于不同的人员和需求锁形成,而我想知道面对一些外部因素,哪一种开发模式更加稳定。比如当团队中突然有人员变动发生,哪一种开发模式能平稳过渡。或者是开发需求突然有重大变化,哪一种开发模式能迅速转型。
  深入了解了各个开发模式的区别后,我对自己的问题有了一些看法。主治医师模式和明星模式基本不用担心非核心人物的人员变动,因为主导权掌握在“主治医师”和“明星”手中,只要核心不变,整个团队就能平稳过渡。而爵士乐模式面对突然改变的需求,能迅速转型,因为这种模式对变化的内容能给予有创意的回应。

问题四

6.1 敏捷的流程简介

产品订单上的任务被进一步细化了,被分解为以小时为单位。
冲刺期间,团队通过每日例会来进行面对面的交流,团队成员大多站着开会,所以又称为每日立会。
每日立会强迫每个人向同伴报告进度,迫使大家把问题摆在明面上。
同时团队要启动每日构建,让大家每天都能看到一个逐渐完善的版本。

  敏捷流程能加强团队交流,但过于频繁的交流(每日立会),甚至是强迫交流,会不会使得开发人员的思维变得混乱?而这种每日立会是否有一些形式主义?在大型团队中进行每日立会,需要所有人都参与吗?如果所有人都必须参与的话,必然会浪费一些人的时间,因为不是所有人都能在同一时间完成手头上的工作,从而参加会议,而工作被打断是一件非常令人恼火的事情。是否可以拆分每日立会,让工作有依赖关系的成员进行高质量的单独小会。
  这些问题不是我所能解答的,而是需要通过大量的工作经验才能摸索出一套独特的方法。

问题五

12 用户体验

  以qq和微信为例,写一些我对于用户体验方面的疑惑。很多人说qq太繁重,什么个性主题、气泡、背景、字体、qq秀乱七八糟的,还集合了很多花里胡哨的功能,不像微信一样简洁。但事实上qq通过简单设置,同样能变得轻便简洁,但是qq能变得简单,微信却不能因人而异的变得更加有趣。而关键这一点,就在于设置。
  在用户体验上,是应该给用户一个开关,还是说直接对功能进行阉割或是强制给予呢?大部分人会倾向于选择一个开关,但是再以小米手机系统为例,小米最新系统MIUI12,推出了一种新式下拉菜单,但同时也保留了旧的模式,用户可以通过一个开关来选择新旧模式。但问题出在MIUI系统中有很多新功能都被加了一个开关,导致整个系统处处都是开关,看上去选择是变得更多了,但实际上却变得更加臃肿,如果一个人换了手机,可能就需要很长时间才能调整好每一个开关,回到原先的设置。
  所以说在软件更新中,是应该强制推出新功能让用户适应,即创造用户体验,还是应该保守一些,制作一个开关,让用户选择,或者干脆阉割新功能,以不变应万变?

附加题

  1994年,Intel 为奔腾 CPU 的浮点除法指令 FDIV 加入了一种新型的实现。这是 Sweeney-Robertson-Tocher(SRT)算法的一种高性能变体,依赖了一个共有 2048 项的硬件查找表。因为这种算法只会访问整个 128x16 尺寸查找表中的一个梯形子集,所以这 2048 项中只有略多于一半的项会被用到。由于一些意外,这 1066 项中有 5 项的值被错误地设置为 0(而不是正确的 2),因此可能导致运算结果的错误。但是,这些错误的索引只会在极少数情况下被访问到,以至于这个问题没有被 Intel 研发流程中的随机测试所发现。
  1994年10月,美国弗吉尼亚州Lynchburg College数学系教授Thomas Nicely为研究孪生质数,发现用电脑处理长除法时一直出错。他用一个数字去除以824,633,702,441时,答案一直是错误的。他在 1994 年的 10 月 24 日(程序员节)向 Intel 提出了反馈,并于 10 月 30 日向其他的一些联系人发送了报告问题的电子邮件,很快这个问题被发到网络上,引起了全世界计算机从业者们的兴趣。
  挪威工程师 Terje Mathisen成功复现了 Nicely 教授的例子。他用汇编语言写了一个简单的测试用例。德国的 Andreas Kaiser 找到了 20 多个特殊的数字,这些数字的倒数在奔腾 CPU 上的计算精度只达到了单精度。加州 Vitesse 半导体公司的 FPU(浮点单元)设计师 Tim Coe分析推断出了 Intel 的 FPU 设计师们是如何设计除法电路的。
  美国麻省一家公司的老板 Cleve Moler总结了 bug 规律,即除数都略少于 3 乘以 2 的某个整数次幂。基于自己找到的规律,Moler 开始与 Coe、Mathisen、Peter Tang(来自美国阿贡国家实验室),以及 Intel 的几位软硬件工程师合作,尝试解决这个 FDIV 错误。他们开发出了一种巧妙的修复方法:当除数有效位的八个高位是 00011111、01001111、01111111、10101111 或 11011111 时,将除数和被除数都同乘以 15/16。这项优化技术被公布到了新闻组,可供全社会无偿自由使用。
  于是,报道「该公司修复了 Intel 奔腾 CPU 浮点数 bug」的新闻,迅速登上了包括纽约时报在内的各大主流媒体。这家当时还名不见经传的小公司,由此正式出现在了公众视野之中。而这家公司就是出品 MATLAB 的 MathWorks。
来源:最难调试修复的 bug 是怎样的? - doodlewind的回答 - 知乎
Pentium FDIV bug - 百度百科

part2:WordCount编程

Github项目地址

PersonalProject-Java

PSP表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划
• Estimate • 估计这个任务需要多少时间 5days 4days
Development 开发
• Analysis • 需求分析 (包括学习新技术) 60 90
• Design Spec • 生成设计文档 30 15
• Design Review • 设计复审 30 15
• Coding Standard • 代码规范 (为目前的开发制定合适的规范) 20 20
• Design • 具体设计 30 60
• Coding • 具体编码 300 600
• Code Review • 代码复审 60 20
• Test • 测试(自我测试,修改代码,提交修改) 120 180
Reporting 报告
• Test Repor • 测试报告 30 30
• Size Measurement • 计算工作量 10 10
• Postmortem & Process Improvement Plan • 事后总结, 并提出过程改进计划 20 20
合计 710 1060

解题思路描述

看到题目就想到用split 函数对字符串进行切割,后来改成了速度更快的Stringtokenizer ,然后使用Map 保存单词和词频,这是最基本的功能。之后就是对词频表进行排序。而字符、单词、行数统计可以通过对输入文件的字符串进行操作计算获得。

代码规范制定链接

codestyle

设计与实现过程

代码包括WordCount 和Lib 两个类,其中WordCount 是主类,Lib 是工具类,其中包括9个方法。
WordCount 中的main 有一个run 方法,接收两个文件路径,然后依次调用Lib类中的各个函数,包括读取文件,构造词频表,分别计算字符、单词、行数,对词频表排序,构造输出信息,输出文件。

  • 各个函数之间的关系
接收 返回
读取文件 内容字符串
计算字符、行数 内容字符串 int 类型的字符数和行数
构造词频表 内容字符串 Map 类型词频表
计算单词 Map 类型词频表 int 类型单词数
对词频表排序 Map 类型词频表 词频字符串
构造输出信息 int 类型字符数、行数、单词数和词频字符串 输出字符串
单词判断 字符串 boolean类型
输出文件 输出字符串
  • 最终工具类Lib包含
/** 读取文件 ...*/
public static String getFile(String filename)
/** 字符统计 ...*/
public static int countCharacters(String content)
/** 单词统计 ...*/
public static int countWords(Map<String, Integer> map)
/** 行数统计 ...*/
public static int countLines(String content)
/** 构造词频表 ...*/
public static Map<String, Integer> countFrequency(String content)
/** 对词频表排序 ...*/
public static String sortFrequency(Map<String, Integer> map, int num)
/** 构造输出文件的信息 ...*/
public static String answerBuilder(int chars, int words, int lines, String frequency)
/** 单词判断 ...*/
public static boolean IsWord(String word)
/** 输出文件 ...*/
public static void outputInfo(String output, String answerBuilder)
  • 由于题目主要是词频统计,所以一开始就使用replaceAll 替换字符串中所有非英文数字的字符,使用Stringtokenizer 来分割读入的文件内容,采用HashMap 来记录词频,通过将HashMap 中的包含映射关系的视图entrySet 转换为List ,然后重写比较器对词频进行排序,这一段作为核心功能,从头到尾没有变过,之后为了功能拆分,将词频排序放在按字典顺序输出功能部分,下文会提到。
Map<String, Integer> map = new HashMap<>();
content=content.replaceAll("[^A-Za-z0-9]", " ");
StringTokenizer st = new StringTokenizer(content, " ");
while (st.hasMoreTokens()) {
    String word = st.nextToken();
    //将单词全部转为小写
    word = word.toLowerCase();
    //根据作业要求,判断截取字符串是否为单词
    if (IsWord(word)) {
        if (map.get(word) != null) {
            int value = map.get(word);
            value++;
            map.put(word, value);
        } else {
            map.put(word, 1);
        }
    }
}
return map;

其后的工作主要是编写输入输出、字符数、单词数、行数和按字典顺序输出功能。

  • 输入输出
    开始使用FileReader 和BufferedReader 按行输入,但是发现这种方法不会记录换行符,导致字符数、行数统计出现问题,之后使用BufferedInputStream 和FileInputStream 输入,能获得所有字符,通过StringBuilder 的append 和toString 两个函数构造最后的文件内容字符串。以下代码段不包含try/catch部分。
byte[] buffer = new byte[1024];
//创建BufferedInputStream 对象
BufferedInputStream bi = new BufferedInputStream(new FileInputStream(filename));
int bytesRead;
//从文件中按字节读取内容,到文件尾部时read方法将返回-1
while ((bytesRead = bi.read(buffer)) != -1) {   
    //将读取的字节转为字符串对象
    String temp = new String(buffer, 0, bytesRead);
    content.append(temp);
}
return content.toString();        
  • 字符统计
    开始使用按字符读取文件的方式计算字符,之后改写了输入方式,直接对获得的文件内容使用String 中的方法Length 获得字符数量。

  • 单词统计
    开始使用Stringtokenizer 分割字符串后,逐个判断计数,之后直接对词频表计算所有Value的和。

for (Integer value : map.values()) {
    words += value;
}
  • 行数统计
    开始使用逐行输入计算行数,但是这个方法不会跳过空行,之后改写了输入方式,通过Stringtokenizer 对换行符切割,然后判断有效行进行计数。
StringTokenizer st = new StringTokenizer(content, "\n\r");
while (st.hasMoreTokens()){
    String line = st.nextToken();
    if(!line.trim().equals("")){
        lines++;
    }
}
  • 按字典顺序输出
    首先是词频排序,这个功能是最早写在获取词频表中的,之后划分到这一块,上文有提到过。
//将HashMap 中的包含映射关系的视图entrySet 转换为List ,然后重写比较器
List<Map.Entry<String, Integer>> list = new ArrayList<>(map.entrySet());
//idea自动转化成lambda表达式
list.sort((o1, o2) -> o2.getValue().compareTo(o1.getValue()));

  之后按Value 大小,记录同Value 值的Key 成为同词频单词表,对该表按字典顺序排序后,记录Key 和Value ,获得前10个记录后,退出函数。
  这一段代码显得有些复杂,但我暂时也想不出更好的优化方法了。

StringBuilder result = new StringBuilder();
//同词频单词表
List<String> sameFrequency = new ArrayList<>();
//输出统计
int outputCount = 0;
for (int i = 0; i < list.size() && outputCount < num; i++) {
    //如果当前字符词频与下一个不一样,则对当前所有同词频单词排序
    if ((i == list.size() - 1) || !list.get(i).getValue().equals(list.get(i + 1).getValue())) {
        //将当前单词加入同词频单词表
        sameFrequency.add(list.get(i).getKey());
        //对同词频单词表排序
        sameFrequency.sort(String::compareTo);
        //按字典顺序记录同词频单词
        for (String s : sameFrequency) {
            String temp = s + ": " + list.get(i).getValue();
            result.append(temp);
            outputCount++;
            if (outputCount != num) {
                result.append("\n");
            } else
                break;
        }
        sameFrequency.clear();
    } else sameFrequency.add(list.get(i).getKey());
}
return result.toString();

性能改进

1.使用Stringtokenizer 代替了split ,加快了分割字符的速度。
使用split

使用Stringtokenizer

2.使用StringBuilder 代替了String 的字符串拼接,加快了字符串拼接速度
使用String 拼接

使用StringBuilder

3.使用BufferedInputStream 和FileInputStream 输入,代替了BufferedReader 和FileReader ,用字节流读取代替了字符流读取,加快了读取速度。
开始时分别用按字符读取和按行读取计算字符数和行数,优化了输入函数了,调整了各个函数,使得一次读取就能进行字符和行数的读取。
使用字符流

使用字节流

在大文件读取上速度能加快近一倍

单元测试

  • 统计字符测试
for (int i = 0; i < 128; i++) {
    char ch = 'i';
    sb.append(ch);
}
assertEquals(128, Lib.countCharacters(sb.toString()));
  • 统计单词测试
String s = "abcd,abcd\nabcd abcd\r abcd(abcd) abcd+abcd-abcd abcd";
assertEquals(10, Lib.countWords(Lib.countFrequency(s)));
  • 统计行数测试
String s = " \n \r \r\n a\na\ra\n\ra \na \ra \n\r a a \n a a \r a a \n\r";
assertEquals(9, Lib.countLines(s));
  • 判断单词测试
String[] trueWords = {"abcd", "abcdef", "abcd123"};
String[] falseWords = {"a", "abc", "a1", "ab123", "123ab", "123456"};
for (String word : trueWords) {
    assertTrue(Lib.IsWord(word));
}
for (String word : falseWords) {
    assertFalse(Lib.IsWord(word));
}
  • 统计词频测试
//构造一个包含字母、数字的字符串
String str = "abc abcd abcd abcd123 abcd123 123ab 123456 Lakers Champion GOGO gogo where amazing happens nice\n";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb.append(str);
}
String test = "..."
assertEquals(test, Lib.sortFrequency(Lib.countFrequency(sb.toString()), 10));
  • 输入输出测试
String s = "abcd,abcd\nabcd abcd\r abcd(abcd) abcd+abcd-abcd abcd";
String path = "test.txt";
Lib.outputInfo(path, s);
assertEquals(s, Lib.getFile(path));

测试结果

覆盖率

其中WordCount 是主函数,单元测试的是Lib类中的各个功能
没有覆盖到的代码主要是catch模块。

异常处理说明

代码较为简单,除了IO异常,并无其他异常

心路历程与收获

了解了如何使用GitHub进行版本控制,不会再出现更新代码后发现bug,回不去的情况了,但是使用的时候还不够熟练,commit的时间点还是有问题,经常会有无效的commit出现,或者写着写着忘记commit,导致多个新增功能或者是多个不同的修改同时commit,这样代码复查起来就更麻烦了。

简单的进行了一些单元测试,只是最基本的测试,但也了解了单元测试的意义,希望在以后的项目中能利用到单元测试的更多功能。

了解了更多关于封装函数的知识,这次项目中的函数封装做的并不好,感觉还有很多提升空间,也希望之后的项目中能更好的封装。

posted @ 2021-03-04 22:22  Elsa1226  阅读(126)  评论(4编辑  收藏  举报