软件工程基础-个人项目2014-代码复审
进行了个人项目的实践之后,我与晏旭瑞同学开展了结对编程项目,现已基本完成。根据老师的要求,现由结对编程的两名人员互相复审对方的个人项目的代码。
本次我复审晏旭瑞第一次个人项目的代码。
个人项目要求: http://www.cnblogs.com/jiel/p/3978727.html
晏旭瑞的代码整体来说看起来非常简洁,没有冗余的部分,多处利用库函数提高了程序的效率,并实现了一定程度上的代码重用;
函数和类的命名有含义而且简明,让人看到名字就知道模块的基本功能。整个程序整体采用过程式设计方法,而过程中调用的方法都是类之中的方法,
将打开文件(遍历文件夹)、迭代读入单词、分析单词并进行计数以及最后的结果排序都分配给不同的方法来完成,而这些不同的方法都是不同的类的成员方法,也就是说总体过程化的程序设计,以及任务的分割以及类与对象实体的构建,也包含了面向对象的设计思想。类的定义都在头文件中,在cpp文件中才对具体的方法进行书写,看起来结构层次分明,类似于接口的实现,一定程度上体现了面向对象编程的封装性,信息隐藏以及抽象原则。整个程序逻辑清楚,分支不多但是却覆盖了应有的情况。
1 #include"findFile.h" 2 int findFile::dfsFolder(string folderPath,vector<string> &fileArray) 3 { 4 _finddata_t FileInfo; 5 string strfind = folderPath + "\\*"; 6 //搜索与指定的文件名称匹配的第一个实例,若成功则返回第一个实例的句柄,否则返回-1L 7 long Handle = _findfirst(strfind.c_str(), &FileInfo); 8 9 if (Handle == -1L) 10 { 11 return -1;// "wrong folder path" 12 } 13 do{ 14 //子目录 15 if (FileInfo.attrib&_A_SUBDIR) 16 { 17 //这个语句很重要 18 if( (strcmp(FileInfo.name,".") != 0 ) &&(strcmp(FileInfo.name,"..") != 0)) 19 { 20 string newPath = folderPath+"\\" + FileInfo.name; 21 dfsFolder(newPath,fileArray); 22 } 23 } 24 //普通文件 25 else 26 { 27 string fileName(FileInfo.name); 28 auto pos=fileName.rfind('.'); 29 if(pos==string::npos)//无后缀的文件,调试了好久,坑啊! 30 continue; 31 string fileSuffix=fileName.substr(pos);//获取文件后缀 32 33 if(regex_match(fileSuffix,suf))//是否匹配给定的文件类型 34 { 35 fileArray.push_back(folderPath+"\\"+FileInfo.name); 36 } 37 } 38 }while (_findnext(Handle, &FileInfo) == 0); 39 _findclose(Handle); 40 return 0; 41 } 42 int findFile::getAllFiles(vector<string> &fileArray)//通过应用返回函数值 43 { 44 return dfsFolder(dir,fileArray); 45 }
同时,程序中添加了许多注释,使程序的可读性提高。迭代器的使用使得单词的读取更加规范化模式化,并且省去了很多冗余的代码。
1 #include"word_iterator.h" 2 3 bool isAlpha(char c) 4 { 5 if(c>='A'&&c<='Z'||c>='a'&&c<='z') 6 return true; 7 else return false; 8 } 9 bool isAlnum(char c) 10 { 11 if(isAlpha(c)||c>='0'&&c<='9') 12 return true; 13 return false; 14 } 15 16 void word_iterator::getWord() 17 { 18 int l=line.size();//无符号转成int 19 while(1) 20 { 21 while(s<(l-2))//找到第一个字母 22 { 23 if(isAlpha(line[s])) 24 if((s>0&&!isAlnum(line[s-1]))||s==0) 25 break; 26 s++; 27 } 28 29 30 if(s>=(l-2)) 31 { 32 position=-1; 33 break; 34 } 35 position=s; 36 t=s+1; 37 if(isAlpha(line[t])&&isAlpha(line[++t]))//前三个是字母,找到一个单词 38 { 39 while(++t<line.size()&&isAlnum(line[t])); 40 length=t-s; 41 str=line.substr(s,length); 42 s=t; 43 break; 44 } 45 else s++;//下次从s开始查找 46 47 } 48 }
自定义迭代器也使得程序多了很多自由性,便于满足具体需求。迭代器暴露给其他类的仅仅是一些接口,比如读取下一个单词,判断有没有下一个单词。至于具体如何读取下一个单词,下一个单词的选取有什么要求,这些都是其他类不需要考虑的。也就是说,迭代器内获取下一个单词的逻辑可以自定义,可以根据实际需求而改变,同时由于接口不变,其他类的代码不需改变,只需继续通过迭代器暴露的接口来进行操作即可,实现了封装性原则以及模块之间一定程度上的独立性,降低耦合度。
尽管晏旭瑞的代码简明扼要,有很多优点,设计模式也很好,但是仍然有一些细微的问题,下面主要讨论代码中的三个小问题:
1.在程序中,部分变量的命名可以改善,使其更有意义,可读性更好,就像类和函数的命名一样,让阅读者一看就知道是做什么用的。
1 for(word_iterator it(s);it.position!=-1;++it) 2 { 3 //如果上次处理了一个单词(或词组),这次减掉一个单词 4 if(i==m) 5 { 6 i--; 7 if(m==1) 8 target=""; 9 else 10 target=target.substr(target.find(' ')+1); 11 } 12 13 //第一遍循环从这里开始,分三种情况 14 if(m==1||i==0) 15 { 16 target=it.str; 17 i++; 18 } 19 else 20 { 21 if(s[p]==' '&&it.position-p==1) 22 { 23 target+=" "+it.str; 24 i++; 25 } 26 else 27 { 28 target=it.str; 29 i=1; 30 } 31 } 32 p=it.position+it.length;//三种情况都需要执行这句话 33 34 //判断是否进行处理 35 if(i!=m) 36 continue; 37 38 39 add(target); 40 }
在该语句块中,变量i和m的可读性不是太好,让人看了不明白是表示的什么量,所以对于有i和m参与的语句,语句的含义最初看来也不是太明了。
1 #ifndef _wordCount_H 2 #define _wordCount_H 3 #include<map> 4 #include<string> 5 #include<regex> 6 #include"struct.h" 7 using namespace std; 8 9 10 class wordCount 11 { 12 private: 13 int m; 14 regex word;//定义单词的正则表达式 15 map<string,int,cmp> wordList; 16 map<string,string> map2; 17 void add(string target); 18 public: 19 wordCount(string wordDef,int a):m(a){regex r(wordDef);word=r;} 20 void statistic(string s); 21 int query(string s);//供wordGroupCount查询使用 22 void outPut(string fileName); 23 }; 24 #endif
在类的定义中找到了m,但是仍然不太明白m的作用,这个变量名称的可读性依然不是太好。对于这些计数变量,或者是表示统计数据等的变量,虽然看起来有些麻烦,但是这些变量的命名依然建议具有含义,这样编写代码者不会混淆其含义,阅读代码者也能够很快明白其含义。
2.程序中某些部分产生局部变量过多,产生了一些程序垃圾,而这些其实是有可能避免的。
1 int findFile::dfsFolder(string folderPath,vector<string> &fileArray) 2 { 3 _finddata_t FileInfo; 4 string strfind = folderPath + "\\*"; 5 //搜索与指定的文件名称匹配的第一个实例,若成功则返回第一个实例的句柄,否则返回-1L 6 long Handle = _findfirst(strfind.c_str(), &FileInfo); 7 8 if (Handle == -1L) 9 { 10 return -1;// "wrong folder path" 11 } 12 do{ 13 //子目录 14 if (FileInfo.attrib&_A_SUBDIR) 15 { 16 //这个语句很重要 17 if( (strcmp(FileInfo.name,".") != 0 ) &&(strcmp(FileInfo.name,"..") != 0)) 18 { 19 string newPath = folderPath+"\\" + FileInfo.name; 20 dfsFolder(newPath,fileArray); 21 } 22 } 23 //普通文件 24 else 25 { 26 string fileName(FileInfo.name); 27 auto pos=fileName.rfind('.'); 28 if(pos==string::npos)//无后缀的文件,调试了好久,坑啊! 29 continue; 30 string fileSuffix=fileName.substr(pos);//获取文件后缀 31 32 if(regex_match(fileSuffix,suf))//是否匹配给定的文件类型 33 { 34 fileArray.push_back(folderPath+"\\"+FileInfo.name); 35 } 36 } 37 }while (_findnext(Handle, &FileInfo) == 0); 38 _findclose(Handle); 39 return 0; 40 }
在该函数中,有一个do-while循环体,在每一次循环中,
string newPath = folderPath+"\\" + FileInfo.name;
和string fileName(FileInfo.name);
都会创建两个新的引用,来指向已有字符串或者是新产生的字符串。当然本来这些字符串是需要被提取并保存以便后用的,但是大可不必在每次循环的时候都创建新的引用类型变量,而是可以在类中定义私有变量来表示相同涵义,在函数中利用这些私有变量来进行字符串地址的引用与保存,这样就不需要在每次循环趟中都创建新的引用变量了。
3.在最后的排序过程中,本程序现将哈希表中的元素导出至vector中,然后调用库函数中的sort方法,并传入自定义的比较器进行比较。比较的方面,调用固有的sort方法并自定义比较器是比较明智并且快速简洁的做法,但是并不一定需要将哈希表中的元素导出至vector中然后再排序。也就是说,可以直接对哈希表进行排序。这样可以省去导出哈希表项所耗费的资源。
1 bool comp(const PAIR &x, const PAIR &y) 2 { 3 if(x.second>y.second) 4 return true; 5 if(x.second==y.second) 6 return x.first<y.first; 7 else return false; 8 } 9 void wordCount::outPut(string fileName)//按照出现次数降序排序 10 { 11 vector<PAIR> pair_vec; 12 for (auto map_iter = wordList.begin(); map_iter!= wordList.end(); ++map_iter)//将map换到vector中 13 { 14 //cout<<setw(20)<<left<<map_iter->first<<map_iter->second<<'\n'<<right; 15 pair_vec.push_back(make_pair(map_iter->first, map_iter->second)); 16 } 17 sort(pair_vec.begin(), pair_vec.end(), comp); //按照comp的规则排序 18 19 int n=pair_vec.size(); 20 if(m!=1&&M<n) 21 n=M; 22 ofstream out(fileName);//在当前目录创建晏旭瑞.txt,输出统计结果 23 for (int i=0;i<n;i++) 24 { 25 out<<setw(40)<<left 26 <<"<"+pair_vec[i].first+">:"<<pair_vec[i].second<<'\n' 27 <<right; 28 } 29 }
将键值对存储在哈希表中,有利于单词的查询(键-值对随机查询,时间复杂度为0(1))。不过哈希表的排序并不容易,因为哈希表不能够进行下标索引,因此一般的排序方法对哈希表都不奏效。在我的程序中我也是使用了哈希表来进行单词的存储以及检索,不过我用的不是map类,而是dictionary类。dictionary类本身实现了IEnumerable接口,因此可以利用System.Linq命名空间中的Enumerable类的方法进行排序,方法主要有OrderBy,OrderByDescending,ThenBy,ThenByDescending等。这些排序方法可以指定排序的对象以及排序的比较器,在哈希表的排序中非常好用,时间复杂度与sort相同,都与快速排序的复杂度一样,为O(nlogn)。
以上是我对晏旭瑞的个人项目代码的复审报告。