A君第四次结对编程作业:第四次作业
作业要求地址
Github项目地址
结对伙伴的作业地址
PSP表格
PSP2.1 |
Personal Software Process Stages |
预估耗时(分钟) |
实际耗时(分钟) |
Planning |
· 计划 |
30 |
60 |
· Estimate |
· 估计这个任务需要多少时间 |
1200 |
1440 |
Development |
· 开发 |
600 |
720 |
· Analysis |
· 需求分析 (包括学习新技术) |
300 |
240 |
· Design Spec |
· 生成设计文档 |
180 |
240 |
· Design Review |
· 设计复审 (和同事审核设计文档) |
80 |
60 |
· Coding Standard |
· 代码规范 (为目前的开发制定合适的规范) |
60 |
40 |
· Design |
· 具体设计 |
60 |
60 |
· Coding |
· 具体编码 |
600 |
480 |
· Code Review |
· 代码复审 |
60 |
30 |
· Test |
· 测试(自我测试,修改代码,提交修改) |
120 |
150 |
Reporting |
. 报告 |
60 |
80 |
· Test Report |
· 测试报告 |
40 |
50 |
· Size Measurement |
· 计算工作量 |
30 |
30 |
· Postmortem & Process Improvement Plan |
· 事后总结, 并提出过程改进计划 |
40 |
70 |
|
合计 |
3460 |
3750 |
part 1 克隆项目
1.首先输入要克隆的网址:https://github.com/Cherish599/WordCount
2.点击Fork拷贝到自己的同名仓库中:
Part 2代码规范
结对编程代码规范
- 缩进:采用 4个空格缩进,不用 Tab键的理由是Tab键在不同的情况下会显示不同的长度。4个空格的距离从可读性来说正好。
- 括号:在复杂的条件表达式当中,用括号清楚地表示出逻辑关系。
- 如果出现{}的存在,采取下面的形式:
if ( condition)
{
DoSomething();
}
else
{
DoSomethingElse();
} - 分行:不要把多条语句放在一行上,不要把不同的变量定义在一行上。
- 命名规范:命名要有一定的可理解性。变量的第一个字母通常为大写字母,Pascal——所有单词的第一个字母都大写;
- 程序注释:将语句注释放在单独的行上,变量注释放在同一行。用句号结束注释文本。每个类的开头做出对这个类的说明。对于函数的注释,将函数的功能注释写在函数的开头的上面一行。对于一些重要的功能语句换行在下方注释。
- 对于大多数异常处理,请使用try-catch语句。
- 将每个功能分离开来,分类书写。
- 析构函数:把所有的清理工作都放在析构函数中。如果有些资源在析构函数之前就释放了,记住要重置这些成员为0或NULL。
- 不要定义无用的变量。
Part 3 计算模块接口的设计与与实现过程
基础功能分析和分工
基础功能分析
首先需要在命令行窗口输入wordCount.exe input.txt来进行对文件的一系列操作,明确此次的项目主要是对文件进行操作,所以我们首先就决定找有关于C#对文件进行操作的资料。决定采用StreamWrite类来处理文本文件。再明确项目功能,由题目可知,主要功能为:
1.统计文件的字符数(只需统计ASCII码不考虑汉字)
明确各个名词的定义:空格、水平制表符、换行符、运算符、英文字母、分隔符。
2.统计文件中单词总数
明确单词的定义:至少以四个字母开头,跟上字母数字符号(我们这里认为可以不跟直接四个字母也算是单词)单词以分隔符分开,不区分大小写(即无论是大写开头的单词还是小写开头的单词只要是一样的单词就不加以区分)
3.统计文件的有效行数(非空白的行)
4.统计文件中各单词出现的次数,最终输出频率最高的10个。
5.频率相同的单词,按照字典序输出。
6.输出到文件txt。
基础功能分工
按照功能需求我们进行了相应的分工:
申颖:负责1、2、3功能
张毅铭负责:4、5功能
合作:读取文件,输出到文件txt
经过讨论,我们的功能流程图如下:
代码组织
经过两个人的商量大致确定了如下的代码组织:
计算功能都在一个计算类WordUti中,在这个类当中含有统计字符数函数CountChar、统计单词数函数:CreateWords、统计文件有效行数函数CountLine、单词进行词频排序并且输出前十个词频的单词的函数(按照字典序输出)SortKey。另外还要将单词和对应的数目存入泛型数组keyValues的函数Dictionary方便进行单词的词频排序。我们整体设计了一个名为WordUti的工具类,里面有五个静态的方法,countline方法与其他方法是解耦的 ,其他的方法中使用了共同的变量,有一定的耦合关系。但为了使用方便 在相应方法里调用的对应的前置方法,避免产生空值异常。统一文件名采用ning.txt。
总体功能图:
关键函数流程图:
自己代码功能负责模块
功能一:统计文件字符数
思路:获取一个文件的字符数可以通过文件.Length来获取。于是首先要获取的是整个文件,然后再采用.Length获得字符数即可。由于开始我们用的是一个可以操作文件的StreamWrite类,所以首先通过StreamWrite类来读入文件然后通过ReadToEnd方法来得到整个文件后求得字符数。这样比较简便。
代码:
//统计字符数。
public static int CountChar(string path)
{
//读入。
StreamReader sr = new StreamReader(path);
//统计文件的字符数。
txt = sr.ReadToEnd();
return txt.Length;
}
功能二:统计文件单词总数
思路:首先明确了单词的定义,要统计单词数首先应该将文本按照分隔符的标准分隔开,于是又去看了分隔符的定义,要通过一个要求把文件给分开于是决定采用正则表达式Regex来实现该功能。具体步骤:首先还是先读取整个文件(这里比较懒直接调用了CountChar方法),pattern1为分隔正则表达式,pattern2为判断单词正则表达式。再按照正则表达式来匹配经过pattern1分隔的文档将单词存入单词集合方便进行单词排序操作。
流程图:
代码:
public static int CreateWords(string path)
{
CountChar(path);
regex = new Regex(pattern1);
string[] output = regex.Split(txt);
regex = new Regex(pattern2);
foreach(string s in output)
{
if (regex.IsMatch(s))
{
//转换为小写字母。
words.Add(s.ToLower());
}
}
return words.Count;
}
功能三:统计文件有效行数
通过sr.ReadLine()!=null函数读取每一行是否为空行,如果不为空行的话就用line对有效行数计数。
代码:
//统计行数。
public static int CountLine(string path)
{
int line = 0;//行数。
string str = "";
StreamReader sr = new StreamReader(path);
while ((str = sr.ReadLine()) != null)
{
line++;
}
return line;
}
代码复审过程
代码自审
根据前面的代码规范自己进行了一个审查:
1.缩进:没有问题。
2.括号:本程序当中含有括号的最多只有一个嵌套并没有什么复杂的逻辑关系都能够清楚正确地表示。
3.出现{}符号:经过和小伙伴的商量,由于第一版本都是计算核心模块所以都写在一个类当中,将每个功能函数都写在这个类当中以便于合并,包括一些判断循环语句的{}符号的使用都正确。
4.分行:出现问题:有多余的空行:
最后删掉了很多多余的空格进行了修改。
5.命名规范:程序中很多变量都是小写开头的,于是将一些小写开头的变量修改成大写开头:
6.程序注释:满足要求。
7.异常处理:本程序暂时没涉及异常处理程序。
8.每个功能分开书写满足要求。
9.没有定义无意义的变量。
代码互审
1.缩进:缩进不符合要求的有一行:
修改后:
2.括号:括号都正确表示了。
3.出现{}符号:满足要求。
4.分行:出现问题:有多余的空行:
将其余多余的空行都删除了。
5.命名规范:程序中很多变量都是小写开头的,于是将一些小写开头的变量修改成大写开头:
6.程序注释:满足要求。
7.异常处理:满足要求。
8.每个功能分开书写满足要求。
9.没有定义无意义的变量。
总结:
审查模块名:class WordUti
出现问题:关键变量开头没大写,有多余空行
审查模块名:SortKey
出现问题:有多余空格,缩进错误。
代码合并
1.由于刚开始两个人商量的时候并没有采取控制台输入的方式所以在合并的时候就修改了原本的代码来进行控制台输入。
2.对文件异常的情况进行了处理。判断文档的路径是否异常,如果异常控制台打印出文件路径异常字样
3.将频数排名前十的单词和频数集合从原始的函数中分离了出来。
最终主函数为:
string path = "ning.txt";
if (args.Length != 0)
{
path = args[0];
}
else
{
// StreamWriter 专门用来处理文本文件的类,可向文件写入字符串
StreamWriter streamWriter = new StreamWriter(@"E:\demo.txt");
//将characters words lines 写入到文件ning.txt当中
//Dictionary 也是一个泛型集合,得到对应的单词以及单词的数量
Dictionary keyValues = WordUti.createDic(path);
//封存结果的一个集合
Dictionary result = new Dictionary();
//统计字符数
result.Add("characters: " ,WordUti.CountChar(path));
//统计单词数
result.Add("words: ", WordUti.CreateWords(path));
//统计行数
result.Add("lines: ", WordUti.CountLine(path));
//得到频数前十的单词及频数封装到result集合里
WordUti.SortKey(keyValues, result);
foreach (var s in result)
{
streamWriter.WriteLine(s.Key + ":" + s.Value);
Console.WriteLine(s.Key + ":" + s.Value);
}
streamWriter.Flush();
streamWriter.Close();
}
4.进行第一次代码Github签入:
进入到文件夹右键打卡Git bash
输入下面的命令将第一次项目传至仓库:
根据题目要求本题采用的是命令行的输入方式,根据命令行输入的内容读取文件内容进行一系列的计算操作然后将结果输出到另外一个文件,在这里我们读取的文件为ning.txt,输出文件为demo.txt。打开cmd,这里如果Wordcount.exe存放的路径为E:\WordCount\WordCount\201731062306\WordCount\WordCount\bin\Debug,则首先在命令行窗口输入E:进入到E盘,然后输入cd E:\WordCount\WordCount\201731062306\WordCount\WordCount\bin\Debug。再输入运行程序WordCount以及读取的文件名ning.txt如图:
接口封装
不同的代码解决不同层面的问题,代码的种类不同,混杂在一起对于后期的维护扩展很不友好,所以它们的组织结构就需要精心的整理和优化。因此把上面的功能都独立出来都写成类的形式,因此最后决定将项目划分为如下几个类:
ContChar类用于统计文件字符数,CountLine类用于统计文件的有效行数,CreatDic类用于得到对应的单词以及单词的数量,CreatWords类用于统计单词数,SortKey类用于将单词按照字典序排序后输出。
各个类之间的关系图:
主函数改为:
string path = "ning.txt";
if(args.Length!=0)
{
path = args[0];
}
// StreamWriter 专门用来处理文本文件的类,可向文件写入字符串
StreamWriter streamWriter = new StreamWriter("demo.txt");
//将characters words lines 写入到文件ning.txt当中
//统计字符数
string txt = CountChar.countChar(path);
Console.WriteLine("characters: " + txt.Length);
streamWriter.WriteLine("characters: " + txt.Length);
//统计单词数
List words = CreateWords.Createwords(txt);
Console.WriteLine("words: " + words.Count);
streamWriter.WriteLine("words: " + words.Count);
//统计行数
Console.WriteLine("lines: " + CountLine.ountLine(path));
streamWriter.WriteLine("lines: " + CountLine.ountLine(path));
//Dictionary 也是一个泛型集合,得到对应的单词以及单词的数量
Dictionary keyValues = CreateDic.createDic(words);
//将单词按照字典序排序过后输出
SortKey.Sortkey(keyValues, streamWriter);
Console.ReadKey();
最后进行第二次代码Github签入:
和第一次的嵌入操作一样第二次的commit的命名为Secondversion。
增加新功能
- 词组统计:能统计文件夹中指定长度的词组的词频
- 自定义输出:能输出用户指定的前n多的单词与其数量
(1)-i 参数设定读入的文件路径
(2)-m 参数设定统计的词组长度
(3)-n参数设定输出的单词数量
(4)-o 参数设定生成文件的存储路径
(5)多参数的混合使用
功能分析
首先输入改为了可以通过-m -n -i -o 来确定相应的功能,之所以首先要分别区分-m -n -i -o 再调用相应的类来实现相应的功能。要实现(1)-(4)的功能只需要在现有的类的基础上再增添一些限制即可,除了增加原来的限制之外还多了一个功能那就是统计指定长度的词组的词频,
功能分工
申颖:词组统计、(5)
张毅铭:(2)-(4)
自己代码功能负责模块
1.明确词组指的是单词的组合,其实就是要想办法将得到的单词集合通过数量的限制输出对应的词组以及数目。还是利用了之前求单词集合的方式,首先通过一个循环嵌套来得到对应长度的单词的组合,存入字符串moment当中,然后再进行消重的操作,最后将结果存入集合rt当中。
流程图:
class WordGroup
{
//词组统计
public static void wordgroup(int m,List words,Dictionary rt)
{
//存入最开始得到的词组
List now = new List();
//用于消去重复的词组
Dictionary group = new Dictionary();
//用来得到最后的词组
string Result = null;
//得到总的单词数目
int L = words.Count();
//得到词组
for (int i = 0; i <= L - m; i++)
{
string moment = null;
for (int j = i; j < i + m; j++)
{
moment += words[j] + " ";
}
//将词组加入到字符串列表now中
if (moment != null)
now.Add(moment);
}
//将字符串列表加入到集合
foreach (string s in now)
{
if (group.ContainsKey(s))
{
group[s]++;
}
else
{
group.Add(s, 1);
}
}
//添加结果集合
foreach (KeyValuePair matching in group)
{
rt.Add(matching.Key, matching.Value);
}
}
}
2.多参数的混合使用也就是要从命令行窗口输入的命令进行提取,通过查阅资料知道命令行输入的字符串为主函数里的args字符串数组里面的成员,所以通过遍历args字符串数组来寻找是否含有对应的命令参数。如果找到了-i 那么-i后面那个数组单元就是读取文件名,如果找到了-o那么-o后面那个数组单元就是存入文件名,如果找到了-n那么-n后面的那个数组单元就是要输出的频数前n的单词,如果找到了-m那么-m后面的那个数组单元就是要统计的词组的长度。核心代码:
for (int i = 0; i < args.Length; i++)
{
//命令选项 -i 代表输入的文档参数
if (args[i] == "-i")
{
Inpath = args[i + 1];
}
if (args[i] == "-n")
{
count = int.Parse(args[i + 1]);
}
if (args[i] == "-o")
{
Outpath = args[i + 1];
}
if (args[i] == "-m")
{
group= int.Parse(args[i + 1]);
}
}
四大原则体现:
Design By Contract:
调用上述方法时对文件路径有一定要求,我设置了以.txt格式的文本来作为数据源,那如果不是文本的话,则会提示错物信息
Information Hiding:
那每个模块的功能是独立的,即使我设置为了静态方法,但是在CountWords的时候使用了正则表达式,具体的正则表达式对于其他模块来讲,应该时封闭的,因此我设置了关于正则表达式的数据时私有的。
Interface Design:
按照功能模块定义为接口或者接口功能的名字,任意接口之间没有什么直接的联系,如直接调用等聚合方式,但是在参数部分,需要使用其他接口调用的结果。
Loose Coupling :
各模块之间可独立运行,在第一版中我们时使用了一个工具类包括了这些方法,同样的使用了相同的静态数据来存储结果,在和同伴讨论后,发现模块之间联系的过于紧密,因此将具体方法分类,模块独立使用。增强他的松耦合。
Part 4 计算模块接口部分的性能改进(申颖)
性能改进:
第一个版本将所有的功能都写在一个类当中:
发现CPU的消耗很大,所以将其解耦,也就是将每个功能分为一个一个的类得到性能分析图:
进行效能分析:
为了方便进行效能分析直接规定输入输出txt文件以及对应的命令参数的值。
1.VS当中内置有效能工具,名叫性能探查器。
2.为了查看程序的执行效率,选择测试CPU使用率->开始:
3.等待分析一段时间后,停止收集,最后就会产生一份效能分析报告:
但是在进行分析报告的时候出现了问题如图:
于是百度查找了一下解决方式:点击调试->选项->右边勾上启用源服务器支持->左边点符号把微软符号服务器勾选上->确定:
4.再次根据1、2步骤进行性能测试,为了方便性能测试将主函数的命令行输入的参数直接固定,因此将参数判定改为:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Text.RegularExpressions;
namespace WordCount
{
class Program
{
static void Main(string[] args)
{
//设置读取路径变量
String Inpath ="ming.txt";
String Outpath = "out.txt";
//默认讲标志量设为-1
int count = -1;
//设置读取词组的个数
int group = -1;
//遍历命令参数
count = 5;
group= 5;
//路径读取失败
if (Inpath == null)
{
Console.Write("未输入文档路径");
Environment.Exit(404);
}
else if (!Inpath.EndsWith(".txt"))
{
Console.Write("文档格式不正确,请使用txt文档");
Environment.Exit(404);
}
else
{
try
{
StreamReader streamReader = new StreamReader(Inpath);
//封存结果的一个集合
Dictionary result = new Dictionary();
//得到文本字符串
string txt = CountChar.countChar(streamReader);
//统计字符数
result.Add("characters: ",txt.Length);
//得到符合要求的英语单词集合
List words = CountWords.CreateWords(streamReader, txt);
//统计单词数
result.Add("words: ", words.Count);
//统计行数
result.Add("lines: ", CountLine.countLine(streamReader));
//Dictionary 也是一个泛型集合,得到对应的单词以及单词的数量
Dictionary keyValues = GetDic.createDic(streamReader, words);
//得到频数前n的单词及频数封装到result集合里
GetRes.SortKey(keyValues, result, count);
if (group != -1)
{
WordGroup.wordgroup(group, words, result);
}
Print.print(result, Outpath);
}catch(Exception e)
{
Console.Write("文件读取异常,未找到该资源,请检查输入");
Environment.Exit(404);
}
}
}
}
}
5.得到性能详细报告:
程序中消耗最大的函数:
其中主要是由于调用了一个函数Regex.LsMatch调用了堆栈底部所以对CPU的消耗比较大。
Part 5 计算模块部分单元测试展示(张毅铭)
测试函数-CountChar(统计字符类)
思路:读取一个简单的文档,使用断言判断字符串长度是否等于自己的预期
测试函数-CountLine(统计行数类)
思路:读取一个简单的文档,使用断言判断其行数是否等于自己预期
测试函数CreatWords(统计单词类)
思路:同样是读取一个文件 得到起文本字符串 ,使用测试函数生成单词词组,判断单词的个数。
测试函数-GetDic(生成单词)
思路:本想使用读取文件,但是速度较慢,为验证功能,我摸拟了一个单词列表,借由此单词列表测试被测试函数,可以测试出对应单词的频数符合自己的预期
测试函数-GetRes(统计词频前n并添加到结果集合)
思路:读取文档生成了一个dictionary的单词集合,借由集合测试被测试函数,会输出到前10的单词 至控制台中,结果如下
测试函数-WordGroup(词组输出)
思路,读取文本文件,得到其单词数量list,测试被测试函数,输出以三个为一组的单词,将测试结果输出至控制台
社区版没有代码覆盖率测试
Part 6 计算模块部分异常处理说明
异常处理机制,忽略因代码问题所产生的异常,我在这里主要考虑的就是有关文件读取方面的相关异常,那我设计了几个相关的操作,其中文件格式和未输入文件是通过if else 在主方法前进行判断的,数据的来源则是最重要的,那有了正确的输入文件和格式,但是路径失败会导致读取不到相应的文件,因此我使用了try catch 语句进行异常的捕获,具体如下:
场景则是防止在用户输入错误的文档格式或者忘记输入文档路径进行的处理机制,否则会引发程序中的异常。
这个则是在streamReader读取文档时因检索不到本地资源导致抛出异常,异常会被catch捕获,输出提示信息并中断程序。
场景则是用户输入了正常的格式及文档路径,但是在本地没有这个资源文件而导致的异常。
Part 7 描述结对的过程
Part 8 附加功能
功能分析:
1.支持两种导入单词文本的方式:①导入单词文本文件,②直接在界面上输入单词并提交。
2.提供可供用户交互的按钮和,实现-i -m -n -o 这四个参数的功能,对于异常情况需要给予用户提示。
3.将结果直接输出到界面上,并提供“导出”按钮,将结果保存到用户指定的位置。
思路:
导入单词文本的方式有两种,所以我们决定采用从本地导入一个文件然后显示其路径并且将改文件的内容展示在选择文件内容中。然后自定义内容框就是直接在界面上输入单词并提交。然后读取这两个richTextBox中的内容作为处理的字符串。也就是实现了-i 的功能,然后通过引入另外一个项目的using字符集来调用计算项目的类实现-m和-n功能。最后在textBox4中输入导出的文件名,列如E:\1.txt就将结果导入到了E盘中的文本文件1.txt中。
任务分工:
申颖:用户界面绘制、实现文件的导入与选择文件内容的输出、实现导出结果到相应文件。
张毅铭:实现自定义输入文件内容、-m -n功能、将结果输出到输出结果框。
自己任务部分:
2.功能一:实现文件的导入与选择文件内容的输出
点击读取按钮(button2_Click)进行代码书写,通过 OpenFileDialog类来传入文件再得到文本文件,得到文件路径然后显示到richTextBox1中:
OpenFileDialog xjOpenFileDialog = new OpenFileDialog();
xjOpenFileDialog.Filter = "文本文件|*.txt";
if (xjOpenFileDialog.ShowDialog() == DialogResult.OK)
{
string xjFilePath = xjOpenFileDialog.FileName;
this.textBox1.Text = xjFilePath;//显示文件路径
StreamReader sr = new StreamReader(xjFilePath, Encoding.Default);
this.richTextBox1.Text = sr.ReadToEnd();//显示内容
}
3.功能二:实现导出结果到相应文件
功能二和功能一有异曲同工之妙首先读取选择文件的路径然后存到一个字符串当中,再append到StreamWriter swobj中然后通过WriteLine方法输入到相应的文件中。
string lujing = this.textBox4.Text;
System.IO.StreamWriter swobj = System.IO.File.AppendText(lujing);
swobj.WriteLine(this.output.Text);
swobj.Flush();
swobj.Close();
实现效果展示:
3.以1中的情况为例,如果导出文件为E:\ou.txt结果如图:
Part 9 提交代码
打开你 Fork 后的项目主页,点击New pull request
Part 10 个人总结
这次的结对真正体会到了什么是真正的结对,在代码上面要和小伙伴先商量如何进行项目内容和进度的安排,还要进行合理的分工,在这期间也遇到了不少的麻烦,比如两个人的时间不合导致很难安排时间,无法很明确地制定时间计划,也浪费了不少时间。还了解到了每个人的擅长的方面是不一样的,所以首先伙伴之间要说出自己擅长的部分,都不会的就需要相互鼓励一起完成,这个过程不得不说是相当痛苦的,不过也是收获最多的。通过制定共同的代码规范来相互查看对方的代码找出错误,在这个过程当中能够加深对项目的理解,读懂别人的代码的思维也是需要不少的时间。我认为结对的过程不一定是最节省时间的方式但是一定是一个很提高能力的过程。再说说代码方面,对于C#对文本的操作有了一个质的理解,还有对一些C#窗体小操作的认识。虽然再一些方面还不够完善,比如在窗体的时候异常处理还没有考虑全面。自己这次在整合两个人的代码的时候也学到了不少小伙伴的一些思路,可以说是收获颇多了。在合作的时候效率还是挺高的,这次的作业我认为是1+1>2的。但是这次的项目除了提高了自己的代码能力以外,我想更多的是安排的能力和分析能力。希望以后还有机会结对编程,很愉快的一次体验。