结对编程—WordsCount

Part1 Github项目地址

Fork仓库的Github项目地址 git@github.com:JusticeXu/WordCount.git
结对伙伴GIthub地址 npc1158947015

Part2 PSP表格

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

Part3 需求分析

文件统计软件wordCount功能如下:

Core计算模块

​ 1.统计文件的字符数函数

​ 2.统计文件的单词总数函数

​ 3.统计文件的有效行数函数

​ 4.统计文件中单词出现的次数,按字典序输出频率最高的10个单词或者词组函数

个性输出模块

​ 1.自定义输出:能输入用户指定的前n多的单词与其数量

最终实际的软件,除了核心的计算模块以外,还要让这个Core模块和使用它的其他模块之间要有一定的API交流

通过设计GUI界面进行实现

思维导图:

Part 4 代码框架与接口

代码规范

参考资料:Google C++代码规范

本来觉得自己写的代码其实算是足够规范的,但是在比较分析了结对伙伴的代码后,觉得人和人的代码风格还是存在让人诧异的差异的,因此找到了目前最为规范的Google C++代码规范(李开复鼎力推荐)拿来阅读。整体阅读下来大概花了一个小时左右。给我最大的感觉是,像Google,Tencent这样的技术公司,对于整体框架安全性的追求远远超过对技术中淫技巧术的追求,还有就是对可读性的要求也非常高,下面是一些例子:

1.在任何能够使用const的情况下,都用使用const。

2.􏳃􏳌􏵕􏱩􏱪􏱞􏱟􏰣􏰤􏰎􏶻􏴣􏱷􏰾􏰚􏶼􏰿􏱔􏰇􏳼􏶵􏶺􏶽􏶾􏱍􏲨􏲽􏰋􏳯􏱿􏱥􏲎􏳃􏱇􏱇􏱇􏱇􏰲􏰲􏰲􏰲􏰘􏰘􏰘􏰘􏱮􏱮􏱮􏱮􏰱􏰱􏰱􏰱􏰯􏰯􏰯􏰯􏰄􏰄􏰄􏰄 􏵜􏴕􏴌􏴤为避免拷贝构造函数、赋值操作的滥用和编译器自动生成,可声明其为private且无需实现。

3.要尽可能得对自己的代码编写注释。

4.代码整体风格要尽量统一:每一行代码字符数不超过80,不使用非ACSII字符,使用时必须使用UTF-8格式等等

但是也有一些东西讲得和老师传授的东西不一样,所以也不能完全地照搬规范,让自己的代码美观可读,整体风格一致,那就是最好看的代码。

计算模块接口的设计与实现过程。设计包括代码如何组织

我们一共实现了6个类,其中包括核心计算类core()和两个异常处理接口。

1.void generate(),用来实现整体功能的耦合;

2.Core();构造函数;

3.bool IsValid();检查每次录入的文本是否符合要求,若满足则返回true,否则返回false;

4.void Copyresult(int result[CELL], int temp[number]);将结果复制到result中;

5.bool TraceBackSolve(int pos);回溯过程是否有正确输出,若有则输出并返回true,否则返回false;

6.void show(int result[CELL]) 自定义输出用户指定的内容;

具体功能函数实现

字符统计函数

#include <iostream>
#include <string>
using namespace std;

int CharacterCount(string sentence);
int main(){
    string Mystring;
    cout << " Type in the string you would like to use: " << endl;
    getline(cin, Mystring);          // inputs sentence
    cout << " Your string is: " << Mystring << endl;
    cout << " The string has " << CharacterCount(Mystring) << " characters " << endl;
    system("pause");
    return 0;
}
int CharacterCount(string sentence){
    int length = sentence.length();        // gets lenght of sentence and assines it to int length
    int character = 1;
    for (int size = 0; length > size; size++)
    {
        if (sentence[size]>='A'&&sentence[size]<='Z') character++;
        else if (sentence[size]>='a'&&sentence[size]<='a') character++;
        else if (sentence[size]>='0'&&sentence[size]<='9') character++;
        else character++;
    }
    if ( sentence[0] == ' '/*space*/ )
        character--;
    return character;
}

代码自审:这一部分的代码比较简单,一开始就想到了用for循环很快就能解决问题。然后要注意当遇到空文档时,怎么保证输出结果正确,是这个功能中最难想到的一部分。

单词统计函数

#include <iostream>
#include <string>
using namespace std;

int WordCount(string sentence);
int main()
{
    
    string Mystring;
    cout << " Type in the string you would like to use: " << endl;
    getline(cin, Mystring);          // inputs sentence
    cout << " Your string is: " << Mystring << endl;
    cout << " The string has " << WordCount(Mystring) << " words " << endl;
    system("pause");
    return 0;
}
// counts the words
int WordCount(string Sentence)
{
    int length = Sentence.length();        // gets lenght of sentence and assines it to int length
    int words = 1;
    for (int size = 0; length > size; size++)
    {
        
        if (Sentence[size] == ' '/*space*/)
            words++;                   // ADDS TO words if there is a space
    }
    if ( Sentence[0] == ' '/*space*/ )
        words--;
    return words;
}

代码自审:这一部分的功能实现和字符统计类似,无非是当读到空格时,words加1。也算是很简单了。下面的功能也是,当读到\n时,行数加1。代码如下。

有效行数统计函数

#include <iostream>
#include <fstream>
#include <string>
using namespace std;

int CountLines(char *filename)
{
    ifstream ReadFile;
    int n=0;
    string tmp;
    ReadFile.open(filename,ios::in);//ios::in 表示以只读的方式读取文件
    if(ReadFile.fail())//文件打开失败:返回0
    {
        return 0;
    }
    else//文件存在
    {
        while(getline(ReadFile,tmp,'\n'))
        {
            n++;
        }
        ReadFile.close();
        return n;
    }
}
int main()
{
    char filename[]="inFile.txt";
    cout<<"该文件行数为:"<<CountLines(filename)<<endl;
    return 0;
}

词频统计函数

该项目的核心任务分为两部分,一是在文本中识别出不同的单词,二是统计所有单词并将这些单词按照词频进行排序。可以将这个部分分为四个模块:输入模块,状态模块,统计模块,输出模块。

识别文本中的单词,我使用的是一个有限状态机模型。

假如文本中没有连词符 '-' ,那么问题十分简单,只有两个状态,一个是单词内部,一个是单词外部,相互转换的条件则是在单词内部的状态下检测到一个非字母字符,或者在单词外部的状态下检测到一个字母字符。

当文本中出现了连词符,那么情况会复杂一些,不过仍然不会太复杂。我们增加了一个临界状态,当读入一串字母之后突然检测到了一个连词符,则会进入到这个状态。这个状态不会持续,一旦读入下一个字符,就会根据它是字母或者非字母字符,进入到单词内部或单词外部的状态。

使用这一个3状态7过程的状态机模型,可以完美地满足需求。

添加单词:

 int  p_index = Hash(word);    //利用单词计算哈希索引

    WordIndex* pIndex = index[p_index];
    while (pIndex != nullptr) {
        Word *pWord = pIndex->pWord;
        if (!strcmp(word, pWord->word)) {        //在哈希索引对应的几个单词中,找到我们需要找到的单词
            pWord->num++;                //如果找到了,首先把这个单词的词频+1

            //接着根据词频调整单词的位置
            Word *qWord = pWord->previous;
            while (qWord->num < pWord->num) {
                if (qWord == pWordHead)
                    return;

                shiftWord(pWord);

                qWord = pWord->previous;
            }

            //然后再在同一词频下根据单词在字典中的顺序调整位置
            while (strcmp(qWord->word, pWord->word) > 0) {
                if (qWord->num > pWord->num) return;
                shiftWord(pWord);
                qWord = pWord->previous;
            }
            return;
        }
        pIndex = pIndex->next;
    }

遇到首次遇到的单词,添加索引:

// Copyright[2018]<Star>
#include "WordList.h"

WordList::WordList() {
    for (int i = 0; i < MAX_INDEX_NUM; i++) index[i] = nullptr;
	wordNum = 0;
}


WordList::~WordList() {
	Word *temp;
    for (int i = 0; i < MAX_INDEX_NUM; i++) {
		for (Word *p = index[i]; p != nullptr;) {
			temp = p;
			p = p->next;
			delete temp;
		}
    }
}

void WordList::addWord(char word[]) {
    // Add the word to the WordList (or word frequency +1)
    int  p_index = Hash(word);
	if (index[p_index] == nullptr) {
		index[p_index] = new Word(word, 1, index[p_index]);
		return;
	}

	Word* pWord = index[p_index];
    while (pWord->next!= nullptr) {
        if (!strcmp(word, pWord->word)) {
            pWord->num++;
			return;
        }
		pWord = pWord->next;
    }
	if (!strcmp(word, pWord->word)) {
		pWord->num++;
		return;
	}

    pWord->next = new Word(word, 1, nullptr);
	//index[p_index] = pWord;
	wordNum++;
	
}

void WordList::outPut() {
    // 100 words are all output via cout
	if (wordNum <= 0) return;

	if (wordNum > 100) wordNum = 100;
	//开辟一片连续的空间以排序(使用最小堆)
	Word **word = new Word*[wordNum + 2];
	word[0] = word[wordNum + 1] = nullptr;

	int iIndex = 0;
	Word *pWord;
	for (int iWord=1; iIndex < MAX_INDEX_NUM; iIndex++) {
		//将索引中的所有结点放入开辟的空间中准备排序
		pWord = index[iIndex];
		while (pWord != nullptr) {
			word[iWord] = pWord;	//上滤
			upFilter(word, iWord);
			pWord = pWord->next;
			//如果有一百个以上结点,可以进行取舍,因为总共只需要输出100个结点
			if (++iWord > 100) break;
		}
		if (iWord > 100) break;
	}
	for (; iIndex < MAX_INDEX_NUM; iIndex++) {
		if(pWord==nullptr) pWord = index[iIndex];
		while (pWord != nullptr) {
			if (word[1]->equal(pWord) == -1) {
				word[1] = pWord;
				downFilter(word, 50);	//下滤
			}
			pWord = pWord->next;
		}
	}
	heapSort(word, wordNum);	//堆排序

	//输出100个单词及词频
	cout << word[2]->word << ' ' << word[2]->num;
	for (int i = 3; i < wordNum + 2; i++) cout << endl << word[i]->word << ' ' << word[i]->num;

	delete[]word;
}


int WordList::Hash(char* word) {
	int HashVal = 0;

	while (*word != '\0')
		HashVal += *word++;

	return HashVal & 511;
}

void WordList::heapSort(Word * word[], int wordNum)
{
	//排序
	int iWord;
	for (iWord = wordNum; iWord >= 1;) {
		word[iWord + 1] = word[1];
		word[1] = word[iWord];
		word[iWord] = nullptr;
		iWord--;
		downFilter(word, iWord >> 1);
	}
}

void WordList::downFilter(Word * word[], int middleNode)
{
	//下滤
	int iHeap = 1;
	Word *left, *right, *pWord = word[1];
	while (iHeap <= middleNode) {
		left = word[iHeap << 1];
		right = word[(iHeap << 1) + 1];
		if (word[iHeap]->equal(left) == 1) {
			if (word[iHeap]->equal(right) == 1) {
				if (left->equal(right) == 1) {
					word[(iHeap << 1) + 1] = pWord;
					word[iHeap] = right;
					iHeap = (iHeap << 1) + 1;
					continue;
				}
			}
			word[iHeap << 1] = pWord;
			word[iHeap] = left;
			iHeap = iHeap << 1;
			continue;
		}
		if (word[iHeap]->equal(right) == 1) {
			word[(iHeap << 1) + 1] = pWord;
			word[iHeap] = right;
			iHeap = (iHeap << 1) + 1;
			continue;
		}
		break;
	}
}

void WordList::upFilter(Word * word[], int downNode)
{
	//上滤
	int iHeap = downNode;
	while (iHeap > 1) {
		if (word[iHeap]->equal(word[iHeap >> 1]) == -1) {
			Word *temp = word[iHeap];
			word[iHeap] = word[iHeap >> 1];
			word[iHeap >> 1] = temp;
		}
		else return;
		iHeap = iHeap >> 1;
	}
}

输入模块&&程序大体结构:

#include<iostream>
#include<fstream>

#include"WordState.h"
#include"WordList.h"
using namespace std;

void wordCount(char *fileName, WordList &wordList);		//用于词频统计
void outPut(char outFile[], WordList &wordList);		//用于输出结果

int main(int argc, char **argv) {
	WordList wordList;

	wordCount("wcPro.cpp", wordList);
	outPut("result.txt", wordList);

	return 0;
}

void wordCount(char *fileName, WordList &wordList) {
	char word[MAX_WORD_LEN];
	WordState wordState;
	processType process;

	ifstream in;
	in.open(fileName);

	int wordPosition = 0;
	bool flag = true;
	do {
		flag = (word[wordPosition] = in.get()) != EOF;

		process = wordState.stateTransfer(word[wordPosition]);
		switch (process) {
			//根据不同状态做出不同的响应 
		case PROCESS_23:
			//删去最后一个字符以及前一个连词符,单词数++ 
			word[wordPosition - 2] = 0;
			wordList.addWord(word);
			wordPosition = 0;
			break;
		case PROCESS_13:
			//删去最后一个字符,单词数++ 
			word[wordPosition - 1] = 0;
			wordList.addWord(word);
			wordPosition = 0;
			break;
		case PROCESS_33:
			//单词外部-单词外部,wordPosition不变 
			break;
		default:
			wordPosition++;
		}
	} while (flag);

	in.close();
}

void outPut(char outFile[], WordList &wordList) {
	ofstream outf(outFile);
	streambuf *default_buf = cout.rdbuf();
	cout.rdbuf(outf.rdbuf());

	wordList.outPut();

	cout.rdbuf(default_buf);
}

状态模块:

WordState wordState;     //用于记录当前的状态
    processType process;     //这个变量用于记录状态迁移的路径

    char c = 0;
    do {
        c = in.get();
        //这个函数可以根据读入字符进行状态转移,并返回转移过程 
        process = wordState.stateTransfer(c);

        switch (process) {
            //根据不同状态做出不同的响应 
        case PROCESS_23:
            //临界状态->单词外部,删去最后一个连词符,单词数++
            break;
        case PROCESS_13:
            //单词内部->单词外部,单词数++ 
            break;
        case PROCESS_33:
            //单词外部-单词外部,放弃这个非字母字符
            break;
        default:
            //其他情况:储存这个字母字符
        }
    } while (c != EOF);

状态迁移:

// Copyright[2018]<Star>
#include "WordState.h"



WordState::WordState() {
    state = OUTERWORD;
}


processType WordState::stateTransfer(char c) {
    // Pass in a character, calculate the next state based on
    // this character and the current state
    // and return a process indicating how the state was migrated
    processType process = state << 4;

    // state transition code
    if ((c >= 'a') && (c <= 'z')) {
        state = INNERWORD;
    } else if (c == '-') {
            if (state == INNERWORD)
                state = CRITICAL;
            else
                state = OUTERWORD;
    } else {
        state = OUTERWORD;
    }

    process = process | state;
    return process;
}

在把所有的代码编写完成后,按要求我们要进行代码互审,我的同伴是用C语言写的代码,在功能上我的代码基本形成了覆盖,而且类的实现,C语言只能用结构体粗陋代替。因此在代码合并过程中,我这边基本以我自己的代码为主。模块的划分我俩都和题目相似,具体按照需求分析来实现。

结对编程中原则的体现

1.Design by Contract (契约式设计)

在Core模块中,题目附加要求能统计文件夹中最常使用前十个词组的词频,在这个功能中,我们的基本思路是使用一个双向链表来存储所需要的数据,具体来说,每个结点记录了一个单词和这个单词的词频,从链表到链表尾词频依次递减,相同词频字母字母靠前的靠近链表头。每次遇到一个单词,我们先去查找有没有对应于这个单词的结点,如果有,就让这个单词的词频加一,否则在链表的尾部添加一个这个单词的结点。然后不论哪种情况,都要对该单词对应的结点进行调整,使它在同一词频的几个其他单词中,根据首字母大小排序,处于正确的位置。最后从链表头开始,依次遍历一百个结点,就能找到词频最高的一百个单词。

这个方法简单粗暴,但效率低下。两个原因,一个是结点交换位置时必须依次在相邻位置进行交换,这个缺点是对于链表这种数据结构来说无法克服;另一个原因是查找单词时必须从链表头开始遍历。我们考察了很多种数据结构,但是没有一种数据结构能够既快速地处理排序,又能快速地查找结点。因此决定给这个链表,根据单词内容构建一个哈希索引。对于每一个单词(实际上是一个字符数组),我们可以对所有字符进行求和,然后通过模除得到哈希索引,通过哈希索引来查找结点,大大提高了查找的速度。为了提高哈希函数的性能,我们选取128作为模,因为任何一个数模除128都可以写成它和127进行按位与(&)运算,与运算的速度远远高于除法。

对于哈希索引成功找到结点的情况很容易可以实现的,而对于首次遇到的单词,无法根据哈希索引来找到它,那就在链表的末尾增加一个新的结点以记录这个单词,然后为它建立一个索引。

2.Information Hiding

I.用GUI对Core模块的调用(题目要求),体现在讲Core模块封装好成为一个dll库,外部只能通过头文件来查看这个类的功能和成员变量,而不能查看修改源码。

Ⅱ.Core模块中对Core类中所有的成员变量都是private,这些变量对外不可直接用。

Ⅲ.Core模块中对Core类的用来完成非对外功能的函数声明在了private下。

3.Interface Design

在C++中,接口能帮助我们实现多态

4.Loose Coupling (松耦合)

Ⅰ.要实现松耦合,其中一点就是要实现类的单一功能原则。
在我们的工程中,处理来自对外的命令参数这是一个类,完成了对参数的基本检测以及对参数中信息的提取。
完成来自用户的请求这是另一个类,这个类实现了generate函数和solve函数,完成了基本功能。
除此之外,对于不同的异常,我们定义了不同的异常类。
Ⅱ.类之间的相互影响要降低
这一方面我们工程已经不能再简化了,因为功能类和输入处理类的交互点在功能类通过输入处理类提供的接口来获取信息,如果这个交互不存在的话,那么功能类完成功能也就无从谈起。虽然可以将那些选项参数后面的参数定义为公共的变量,但是这些变量还是要经过输入处理类的处理之后才能被功能类所使用。

单元测试

WordList类是我认为最有风险的一个类,它用于实现这个项目最最最主要的功能:词频统计。而为了实现这个功能,它又要调用各种子函数(如哈希函数),从函数调用图来看,它的入度和出度总和是最高的。具体的测试用例设计表格在下面给出。

13个测试用例,从最简单的对一至两个单词进行的功能测试,到最后面对高频词、长单词、超多种类单词进行性能测试,基本把能想到的部分都测了。

其中,第八个超长单词测试,我们使用了一个22个字母组成的单词进行测试,注意到我们代码中为单词设置的最大长度是20,所以这里产生了错误。修改的方法很简单,重新设定单词最大长度即可。

第12和第13个测试用例出错是我没有想到的,这个测试用例无法正常运行,提示说这是一个非法堆指针错误。

一开始我以为是堆空间不够了(因为从aaa-zzz总共有一万多个不同的单词,意味着链表有一万多个结点),我就调大了堆空间,但是没有奏效。我就只好硬着头皮使用断点调试,发现其实链表建立很成功,出错的是在测试执行完毕之后,释放空间的那段代码。为了少些几行代码,在释放链表空间的时候,我采取了简单粗暴的递归方式:在释放一个结点之前,首先调用next结点的析构函数,然后再释放自己。这就是一个递归的过程。

//错误的示例
    ~Word() {
        delete next;
        next = nullptr;
    }

但是,现在我们总共有一万多个链表结点,也就是要递归调用10000多层!!!栈空间肯定不够了。要怪只能怪我偷懒,后来我老老实实改成了用一个for循环释放结点,测试就成功通过了。

//正确的示例
    Word *p = pWordHead;
    while (p != nullptr) {
        pWordHead = p->next;
        delete p;
        p = pWordHead;
    }

成功的单元测试脚本运行结果如下图。一个值得注意的现象是,用例12和用例13分别是顺序多单词测试和逆序多单词测试。在顺序多单词测试中,所有的单词都按照字典顺序进入链表,不需要做多余的排序,因此仅仅用了307毫秒就完成了对一万七千多个单词的词频统计。相比之下,用例13每个单词都按照字典逆序进入链表,这意味着每个单词都要进行一次排序(而且这个排序很慢,是一个节点一个节点地挪动,在前面也解释了,这是链表的硬伤),最后用了24秒才完成统计,是用例12的80倍!如果不使用链表,而是使用向量的话,可以考虑使用先统计后排序的策略,向量排序是很快的,它有封装好的快速排序的算法,可惜我既然已经走上了链表的道路,再推翻重写就有点不太愿意了。

效能分析

为了寻找一份一个在内容上能造成足够压力的txt文档,我在网上找了以《巴黎圣母院》为首的四部英文名著,把它们整合成一份大小超过5M的txt文档。

这个运行时间让我十分惊讶,首先我决定对I/O进行优化。我们的程序读取文件采用的是逐字符读取的方式,边读文件边处理,那么一个文件里面字符越多,我们的I/O次数就越多。于是,我们对此进行了改进。先将文件的所有内容一次性读到内存中,然后就可以关闭文件,从内存中逐字符判断了。

// 改进的I/O代码
ifstream in(fileName, ios::binary);
if (!in) {  // Determine if the file exists
    cout << fileName << "file not exists" << endl;
    return nullptr;
}
filebuf *pbuf = in.rdbuf();

// 调用buffer对象方法获取文件大小  
long size = pbuf->pubseekoff(0, ios::end, ios::in);
if (size == 0) {
    cout << fileName << "file is empty" << endl;
    in.close();
    return nullptr;
}
pbuf->pubseekpos(0, ios::in);

// 分配内存空间  
char *ch = new char[size + 1];
pbuf->sgetn(ch, size);
ch[size] = '\0';
in.close();

在进行了这一优化后,运行时间减少了5秒(是的,只减少了5秒而已)。

Part 5 结对过程

哈哈哈这张照片我被拍成闭着眼睛的很不爽哦,但是在结对的过程中,我根据自己的实际体会总结了有以下几个优点和缺点:

优点:

1.结对编程当中,遇到困难了可以相互鼓励加油,如果其中一个人十分有趣的话,整个编程过程还是十分活跃的

2.当有人观看你写代码的时候,你会比一个人独处写代码的时候更加谨慎仔细

3.遇到问题了两个人可以一同解决,甚至整个团队可以一起来探讨问题,效率还是大大提高了的

缺点:
1.有时候一个的人注意力不够集中(大部分是我……),反而会拖慢整体工作的效率

posted @ 2019-10-16 11:02  rain-coding  阅读(274)  评论(1编辑  收藏  举报