第三次作业-结对编程(wordcount)

GIT地址 https://github.com/gentlemanzq/WordCount.git
GIT用户名  gentlemanzq
结对伙伴博客地址
博客地址  https://www.cnblogs.com/gentlemanzq/
作业链接 https://edu.cnblogs.com/campus/xnsy/SoftwareEngineeringClass1/homework/2882

这一次结对编程,怎么说呢。带来了一次不一样的编程体验,很难说清楚。具体后面再说,先看作业


  • 一.结对过程

照片如下:(一直很疑惑为什么后台更改的照片,前端不会更改,百度了也没有解决办法。将就看吧)


  • 二.PSP表格

PSP2.1

Personal Software Process Stages

预估耗时(分钟)

实际耗时(分钟)

Planning

计划

 30  20

· Estimate

· 估计这个任务需要多少时间

 30  20

Development

开发

 670  740

· Analysis

· 需求分析 (包括学习新技术)

 180  180

· Design Spec

· 生成设计文档

 20  10

· Design Review

· 设计复审 (和同事审核设计文档)

 20  20

· Coding Standard

· 代码规范 (为目前的开发制定合适的规范)

 30  30

· Design

· 具体设计

 60  60

· Coding

· 具体编码

180  200

· Code Review

· 代码复审

 60  80

· Test

· 测试(自我测试,修改代码,提交修改)

 120  160

Reporting

报告

 85  105

· Test Report

· 测试报告

 45  60

· Size Measurement

· 计算工作量

 20  15

· Postmortem & Process Improvement Plan

· 事后总结, 并提出过程改进计划

 20  30
 

合计

 785  865


  • 三.解题思路

  1.首先拿到题目仔细阅读,理清题意。大致需要完成的功能都是关于字符操作和计数排序的操作。根据这几点需求,我们决定使用泛型Dictionary来完成这一系列操作。

  2.关于如何解题,是根据作业要求来的,首先需要将文件读入成一个字符串,然后对其进行操作,如果是统计字符数量和行数,需要借助Regex函数通过正则表达式 ‘’.‘’来统计除换行符之外的字符,最后加上换行符。(此处要注意换行符是两个字符)

  3.判断是否是单词的时候,先使用for循环将大小写统一转换为小写,重新定义一个新的Dictionary,然后使用笨办法if语句进行判断单词长度是否超过4个并且前四个是否是英文,如果满足条件则重新赋给新的dictionary。

  4.输出频率最高的10个词,如果频率最高则按字典序输出这个地方。将新的dictionary先用frequencies函数进行统计判断,然后将结果首先按照value值进行排序,然后再按照key值进行排序

  5.对每一个功能项进行封装,方便后面增加功能。

  6.考虑到以上涉及的知识,所以我们在编码前,首先参考了字典的用法,如何进行单词判断,如何按照key值和value值进行排序(参考这两篇博客【1】【2】)。其次是关于regex,正则表达式的使用(参考这篇博客【3】)。

  【1】:https://www.cnblogs.com/wt-vip/p/5997094.html

  【2】:https://blog.csdn.net/ybhjx/article/details/69668442

  【3】:https://blog.csdn.net/u012102536/article/details/85160138


  • 四.代码设计及接口封装设计

  1.首先对于每一个功能大致设置一个类,初步设计六个类,将除了program类其余放入function文件夹中,具体关系后面说明(PS:增加功能之后再添加)

  2.类与类之间的调用关系具体为:program类中调用path,linescount,asccount类。在asccount类中会调用linescount参与部分计算。wordcount调用ynword进行判断

  3.启动主函数在program类里面,如果要计算有多少个字符就调用ascount里面的agelife方法,如果要统计有多少行,就调用linescount中的lines方法。同理其余都是一样。

  4.基础功能的难点在于判断是否是单词,并且需要按照次数,字典序排序,在wordcount函数中先进行是否单词判断,此处调用ynword。具体设计见流程图

  5.单元测试设计,主要测试function文件夹中的功能函数,类图如下 (PS:单元测试代码后见代码复审)

  6.接口设计及特色,由于功能都具有各自特色,相互影响性不高,故将每个功能都单独成块,抽离出来。功能都单独返回值,不会在功能中输出。并将有用的参数通过ref传出。

  7.算法设计关键:行数 总字符数都是通过正则表达式的方式进行统计,判断单词出现频率,调用字典的封装好的函数。在判断是否是单词时,将文本读成字符串,字符串再通过正则表达式拆分成字符串数组。


  • 五.代码规范

  1.   不要冗余无用代码,过于冗余的代码可以清理一下,一些已经注释掉的代码可以删除

  2、不变的值,尽量写个常量类。

  3、尽量使用if{}else,不要一直if去判断。

  4、减少循环调用方法;减少IO流的消耗资源。

  5.   当一行代码太长时,将其截断成两行写。

  6.   常用缩进和换行,使代码层次清晰,明了。

  7.   注释的量不应该少于代码量的三分之一。ps(变量统一使用例如/// <param name="s">文件读入路径</param>的注释方式)

  8.   定义变量名字和方法名字的时候尽量使用英文缩写,或者拼音缩写,便于识别。

  9.   对泛型进行循环时,都采用foreach而不使用for。

  11. 对于功能函数写入一个function文件夹中,便于以后功能升级。

  12. 一屏原则:一个方法体的代码幅应该在一屏比较和合理;逻辑复杂的代码可以抽离出方法体。


  • 六.代码复审及部分单元测试

  1.在没有封装功能前,我们各自对对方写的代码进行第一次互审。耗时:20min

  2.在进行封装之后,我们一起针对几个模块功能进行审查,首先针对逻辑上第一个调用的计算行数的功能模块linescount。经过二人的审查,觉得代码没有问题。为了测试正确,此处进行单元测试。

public class linescountTests
    {
        [TestMethod()]
        public void linesTest()
        {
            path.s = @"D:se.txt";
            int x = 0;//第一次测试时输入5,第二次输入0
            Assert.AreEqual(x, linescount.lines());
           // Assert.Fail();
        }
    }

  测试结果如下:此处第一次在记事本中输入两行测试成功,但是在输入0行时测试失败,此处出现大问题,当没有输入文本时,行数没有进行判断,所以出现错误。

  3.审查asccount类(ps:功能为统计有多少字符),经过检查之后,并未发现问题,于是进行单元测试。

 public class asccountTests
    {
        [TestMethod()]
        public void asccountsTest()
        {
            path.s = @"D:se.txt";
            int num = 8;
            Assert.AreEqual(num, asccount.asccounts());
            //Assert.Fail();
        }
    }

  测试结果如下:当定义num=0,文本不输入字符时,出现测试错误。反应过来依旧是没有判断为零情况,所以才会出现错误。

  4.根据逻辑思维,由于想要审查countword类必须要先审查ynword,保证其正确性,故先审查ynword功能模块,经过前两次错误,此次审查小心谨慎,依旧没有发现问题。故接着进行单元测试

 public void ynword1Test()
        {
            int w = 1;
            string[] n = { "word1" };
            string[] newword = { "word1" };
            string[] test = ynword.ynword1(n, ref w);
            Assert.AreEqual(newword[0],test[0] );
        }

  5.使用10个不同测试样例,重复以上操作

  6.代码测试覆盖率,由于这个是社区版,没有测试覆盖率。


  • 七.异常处理

  1.关于输入路径,输出路径异常处理(暂时只想到路径异常)

 try
                {
                    for (int i = 0; i < args.Length; i++)
                    {
                        if (args[i] == "-i") path.s = args[++i];//-i 命令行
                        else if (args[i] == "-n") max = Convert.ToInt32(args[++i]);//-n 命令行
                        else if (args[i] == "-o") path.outputpath = args[++i];//-o 命令行
                        else if (args[i] == "-m")
                        {
                            len = Convert.ToInt32(args[++i]);
                        }
                    }
                }
                catch
                {
                    Console.WriteLine("输入或者输出的路径有误");
                }
//---------------------------------------------------------
 try
                {
                    Console.WriteLine("不输入参数,请手动输入读入文件路径");
                    string s = Console.ReadLine();
                    path.s = s;
                    max = 10;
                    Console.WriteLine("请手动输入输出路径");
                    string s1 = Console.ReadLine();
                    path.outputpath = s1;
                }
                catch
                {
                    Console.WriteLine("输入或者输出的路径有误");
                }

  未处理:

  处理后:


  • 八.改进代码

  1.改进所用时间:45min+。

  2.刚开始结对编程的时候,第一时间想用数组,字符串数组来写的,但是在进行一定的编码后,觉得数组太麻烦,实在是不适合,故最后决定使用字典泛型。

  3.使用字典编程之后,刚开始所有功能模块都写在一起,代码耦合性太差了,后由于要增加功能,并且要进行封装接口,故把所有的功能模块都抽离出来,写入function文件夹

  4.刚开始统计行数和字符总数时,用的另外的方法,后面改进使用正则表达式的方式,提高效率。

  5.见效率分析图及最耗时函数


  • 九.部分代码展示

  1.统计行数展示:ps:需要注意0行的情况

 public static int lines()//统计文件中的行数
        {
            string str = File.ReadAllText(@path.s);
            int nr = Regex.Matches(str, @"
").Count ;
            if (nr != 0)
                nr = nr + 1;
            return nr;
        }

  2.统计总字符个数展示:ps:需要注意换行符是/r/n两个字符。“.”只能统计/r不能统计/n故加上行数。需注意0个字符的情况。

 public static int asccounts()//打开文件并统计字符个数
        {
            string str = File.ReadAllText(@path.s);
            int num = Regex.Matches(str, @".").Count;
            if (linescount.lines() == 0)
                return num + linescount.lines();
            else
                return num + linescount.lines() - 1;
        }

  3.统计单词个数展示:ps:在这里统计单词数需要对每一个进行判断,调用了ynword,此处不展示,用的笨办法if多次判断

public static Dictionary<string, int> Countword()
        {
            
            string str = File.ReadAllText(@path.s);
            Dictionary<string, int> frequencies = new Dictionary<string, int>();
            string[] words = Regex.Split(str, @"W+");
            int k = 0;
            string[] newwords = ynword.ynword1(words,ref k);
            string[] newwords1 = new string[k];
            for (int i = 0; i < k; i++)
            {
                newwords1[i] = newwords[i];
            }
            foreach (string word in newwords1)
            {

                if (frequencies.ContainsKey(word))
                {
                    frequencies[word]++;
                }
                else
                {
                    frequencies[word] = 1;
                }
            }
            return frequencies;
        }

  4.主函数展示: ps:此处由于增加功能,通过查阅资料,知道了命令行输入是存入args中的,故通过这种方式进行操作。

  static void Main(string[] args)
        {
            int temp = 0;
            int max = 0;
            int len = 0;
       //如果命令行有参数执行
if (args.Count() != 0) { for (int i = 0; i < args.Length; i++) { if (args[i] == "-i") path.s = args[++i];//-i 命令行 else if (args[i] == "-n") max = Convert.ToInt32(args[++i]);//-n 命令行 else if (args[i] == "-o") path.outputpath = args[++i];//-o 命令行 else if (args[i] == "-m") { len = Convert.ToInt32(args[++i]); } } if (path.s == null || path.outputpath == null)//路径为空则不存在 { Console.WriteLine("路径不正确,文件不存在"); } }
       //命令行无参数,执行
else { Console.WriteLine("不输入参数,请手动输入读入文件路径"); string s= Console.ReadLine(); path.s = s; max = 10; Console.WriteLine("请手动输入输出路径"); string s1 = Console.ReadLine(); path.outputpath = s1; } Dictionary<string, int> frequencies = function.wordcount.Countword();//调用wordcount中方法统计单词 Dictionary<string, int> dic1Asc = frequencies.OrderBy(o => o.Key).ToDictionary(o => o.Key, p => p.Value);//按照字典序进行排序 int sum = function.wordcount.sum1(dic1Asc);//计算出单词总数量 Console.WriteLine("字符数:"+asccount. asccounts());//计算出字符数量 Console.WriteLine("单词总数:" + sum); Console.WriteLine("行数:"+linescount. lines());//计算出行数 //先按照出现次数排序,如果次数相同按照字典序排序 Dictionary<string, int> dic1Asc1 = frequencies.OrderByDescending(o => o.Value).ThenBy(o => o.Key).ToDictionary(o => o.Key, p => p.Value); foreach (KeyValuePair<string, int> entry in dic1Asc1) { if (temp == max) break; string word = entry.Key; int frequency = entry.Value; temp++; Console.WriteLine("{0}:{1}", word, frequency); } Console.ReadKey(); }

  • 十.总结

刚开始觉得结对编程没有太大的用处,但是通过这次结对编程的经历之后,感觉了结对编程的好处,1+1还真是大于2的。由于刚开始陷入了误区,纠结了许多小问题,通过搭档的提醒,一下子豁然开朗,一个人的主观思维是不完美的,只有通过和他人的合作,才能使得代码趋紧无错,满足所有情况。有了一个实时搭档,在编码时在旁边指出自己的不足和思维漏洞,这样提高了效率,降低了检查时重来的时间。此次收获良多。

原文地址:https://www.cnblogs.com/gentlemanzq/p/10647267.html