Loading

寒假作业(2/2)

这个作业属于哪个课程 2021春软件工程实践|W班
这个作业要求在哪里 寒假作业2/2
这个作业的目标 阅读《构建之法》,提出问题。
完成WordCount程序
其他参考文献

阅读《构建之法》并提问

问题1

p204原文

image-20210304215520363

p204 在表9-2种中列出项目可能会遇到的风险和类别,我对其中的技术和环境的风险类别有一些疑问,据我查询资料所了解,产品经理可能不需要了解很多的技术,那么PM要怎么判断一个项目的开发和测试工具、平台、安全性呢?还有法律法规这一风险来源,作为一个产品经理,是否还要求需要对行业的法律法规有一定的了解呢?

关于我自己提出的问题,首先在技术这一风险层面,普遍认为产品经理如果懂技术更好,可以充分理解技术人员的难处来平衡各方。在法律法规这一方面,我在网上了解到的是在一个PM接项目时,会由公司的法务部门来进行对接,从而解决PM在法律层面上的知识漏洞。

问题2

p61原文:你发现他把时间都花在“解决(低层次)问题”上了,你想考察的“算法技能”、“C#程序设计技能”都无暇顾及。注意,这是在他认为非常精通的编程工具和编程语言中出现这样的问题。你要这样的员工么?

原文谈到一位简历上“精通Visual Studio C#”编程的面试者在被面试官要求用IDE写一段程序之后,漏洞百出。我的疑问是如何界定一个问题属于“低层次”的问题呢?首先我同意书中对面试过程中出现低级错误的观点,但我对“低层次”问题的界定仍然感到模糊,所谓的“精通”当然不该包含文中所说的“少了一个分号”、“怎么设断点”等问题,但一位程序员所谓的“精通”是否应该掌握一门语言的绝大部分内容呢?

就我个人的观点,“精通”自然应该是自己最为掌握的一项技能,从我个人的经历来说,真正掌握一门语言,应该是能够学习一门技术最前沿的发展方向,比如新版本新特性等等。

问题3

p351原文:大家听了很多创新者的故事,有些人想,他们真了不起,第一个想出了这些美妙的想法,要是我早生几十年,也第一个实现那些想法就好了。其实,大部分成功的创新者都不是先行者,例如搜索引擎,Google是很晚才进入这个领域的。

文中认为“创新者都是一马当先”这个观点是错误,还举到了Google公司涉足搜索引擎行业并不是最早的这个例子,但据我所了解,Google公司在其他领域却有着一马当先的创新,比如手机系统、云计算等领域。再说到我认为最顶尖的产品经理,行业内的认为就是“产品格局&社会价值”,所谓“传说中能够改变世界的产品经理”,那么这类顶尖的产品经理他们所长于的不正是创新吗,但他们能够更清晰地看清创新的社会价值和产品格局,这些不都是创新的例子吗?所以我认为“大部分成功的创新者都不是先行者”这个观点说法并不准确。

问题4

p85原文:在结对编程模式下,一对程序员肩并肩、平等地、互补地进行开发工作。他们并排坐在一台电脑前,面对同一个显示器,使用同一个键盘、同一个鼠标一起工作。他们一起分析,一起设计,一起写测试用例,一起编码,一起做单元测试,一起做集成测试,一起写文档,等等。

首先文中的结对编程与我原先预先的结对编程并不相同,我本来认为结对编程就是两个人组成一个团队完成任务,但看到文中的“同一台电脑,同一个显示器,同一个键盘”,我产生了疑问,这种方式是否会降低一个团队的效率呢?两个人做同样的事情,文中谈到的两个都对代码有着最强的熟悉感,能够比其他审核人员更快的找到问题所在,这点我完全统一,但这种方式是否是对开发人员的浪费呢?

我的理解是结对编程有很多优点,并且由老手带新手能够很快帮助新手快速熟悉自己不熟悉的领域。我在网上还看到一个观点:“结对编程人员产生的代码多于两倍“,我认为结对编程不适用于一整个团队,但对于团队的个别开发人员还是十分有帮助的。

问题5

P215原文:如果用户有不同的安全需求,切记要定义不同的角色来适应这些需求。如下面的例子:
受欢迎的典型用户—指那些按设计者的期望使用系统的用户,如“网站的购物者”;

不受欢迎的典型用户——指那些有不正当目的的用户,如在一个房地产业主论坛中滥发房屋中介广告的用户,这些用户也许在别的系统中(如房屋中介论坛)是受欢迎的。

书中说到用户可能有不同的需求,同时还要定义不受欢迎的典型用户,但下面又说到”我们的软件不是为所有人服务的“,我认为这中间有一定的矛盾,既然要定义用户,是否应该包含所有的用户使用可能?甚至包括利用软件漏洞进行功击的黑客。我个人的看法是应该仅可能多的定义出用户,虽然可能无法模拟出所有的使用环境,但更多的典型用户可以帮助软件团队更好地分析项目使用环境,包括安全问题的考虑。

WordCount编程

Github地址

项目地址

PSP表格

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

解题思路

第一遍看完题目,我首先想到的是对单词的判定,以及如何编写代码可以提升自己程序的性能。在充分理解完需求之后,我决定按三个核心模块来重新分析需求。

1、统计字符数

由于助教已经确认过不会出现ACSII范围外的字符,且需求是除了英文、数字外,空格、制表符和换行符都算作字符,所以我的思路是在读取文件后直接遍历文件获取字符数。

2、统计单词数

需求是至少以4个英文字母开头,以分隔符分割,其中除英文字母和数字外的任意字符均算作分隔符,其中单词不区分大小写。

我的思路是在遍历文件的同时,维护一个单词标记位,当标记位满足要求时,将其判定为单词。

3、统计最多的10个单词及词频

我对统计词频的思路就是维护一个以单词为key、出现次数为value的map,同时这个map需要按value降序排序,当value相同时,按key的字典序排序。

代码规范

codestyle.md

设计与实现

类设计

public class Lib{
    public static int lineNum;
    //文件工具类,用于读取文件和写入文件
    public static class FileUtil{...}
    //统计字符数,返回long
    public static long countChars(String inputPath);
    //统计单词数
    public static long countWords(String inputPath);
    //统计词频,返回string
    public static String countWordFrequency(String inputPath);
    //统计每行单词,返回map
    private static Hashtable<String,Long> countLineWords
            (String line,Hashtable<String,Long> mapWord,boolean countWords);
    //map排序,将值按升序排序,当值相同时键按字典序排序    返回map
    private static Map<String, Long> sortWord(Map<String,Long> map);
    //遍历文件统计单词
    private static Hashtable<String,Long> countWordsTable
            (BufferedReader tempReader,boolean countLine);
}

I/O设计

选用BufferedReader,最初的想法是用BufferedReader的readLine()方法处理文件每行的字符串,用每行字符串的长度来统计字符数,但发现在统计字符时readLine()方法无法统计到换行时的换行符号,后来又发现用read()方法读取每个字符可以读到换行符,所以统计字符模块使用read()方法,统计单词数和词频模块使用readLine()方法。

数据处理设计

因为我统计单词和词频都是用每行的字符串来统计一行内的单词,所以我只需要设计一个函数处理字符串即可,因为统计单词数和统计词频是两个独立的模块,但又都需要遍历文件处理,处理时一样要判断单词,为了减少代码的重复率,我设计了一个函数让他们调用相同的函数,并通过一个布尔值来判断是谁调用了函数。

统计字符数

利用BufferedReader的read()方法,遍历文件即可。

while(tempReader.read()!=-1)         //遍历reader
    charsNum++;
判断单词逻辑

因为我没有对文件进行分割,直接利用每行的字符串来处理,所以我需要判断一个字符串包含的单词数和出现次数,并且还需要维护一个map,将新判断的单词和出现次数添加到map中。我利用维护一个wordFlag来判断单词。

流程图如下:

未命名文件

因为整部分都是我的核心判断逻辑,所以我这一段贴的代码比较多。核心代码如下:

for(int i=0;i<line.length();i++){
    if(Character.isLetter(line.charAt(i))){
        wordFlag++;                             //如果是字母,将wordFlag++;
        tempWord.append(line.charAt(i));        //用于清空tempWord
    }
    else if(Character.isDigit(line.charAt(i)) && wordFlag>=4){
        wordFlag++;        //当wordFlag>=4时,判定为一个单词,若后续为数字,将wordFlag继续++
        tempWord.append(line.charAt(i));
    }
    else if(wordFlag<4){  //当wordFlag<4且遇到字符为数字时,说明不可能为单词,将wordFlag置零
        wordFlag=0;
        tempWord.delete(0,tempWord.length());
    }
    if(!Character.isLetterOrDigit(line.charAt(i)) || i==line.length()-1){
        if(wordFlag>=4){                //当遇到分隔符或检索到行尾时
            wordsNum++;            //若有wordFlag>=4,判定为单词有效,将有效单词++
            String tempWordString=tempWord.toString().toLowerCase();
            if(!countWords){ //加入map前判断单词是否存在,存在则取出其value,将其+1后放入map
      long count=mapWord.containsKey (tempWordString)?mapWord.get(tempWordString):0;
                mapWord.put(tempWordString,count+1);
            }
        }
        wordFlag=0;              //不论是否为有效单词,都将wordFlag置零
        tempWord.delete(0,tempWord.length());
    }
}
统计单词数

将文件打开得到BufferedReader,将其传入判断单词函数。

Hashtable mapWord=countWordsTable(tempReader,true);//调用函数
return (long) mapWord.get("wordsNum");
统计有效行数

因为判断有效行数不是一个独立的模块,所以我将其与判断单词数一起判断。

统计词频

因为判断单词的函数返回一个map,那统计词频只需将其排序后生成字符串返回即可,按照value降序排序,当value相同时,按key的字典序排序。

Map<String,Long> sortMap=sortWord(countWordsTable(tempReader,false));//调用函数         
for (Map.Entry<String, Long> entry : entrySet) {
    frequency.append(entry.getKey()).append(": 		").append(entry.getValue()).append('\n');		//遍历map生成字符串
}
return frequency.toString();//返回字符串

性能改进

改进思路

我认为可以从以下几个方面改进程序的性能。

I/O方面

读取文件采用了带缓存的BufferedReader,减少了I/O操作。

判断单词

我的程序采用的方式是用BufferedReader一次读取一行,每行独立判断单词的数量、出现频率等。在与同学交流之后我发现还可以采用按分割符分割字符串,后再单独进行判定单词的方式,可以进一步提高性能。但因为我已经在测试阶段,所以没有修改我的程序。

词频的输出

由于我是按map来存单词的出现次数的,所以需要单独对整个存单词的map进行一次按value排序,在参考了其博客后,选择用流式编程来排序。

三个模块多线程运行

我让三个模块开三个线程同时处理文件,而不是等一个模块处理完后轮到下一个模块处理,该方法极大得缩短了处理时间,但缺点是运行时可能会造成内存占用过高的问题。

改进前后

测试数据规模

随机生成,文件大小320M,一千五百万个有效单词,五十万行,超过三亿个字符。

for(int i=0;i<=50000000;i++){
    for(int j=0;j<(int)(Math.random()*10);j++){		//随机生成0~10的字符串
        c=(char)(int)(Math.random()*26+97);
        stringBuilder.append(c);
    }
    stringBuilder.append((int)(Math.random()*1000)).append(',');//随机加上1000以内数字
    if((int)(Math.random()*100)==50)
        stringBuilder.append('\n');				//随机换行
}
改进前

运行时间52s,内存占用最高1.53G

image-20210305164901966
改进后

运行时间35s,内存占用最高2.53G

image-20210305164609299

程序测试

覆盖率

image-20210305165508217

总覆盖率78%,因为我在WordCount类中保留和生成随机文本的测试函数,仅在测试时使用,所以覆盖率应该更高。

覆盖率的优化:

  • 简化代码逻辑
  • 剔除重复代码

单元测试

字符统计测试
@Test
public void testCountChars() throws IOException {
    String testFile="charsTest.txt";
    BufferedWriter out=new BufferedWriter(new FileWriter(testFile));
    String testString="abcABC\n...123adg\n";//构造包含字母、数字、换行符、符号的字符串;
    for(int i=0;i<1000;i++)
        out.write(testString);                      //循环写入一千次
    out.close();
    int charsNum= Math.toIntExact(Lib.countChars(testFile));//实际结果
    assertEquals(testString.length()*1000,charsNum);
}
单词数量统计测试
@Test
public void testCountWords() throws IOException {
    String testFile="wordsNumTest.txt";
    BufferedWriter out=new BufferedWriter(new FileWriter(testFile));
    String testString="abcd123 test456 .123 lqww3\n";           //构造三个单词的字符串
    for(int i=0;i<1000;i++)
        out.write(testString);                                  //循环写入一千次
    out.close();
    int wordsNum= Math.toIntExact(Lib.countWords(testFile));
    assertEquals(3000,wordsNum);                        //实际结果应为3000个单词
}
单词频率测试
@Test
public void testCountWordFrequency() throws IOException {
    String testFile="frequencyTest.txt";
    BufferedWriter out=new BufferedWriter(new FileWriter(testFile));
    String testString="aaAA AAaa asd123 abcd ABCD lqwLQW \n";//每行aaaa两个、abcd2个、lqwLQW1个
    for(int i=0;i<1000;i++)
        out.write(testString);                              //循环写入一千次
    out.close();
    String actual=Lib.countWordFrequency(testFile);
    String expected="aaaa: 2000\nabcd: 2000\nlqwlqw: 1000\n";	//预期结果
    assertEquals(expected,actual);
}
测试结果

三个函数均通过测试。

image-20210305181257713

异常处理说明

程序的异常基本上是文件处理异常,如果找不到输入文件、输出文件错误等等。

当输入文件错误时,抛出找不到文件异常。

image-20210309155912621

当命令行参数不为2个时会提示,"参数错误!"

if(args.length!=2){
    System.out.println("参数错误!");
    return ;
}
image-20210309160015219

心路历程与收获

心路历程

  • 我最开始看到作业要求的时候就一心想着先把单词判断的逻辑写好,也没注意到要把核心模块化的问题,认为先把大致逻辑写好,再去分模块应该也没关系,这直接导致了我的代码耦合度过高,要分模块时无从下手,这还导致了我错误地判断了我的程序的性能,在和同学一起测试的时候发现自己的程序比他们跑得更快(但是我也不知道是为什么),等分完模块,算法的不足就逐渐显现出来。所以我学到在开始编写代码之前,应该先分析清楚所有的需求,就算是对需求的某一方面立刻有了想法,应该是先记录自己的想法,再去分析完整的需求。
  • 在性能改进方面,我认为只有对底层有一定的了解,才能真正找到对自己项目性能改进的最优解;以及对项目性能测试的用例,一开始我只是简单生成大文件,认为只要足够大的文件就足够测试程序的性能,没有意识到测试用例的随机性。在我重新生成了完全随机的测试用例之后,就发现了与用了更好的算法的同学们的性能差距。

收获

  • 意识到版本管理的重要性,以及每写出一些有效代码就提交一次代码,这样还可以很方便得帮助自己查错,除此之外,我认为也很重要的就是在每次提交时的描述,能让自己清晰的看到自己每次提交的内容,而不是还要回看修改的代码。
  • 在测试用例方面,我发现一个人很容易有自己的盲区,独自测试很难覆盖自己程序的所有BUG,但多人一起想测试用例就可以找出更多的BUG。
  • 第一次进行单元测试,也是第一次进行比较系统化的测试。单元测试可以帮助我们改善整体设计,把整个程序的错误细分到每个模块的错误,有效得帮助我们差错。

posted @ 2021-03-05 21:55  Savona  阅读(133)  评论(5编辑  收藏  举报