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, @"\r").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的。由于刚开始陷入了误区,纠结了许多小问题,通过搭档的提醒,一下子豁然开朗,一个人的主观思维是不完美的,只有通过和他人的合作,才能使得代码趋紧无错,满足所有情况。有了一个实时搭档,在编码时在旁边指出自己的不足和思维漏洞,这样提高了效率,降低了检查时重来的时间。此次收获良多。