1、Github项目地址
2、PSP表格(程序各个模块在开发上预计耗费的时间和实际耗费的时间)
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 30 |
· Estimate | · 估计这个任务需要多少时间 | 60 | 30 |
Development | 开发 | 1680 | 2320 |
· Analysis | · 需求分析 (包括学习新技术) | 120 | 100 |
· Design Spec | · 生成设计文档 | 30 | 30 |
· Design Review | · 设计复审 (和同事审核设计文档) | 30 | 20 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | 30 |
· Design | · 具体设计 | 60 | 120 |
· Coding | · 具体编码 | 1200 | 1680 |
· Code Review | · 代码复审 | 30 | 40 |
· Test | · 测试(自我测试,修改代码,提交修改) | 180 | 300 |
Reporting | 报告 | 390 | 510 |
· Test Report | · 测试报告 | 180 | 200 |
· Size Measurement | · 计算工作量 | 30 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 180 | 300 |
合计 | 2130 | 2860 |
3、关于Information Hiding, Interface Design, Loose Coupling等方法的接口设计
Information Hiding
- 其实题目给出的三种方法或者说是原则都是和封装有关的,在我的理解里封装是技术,而比如信息隐藏实则是目的。虽然我们在实际的代码中可以说是没有用到面向对象的格式来编写,但其实用到的仍然还是面向对象的思想。比如说计算核心Core的内部功能是不展示出来的,通过作业规定的两个接口来使用户和代码交换各自需要的东西。类似的在用户输入模块、读取文件模块等我们都设计了与计算模块Core类似的接口,每个不同独立的功能都封装成了函数,保证信息的隐藏功能。
Interface Design
- 我查阅了一些资料,找到的大多数的接口设计貌似都是很陌生而且与现在所学格格不入。但还是从其中学习到了一些无论设计什么接口都应该最起码遵循的原则,比如命名必须规范优雅,保证接口要做的事情是比较单一的事情(单一性),良好的可扩展性和可移植性,而在实际编程中我们也是这样做的。
Loose Coupling
- 这个词刚刚看上去我甚至都不知道是什么意思,在维基百科上才大概了解了一些:
In computing and systems design a loosely coupled system is one in which each of its components has, or makes use of, little or no knowledge of the definitions of other separate components. Subareas include the coupling of classes, interfaces, data, and services.[1] Loose coupling is the opposite of tight coupling.
——引用自维基百科
- 但是仍然觉的对这个概念很不清晰,继续往下看并按照中文翻译查了一些资料,大概知道了其还是指一个组件与另一个组件具有直接联系的程度。仍然还是封装与非封装的意思。在这里举几个我们程序中的封装接口的例子:
void InputHandler(int argc, char* argv[], bool &enable_loop, int &word_or_char, char &head, char &tail, string &Filename);
void ReadFile(string FileName, vector<string> &words);
void DFS_Length(Graph G, int v, vector<string> words, char tail);
4、计算模块接口的设计与实现过程
4.1 问题分析
以前做过类似的题,输入的所有单词能否全部首尾相连形成链。由于单词首尾相连有多种连接方式,故基本的数据结构为图。
建图有两种方式,一种是以单词为节点,如果单词间正好可以首尾连接,则添加一条边,该边即为连接的字母。另一种建图方式是以字母为节点,以单词为边,出现一个单词,即把首字母节点向尾字母节点添加一条边,边的值即为该单词。
对于这道题目而言,由于单词需要输出,加之对第二种建图方式掌握并不熟练,因此选择的是第一种建图方式。
模型确立后,问题就可以简化成“求图中的最长链”,即最长路径问题,显然问题是多源最长路径问题。
4.2 数据结构与算法
数据结构为图,存储方式为邻接矩阵,理由是能更契合floyd算法。
对于无环情况,由于为多源最长路径问题,联想到最短路径问题,可以确定为floyd算法。
而对于有环情况,由于出现了正值环,floyd算法不再适用。在找不到更有解决方法的情况下,只能适用DFS深度优先搜索求解。
4.3 模块组织
ReadFile: 读取文件的模块,将文件中的单词提取进入容器vector中。
Graph: 图的定义。
InputHandler:处理输入的模块,读取命令行并处理参数。
FindLongestWordList: 计算模块,内含计算接口。计算出单词中的最长链。
4.4 算法关键
首先需要判断有无环,对于没有-r参数的输入来说,如果有环需要报错。这里也是用到DFS的染色算法。每个点有三种状态:未遍历过,遍历过,当前序列正在遍历。如果一次DFS中一个点与正在遍历中的点相连了,说明DFS回到了之前的点,即图中有环。
另一问题是由于无环情况最多可有10000个单词,而floyd算法时间复杂度为O(n^3),暴力的计算显然是不行的。考虑到对于无环的情况,有如下特性:对于单词element和elephant,由于无环,这两个单词最多只有一个会出现在链中。(否则会出现element, t..., ..., ....e, elephant / element,这样一定是有环的),而如果要满足字母最多,显然这时候需要选择elephant加入链中。因此我们可以对于所有首尾字母相同的单词,保留首尾字母组合中,最长的一个单词。这样的操作之后,最多的单词数目为351,即使是时间复杂度O(n^3)的算法也能很快得出结果。另外可以计算得,最长链的长度最大为51。
5、UML图
6、计算模块接口部分的性能改进
- 首先是无环情况,其性能最大阻碍是10000个单词大样本情况下,floyd算法时间复杂度过高导致的。但是在4.4有介绍过,我们可以通过无环单词链的特性来削减样本数量,削减后单词数量少,即使时间复杂度高也能很快跑出结果。因此性能方面上没有太大问题。
- 其次是有环情况,由于DFS算法仍属于暴力递归搜索,并不算很好的算法,其性能也着实较差。但是我们也想不到更好的解决算法,所以并没有改进。
7、关于Design by Contract, Code Contract的优缺点以及结对作业中的体现
- 契约式编程对于软件工程是一个极大的理论改革,对于C/S模式造成了极大的影响和冲击。对于C/S模式,我们看待两个模块的地位是不平等的,我们往往要求server非常强大,可以处理一切可能的异常,而对client不闻不问,造成了client代码的低劣。而在DbC中,使用者和被调用者地位平等,双方必须彼此履行义务,才可以行驶权利。调用者必须提供正确的参数,被调用者必须保证正确的结果和调用者要求的不变性。双方都有必须履行的义务,也有使用的权利,这样就保证了双方代码的质量,提高了软件工程的效率和质量。缺点是对于程序语言有一定的要求,契约式编程需要一种机制来验证契约的成立与否。而断言显然是最好的选择,但是并不是所有的程序语言都有断言机制。那么强行使用语言进行模仿就势必造成代码的冗余和不可读性的提高。比如.NET4.0以前就没有assert的概念,在4.0后全面引入了契约式编程的概念,使得契约式编程的可用性大大提高了。此外,契约式编程并未被标准化,因此项目之间的定义和修改各不一样,给代码造成很大混乱,这正是很少在实际中看到契约式编程应用的原因。在我们的代码中,对于模块间使用了契约的思想,保证双方地位的平等。调用者的传入参数必须是正确的,否则责任不在被调用者,而在传入者。
——优缺点引用自维基百科
8、计算模块部分单元测试展示
- 在单元测试部分我们对程序中除输出部分外(由于输出部分只是一个简单的输出到文件)其他所以部分或函数进行的全面的单元测试,如图共25个,单元测试的全部代码也已上传至Github。下面我将拿出部分单元测试代码具体介绍,并在这部分的最后附上单元测试的测试服概率截图。
TEST_METHOD(TestMethod3)
{
// TODO: normal_test3
char* words[101] = { "element", "heaven", "table", "teach", "talk"};
char* answer[101];
for (int i = 0; i < 101; i++)
{
answer[i] = (char*)malloc(sizeof(char) * 601);
}
int l = gen_chain_word(words, 5, answer, 0, 0, true);
Assert::AreEqual(l, 4);
Assert::AreEqual("table", answer[0]);
Assert::AreEqual("element", answer[1]);
Assert::AreEqual("teach", answer[2]);
Assert::AreEqual("heaven", answer[3]);
for (int i = 0; i < 101; i++)
{
free(answer[i]);
}
}
- 上面的单元测试代码是测试计算核心中的gen_chain_word接口函数,由于单元测试需要我手动加入words,所以这里的单元测试数据比较小,就是构造一个有环有链的单词文本,并且是在输入‘-r’的情况下,从而得到一个正确的单词链。
TEST_METHOD(TestMethod6)
{
// TODO: normal_test6
char* words[101] = { "apple", "banane", "cane", "a", "papa", "erase" };
char* answer[101];
for (int i = 0; i < 101; i++)
{
answer[i] = (char*)malloc(sizeof(char) * 601);
}
int l = gen_chain_char(words, 6, answer, 'a', 'e', false);
Assert::AreEqual(l, 3);
Assert::AreEqual("a", answer[0]);
Assert::AreEqual("apple", answer[1]);
Assert::AreEqual("erase", answer[2]);
for (int i = 0; i < 101; i++)
{
free(answer[i]);
}
}
- 上面的单元测试代码是测试计算核心中的gen_chain_char接口函数,这里构造了一个没有环的文本数据,而且其最多单词链和最长单词链不同,并固定了首尾字母。
TEST_METHOD(TestMethod2)
{
// 正确_2
int argc = 6;
char* argv[101] = { "Wordlist.exe", "-r", "-h", "a", "-c", "test_1.txt" };
char head;
char tail;
bool enable_loop;
int word_or_char = 0;
string Filename;
InputHandler(argc, argv, enable_loop, word_or_char, head, tail, Filename);
Assert::AreEqual(enable_loop, true);
Assert::AreEqual(word_or_char, 2);
Assert::AreEqual(head, 'a');
Assert::AreEqual(tail, char(0));
Assert::AreEqual(Filename, (string)"test_1.txt");
}
- 上面的单元测试代码是测试接收命令行输入函数InputHandler,这里没有什么太多好说的, 就是把命令行输入参数的所有正确组合全部测试一遍即可(参数输入顺序可以改变)。
单元测试覆盖率截图(由于C++没有找到直接测试单元测试覆盖率的插件,这里用的方法是将单元测试代码移至main函数中用OpenCppCoverage插件得到的覆盖率,部分异常测试没有放进来,所以覆盖率没有达到100%)
9、计算模块部分异常处理说明
- 在异常处理模块我们一共自定义了8种类型的异常,接下来我将会结合每种异常的单元测试说明每种异常的设计目标以及错误对应的场景(单元测试的构造方法就是保证此函数可以捕捉到异常且捕捉的是与当前错误相对应的异常,否则单元测试不通过)。
1. 错误的参数组合(其中包括出现多个相同命令比如‘-r’、‘-r’,‘-h’和‘-c’同时出现,‘-h’和‘-c’都没有,即不指定求何种单词链)
TEST_METHOD(TestMethod3)
{
// 错误_1
int argc = 5;
char* argv[101] = { "Wordlist.exe", "-r", "-r", "-c", "test_1.txt" };
char head;
char tail;
bool enable_loop;
int word_or_char = 0;
string Filename;
try {
InputHandler(argc, argv, enable_loop, word_or_char, head, tail, Filename);
Assert::IsTrue(false);
}
catch (myexception1& e) {
Assert::IsTrue(true);
}
catch (...) {
Assert::IsTrue(false);
}
}
这个单元测试是‘-r’出现了两次,错误的参数组合。
2. 指定单词链首尾不合法(比如‘-h’、‘1’或者‘-t’、‘ag’)
TEST_METHOD(TestMethod7)
{
// 错误_5
int argc = 6;
char* argv[101] = { "Wordlist.exe", "-r", "-h", "1", "-c", "test_1.txt" };
char head;
char tail;
bool enable_loop;
int word_or_char = 0;
string Filename;
try {
InputHandler(argc, argv, enable_loop, word_or_char, head, tail, Filename);
Assert::IsTrue(false);
}
catch (myexception2& e) {
Assert::IsTrue(true);
}
catch (...) {
Assert::IsTrue(false);
}
}
这个单元测试是‘-h’指定首字母为‘1’,明显是错误的。
3. 输入的参数不是指定的那几个参数,不符合规定(如输入‘-b’)
TEST_METHOD(TestMethod9)
{
// 错误_7
int argc = 5;
char* argv[101] = { "Wordlist.exe", "-b", "-r", "-c", "test_1.txt" };
char head;
char tail;
bool enable_loop;
int word_or_char = 0;
string Filename;
try {
InputHandler(argc, argv, enable_loop, word_or_char, head, tail, Filename);
Assert::IsTrue(false);
}
catch (myexception3& e) {
Assert::IsTrue(true);
}
catch (...) {
Assert::IsTrue(false);
}
}
这个单元测试是输入参数‘-b’显然是不符合规定的。
4. 文件不存在的情况
TEST_METHOD(TestMethod2)
{
// 错误
vector <string> words;
try {
ReadFile("normal_test3.txt", words); // 不存在的文件
Assert::IsTrue(false);
}
catch (myexception4& e) {
Assert::IsTrue(true);
}
catch (...) {
Assert::IsTrue(false);
}
}
这个单元测试是测试了一个在此路径下不存在的文件。
5. 读取的文件中有单词长度超过600
TEST_METHOD(TestMethod2)
{
// 错误
vector <string> words;
try {
ReadFile("long_word_test.txt", words);
Assert::IsTrue(false);
}
catch (myexception4& e) {
Assert::IsTrue(true);
}
catch (...) {
Assert::IsTrue(false);
}
}
这个单元测试是文件中存在长度超过600的单词。
6. 读取的文件中无环的超过10000个单词,有环的超过100个单词
TEST_METHOD(TestMethod2)
{
// 错误
vector <string> words;
try {
ReadFile("more_words_test.txt", words);
Assert::IsTrue(false);
}
catch (myexception4& e) {
Assert::IsTrue(true);
}
catch (...) {
Assert::IsTrue(false);
}
}
这个单元测试是所测试文件中单词数超过了10000。
7. 读取文件中有单词环且参数没有输入‘-r’
TEST_METHOD(TestMethod10)
{
// wrong_test2
char* words[101] = { "alement", "oeaven", "tabla", "teaco", "talk" };
char* answer[101];
for (int i = 0; i < 101; i++)
{
answer[i] = (char*)malloc(sizeof(char) * 601);
}
try {
int l = gen_chain_char(words, 5, answer, 0, 'n', false);
Assert::IsTrue(false);
}
catch (myexception7& e) {
Assert::IsTrue(true);
}
catch (...) {
Assert::IsTrue(false);
}
}
这个单元测试是传入单词可以形成环,且用户没有传入参数‘-r’。
8. 读取文件中无法形成最少两个单词的单词链
TEST_METHOD(TestMethod11)
{
// wrong_test3
char* words[101] = { "alement", "oeaven", "tabla", "teaco", "talk" };
char* answer[101];
for (int i = 0; i < 101; i++)
{
answer[i] = (char*)malloc(sizeof(char) * 601);
}
try {
int l = gen_chain_word(words, 5, answer, 'b', 'n', true);
Assert::IsTrue(false);
}
catch (myexception8& e) {
Assert::IsTrue(true);
}
catch (...) {
Assert::IsTrue(false);
}
}
这个单元测试是规定了首尾字母后,单词链中没有用户所要求的单词链。
10、界面模块的详细设计过程(GUI)
- 在界面模块这方面我们没有实现GUI,而是完成了最基本的命令行模块。其实如果是命令行模块的话就非常简单了,根据在命令行输入的内容及长度存入char* argv[]以及int argc中,然后再传给InputHandler函数中对传入的参数进行分析处理,主要是识别错误的参数输入(第9部分已经详细介绍)以及将正确的参数组合中的信息存下来,比如说head和tail是否有限定,单词文本是否允许有环以及要求的单词链是要单词最多还是单词总长度最长。由于实现很简单,这里不必再贴上代码赘述。
11、界面模块与计算模块的对接(GUI)
- 命令行模块与两个计算核心模块的对接其实也很简单。我们从命令行读入的各类参数如果是正确无误的,那么我们可以相对应地确定传入两个计算模块的head、tail、enable_loop以及执行哪个计算模块的判断变量。即确定规范的单词链首字母尾字母,如果没有规定则传入0,是否允许有环的变量。如果不允许,则需要判断传入单词文本是否可以形成环,如果形成环则报告异常。下面是简单是一张命令行输入截图:
12、结对之过程
- 由于与队友为舍友,结对时相对简单很多,只需要到对铺和队友一起结对编程就行了。我们的水平差不多, 编程能力和数据结构算法的掌握都不算太好。初期时我们主要是一起讨论算法,如何实现基本的功能,数据结构应该用什么。敲定一个算法之后就开始分头找资料,最后再汇总资料,交给他来敲代码或者我来在一些地方进行修改。编写时经常会遇到一些意料不到的bug,最后必须一起搜索如何解决。但是两个人在一起编写代码时,有一个人来随时审视代码,有不懂的地方或者不对劲的地方另一人都可以随时提出来。因此虽然结对编程效率没有提高, 但是效果会比两个单人编写来的更好。
总的来说这次题目难度还是没有那么爆炸,所以我们之间的合作也比较愉快。至于提意见的艺术是根本用不上的,毕竟是舍友也不会产生矛盾。下面是我们在初期时讨论算法的图片:
13、结对编程的优缺点及评价
结对编程优缺点
- 下面是一些结对编程的优点:程序员互相帮助,互相教对方,可以得到能力上的互补。可以让编程环境有效地贯彻Design。增强代码和产品质量,并有效的减少BUG。降低学习成本。一边编程,一边共享知识和经验,有效地在实践中进行学习。在编程中,相互讨论,可能更快更有效地解决问题。当然,结队编程也会有一些不好的地方:对于有不同习惯的编程人员,可以在起工作会产生麻烦,甚至矛盾。有时候,程序员们会对一个问题各执己见(代码风格可能会是引发技术人员口水战的地方),争吵不休,反而产生重大内耗。两个人在一起工作可能会出现工作精力不能集中的情况。程序员可能会交谈一些与工作无关的事情,反而分散注意力,导致效率比单人更为低下。
评价(队友陈致远)
-
优点:
- 认真负责,轮流编程时的任务完成准时而且质量很高
- 有探索精神,有遇到无论软件问题还是算法问题一定要探个究竟
- 考虑全面,程序无论正确情况方面还是报错方面都考虑的很细致
-
缺点:
- 我们项目经验都比较少,有些地方都不是很得心应手