项目 | 内容 |
---|---|
这个作业属于哪个课程 | 软件工程 罗杰 |
这个作业的要求在哪里 | 结对项目 最长单词链 |
我在这个课程的目标是 | 熟悉软件开发整体流程,提升自身能力 |
这个作业在哪个具体方面帮助我实现目标 | 实践教材中内容,体会“结对编程”模式 |
- 本项目的Github链接为:https://github.com/Diralpo/LongestWordChain
开发前的PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
Planning | 计划 | 30 | |
· Estimate | · 估计这个任务需要多少时间 | 30 | |
Development | 开发 | 720 | |
· Analysis | · 需求分析 (包括学习新技术) | 90 | |
· Design Spec | · 生成设计文档 | 60 | |
· Design Review | · 设计复审 (和同事审核设计文档) | 30 | |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | |
· Design | · 具体设计 | 120 | |
· Coding | · 具体编码 | 210 | |
· Code Review | · 代码复审 | 60 | |
· Test | · 测试(自我测试,修改代码,提交修改) | 120 | |
Reporting | 报告 | 90 | |
· Test Report | · 测试报告 | 30 | |
· Size Measurement | · 计算工作量 | 30 | |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | |
合计 | 840 |
- Wikipedia中有关Information Hiding、Interface Design和Loose Coupling定义如下:
- Information Hiding
In computer science, information hiding is the principle of segregation of the design decisions in a computer program that are most likely to change, thus protecting other parts of the program from extensive modification if the design decision is changed. The protection involves providing a stable interface which protects the remainder of the program from the implementation (the details that are most likely to change).
- Interface Design
(No relevant description.)
- 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.
其实这三者的共同目的或诉求就是:通过对接口进行设计,来保证类与类之间的隐私性、独立性,使属性不会过度共享(暴露),使对象的信息不可直接访问,而是要调用方法,从而保证对象的安全性。我们在进行接口设计时,将每个类的信息设为private属性,并提供获取属性和合理的修改属性的方法,而不可以直接访问或修改属性,并提供总的接口,可能内部要调用其他类的方法,但不需要用户或测试人员调用,他们只需要调用总的接口即可,以此来保证安全性。
计算模块接口的设计和实现过程
在这里我们首先设计了一个Solver类用于实现对单词个数最多字符串和单词长度最长字符串的计算,Node类实现存储计算中所需要的各个单词的节点数据。
算法的大致思路如下:
考虑这是一个特殊的有向无环图的最长路径问题(无环情况下),我们可以将所有首先将所有的单词进行拓扑排序,按排序后的顺序可以使用动态规划的算法实现寻找出最长的路径。而对于有环的情况,我们使用了有剪枝策略的DFS搜索算法查找出最长单词链。
独到之处:无环情况下,按单词首字母和尾字母将单词分为26类,减少了单词数量较多时的排序时间。
- 计算模块部分各个实体间关系如下:
计算模块接口部分的性能改进
- 以下为无环,单词数量为10000的运行情况
从图中可以看出占用时间最长的是计算出文本中的最长单词链的函数,在这里我们考虑使用动态规划算法来减少计算模块所花的时间。
- 以下为有环,单词数量为74的运行情况
而对于有环的情况,我们这里就实现了一个基础的DFS算法,并采取了一定的剪枝策略,比如记录已经遍历过的点它的最长路径,然后当遍历到这个点的时候,判断一下当前长度+当前点的最长路径长度,如果小于当前发现的最长路径长度,那就不需要继续遍历下去了,因为当前节点所能到达的最长路径仍然小于当前最长路径。用这些剪枝策略,减少了一些递归遍历,一定程度上优化了算法。
- 有关Design by Contract 和 Code Contract 的一些思考
Design by Contract和Code Contract的定义可在链接中查看。
Design by Contract,指的是契约编程。在面向对象编程中,“接口”是我们着重要关注的部分,面向对象编程就好像打磨好一个个小部件,而这些部件需要通过“接口”连接配合,成为一个整体。然而,当前的部件是否需要对之前的部件的正确性负责呢?或者说,如果之前的部件出现错误,当前部件是否要对这种错误进行补救措施呢?契约编程解决了这种问题。
所谓“契约”,就好像各个部件之间做好的约定一般:如果你传递给我的输入是不符合我的接口规范的,那我有权直接拒绝(结束程序),即方法的输入有一定的约束和期望。这种“契约”所保证的是,确保当前部件起码具有正确的输入,如果输入是不符合接口要求的,只能说明之前的部件存在bug。
契约编程的好处是保证了程序的健壮性,合理地“分摊责任”,使得编程人员在分工时,仅需要对自己的模块按照契约负责,并对输入提出一定的要求限制即可。考虑了契约,才可以保证面向对象的可靠性和可扩展性。
在本次结对编程中,我们的计算模块(即Core)中的接口,对调用时传入参数进行了限制,采用了“契约编程”的模式,如果传入参数不符合接口的要求,说明调用者存在问题,没有遵循契约,因此可以通过assert跳出,撕毁契约。在单元测试中,我们同样也使用assert来进行测试。但最终完成测试后,从程序性能的角度出发,在已经保证各部件接口正确的情况下,我们删除了assert来提高效率。
计算模块单元测试展示
在测试计算模块的之前,我们首先测试了一下命令行参数处理模块是否正确:
TEST_METHOD(TestMethod1)
{
// TODO: 在此输入测试代码
argc = 3;
argv[1] = "-c";
argv[2] = "input.txt";
tag = -1;
headCh = ' ';
endCh = ' ';
isRing = false;
filename = std::string();
getopt(argc, argv, tag, headCh, endCh, isRing, filename);
Assert::AreEqual(1, tag);
Assert::AreEqual(headCh, ' ');
Assert::AreEqual(endCh, ' ');
Assert::IsFalse(isRing);
Assert::AreEqual(0, filename.compare("input.txt"));
}
TEST_METHOD(TestMethod2)
{
// TODO: 在此输入测试代码
argc = 3;
argv[1] = "-w";
argv[2] = "input.txt";
tag = -1;
headCh = ' ';
endCh = ' ';
isRing = false;
filename = std::string();
getopt(argc, argv, tag, headCh, endCh, isRing, filename);
Assert::AreEqual(0, tag);
Assert::AreEqual(headCh, ' ');
Assert::AreEqual(endCh, ' ');
Assert::IsFalse(isRing);
Assert::AreEqual(0, filename.compare("input.txt"));
}
类似的TEST_METHOD我们写了6个,用以测试各项参数读入处理后,tag、headCh、endCh、isRing和filename是否正确。
然后,我们又对计算模块的单词个数最多字符串和单词长度最长字符串这两个方法进行了单元测试:
- 单词个数最多字符串int gen_chain_word();
TEST_METHOD(TestMethod1)
{
// TODO: 在此输入测试代码
int wordIndex = 0;
char **result;
char headCh = ' ';
char endCh = ' ';
bool isRing = false;
char *wordlist[100];
wordlist[0] = new char[20]{ "annzcclv" };
wordlist[1] = new char[20]{ "klebwukqbui" };
wordlist[2] = new char[20]{ "qhqkibinpyew" };
wordlist[3] = new char[20]{ "fkapwouje" };
wordlist[4] = new char[20]{ "mitecsqa" };
wordlist[5] = new char[20]{ "mogowquzdsmto" };
wordlist[6] = new char[20]{ "oxkyhmgemdfpq" };
wordlist[7] = new char[20]{ "hzvreibfb" };
wordlist[8] = new char[20]{ "phgxdlmyrw" };
wordlist[9] = new char[20]{ "kuckfwlghglua" };
wordlist[10] = new char[20]{ "ucqavnwkqseyy" };
wordlist[11] = new char[20]{ "quhxkzqxf" };
wordlist[12] = new char[20]{ "iwoegjfbxhu" };
wordIndex = 13;
result = new char*[wordIndex];
int ans = gen_chain_word(wordlist, wordIndex, result, headCh, endCh, isRing);
Assert::AreEqual(4, ans);
Assert::IsFalse(std::strcmp(result[0], "mogowquzdsmto"));
Assert::IsFalse(std::strcmp(result[1], "oxkyhmgemdfpq"));
Assert::IsFalse(std::strcmp(result[2], "quhxkzqxf"));
Assert::IsFalse(std::strcmp(result[3], "fkapwouje"));
}
TEST_METHOD(TestMethod3)
{
// TODO: 在此输入测试代码
int wordIndex = 0;
char **result;
char headCh = ' ';
char endCh = ' ';
bool isRing = false;
char *wordlist[100];
wordlist[0] = new char[20]{ "annzcclv" };
wordlist[1] = new char[20]{ "klebwukqbui" };
wordlist[2] = new char[20]{ "qhqkibinpyew" };
wordlist[3] = new char[20]{ "fkapwouje" };
wordlist[4] = new char[20]{ "mitecsqa" };
wordlist[5] = new char[20]{ "mogowquzdsmto" };
wordlist[6] = new char[20]{ "oxkyhmgemdfpq" };
wordlist[7] = new char[20]{ "hzvreibfb" };
wordlist[8] = new char[20]{ "phgxdlmyrw" };
wordlist[9] = new char[20]{ "kuckfwlghglua" };
wordlist[10] = new char[20]{ "ucqavnwkqseyy" };
wordlist[11] = new char[20]{ "quhxkzqxf" };
wordlist[12] = new char[20]{ "iwoegjfbxhu" };
wordIndex = 13;
result = new char*[wordIndex];
headCh = 'k';
int ans = gen_chain_word(wordlist, wordIndex, result, headCh, endCh, isRing);
Assert::AreEqual(3, ans);
Assert::IsFalse(std::strcmp(result[0], "klebwukqbui"));
Assert::IsFalse(std::strcmp(result[1], "iwoegjfbxhu"));
Assert::IsFalse(std::strcmp(result[2], "ucqavnwkqseyy"));
}
- 单词长度最长字符串int gen_chain_char();
TEST_METHOD(TestMethod2)
{
// TODO: 在此输入测试代码
int wordIndex = 0;
char **result;
char headCh = ' ';
char endCh = ' ';
bool isRing = false;
char *wordlist[100];
wordlist[0] = new char[20]{ "annzcclv" };
wordlist[1] = new char[20]{ "klebwukqbui" };
wordlist[2] = new char[20]{ "qhqkibinpyew" };
wordlist[3] = new char[20]{ "fkapwouje" };
wordlist[4] = new char[20]{ "mitecsqa" };
wordlist[5] = new char[20]{ "mogowquzdsmto" };
wordlist[6] = new char[20]{ "oxkyhmgemdfpq" };
wordlist[7] = new char[20]{ "hzvreibfb" };
wordlist[8] = new char[20]{ "phgxdlmyrw" };
wordlist[9] = new char[20]{ "kuckfwlghglua" };
wordlist[10] = new char[20]{ "ucqavnwkqseyy" };
wordlist[11] = new char[20]{ "quhxkzqxf" };
wordlist[12] = new char[20]{ "iwoegjfbxhu" };
wordIndex = 13;
result = new char*[wordIndex];
int ans = gen_chain_char(wordlist, wordIndex, result, headCh, endCh, isRing);
Assert::AreEqual(4, ans);
Assert::IsFalse(std::strcmp(result[0], "mogowquzdsmto"));
Assert::IsFalse(std::strcmp(result[1], "oxkyhmgemdfpq"));
Assert::IsFalse(std::strcmp(result[2], "quhxkzqxf"));
Assert::IsFalse(std::strcmp(result[3], "fkapwouje"));
}
TEST_METHOD(TestMethod9)
{
// TODO: 在此输入测试代码
int wordIndex = 0;
char **result;
char headCh = ' ';
char endCh = ' ';
bool isRing = false;
char *wordlist[100];
wordlist[0] = new char[20]{ "rlqokvxuq" };
wordlist[1] = new char[20]{ "vvitmqskdyeap" };
wordlist[2] = new char[20]{ "llkgasgiuzlgx" };
wordlist[3] = new char[20]{ "cxadwktc" };
wordlist[4] = new char[20]{ "yinrlisikdjq" };
wordlist[5] = new char[20]{ "cbrcxzoyigcv" };
wordlist[6] = new char[20]{ "roeuzja" };
wordlist[7] = new char[20]{ "pwwbogbwp" };
wordlist[8] = new char[20]{ "rjztssi" };
wordlist[9] = new char[20]{ "vypbjouumrc" };
wordlist[10] = new char[20]{ "vgorbjxqpap" };
wordlist[11] = new char[20]{ "vrczrlwavkfq" };
wordIndex = 12;
result = new char*[wordIndex];
isRing = true;
int ans = gen_chain_char(wordlist, wordIndex, result, headCh, endCh, isRing);
Assert::AreEqual(5, ans);
Assert::IsFalse(std::strcmp(result[0], "vypbjouumrc"));
Assert::IsFalse(std::strcmp(result[1], "cxadwktc"));
Assert::IsFalse(std::strcmp(result[2], "cbrcxzoyigcv"));
Assert::IsFalse(std::strcmp(result[3], "vvitmqskdyeap"));
Assert::IsFalse(std::strcmp(result[4], "pwwbogbwp"));
}
与测试命令行参数处理模块类似,我们对gen_chain_word和gen_chain_char进行了headCh、endCh和isRing这几个参数的各种情况的测试,充分考虑了各个分支,达到了比较好的测试效果。最终我们的总体覆盖率如下:
计算模块异常处理说明
WordRingsException:当默认情况下输入出现单词环时抛出
IllegalInterfaceParaException:当调用接口时传入不合理的参数时(如len <= 0,head或tail既不为' '也不是字母时)
IllegalParametersException:命令行参数出现错误时
FileNotExitException:输入的文本不存在时
命令行模块设计
命令行处理我们采用最简朴的字符串比较方法,依次对参数进行分析:
对于总体我们处理了不含有“-w”和“-c”的异常(因为此时我们不知道该如何选择最长字符串),同时在每一个分支中,我们也都进行了异常处理,如既有“-w”又有“-c”的,或是“-h”和“-t”后面没有字母或是不是字母字符等等情况。
命令行处理模块将从控制台读入的信息处理好后,将信息储存在tag,headCh,endCh,isRing和filename中,供getFileInput()方法调用。
命令行模块与计算模块的对接
命令行模块引入了定义接口的头文件,直接可以通过需求,调用定义的两个接口中的一个,实现计算最长单词链的过程。
- 结对编程的美妙过程
总的来说本次结对编程的体验很不错,开始很顺利,过程中虽然遇到了或大或小的麻烦,但我们共同克服,最后写出的项目质量也还不错。
刚开始的时候,我们还不是很适应“驾驶员”、“领航员”的模式,基本就是商量着写代码,但效率也还不错,设计的时候两个人相互补充,也避免了未考虑到某些方面的情况发生。很快我们就将“有向无环图”的情况完成,并进行了测试,性能还不错。不过之后的“有向有环图”的设计就陷入了僵局,我们除了暴力深度搜索想不出什么比较好的方式(事实是我们最后也还是暴力搜索),一度两人都在摸鱼。不过后来,我们决定先将其完成,然后再进行剪枝来优化(毕竟测试数据集也不是很大),很快也将“有向有环图”的情况完成。最后的单元测试和错误处理模块,我们也很顺利地完成。
总体来说这次结对编程让我们体验到了这种新的编程模式,能够和队友在交流碰撞,产生新的想法和点子,互相纠正、互相补充,这种经历十分可贵,于我个人而言也是一次很大的提升。不过,这都是建立在我和我的队友是平日关系密切、很熟悉的前提下,如果在公司中采取这种方式,我就不是很肯定效率会如何了。
最后,奉上结对编程留念图:
有关结对编程的一些感悟
邹欣大大的有关“结对编程与两人合作”的文章镇楼!!
因为觉得这种编程方式是一个很有趣的事情,所以在写本次结对编程作业的时候也是严格按照两个人一起编码的方式进行的。体验下来还是有一些感想的:
- 结对编程的优点:
- 一个人编码,一个人思考设计+复查,可以高效推进且保证较高的准确率
- 两个人不断交换身份,也可以使得编程不那么疲惫,保持高效工作状态
- 结对编程的缺点:
- 由于本次我的队友就是我的室友,十分熟悉所以不需要磨合,但现实情况下,可能磨合起来还是比较麻烦,甚至可能出现两个人就是不合适的情况
- 评价队友
- 优点:
- 代码能力极强
- 思路清晰活跃,是一名优秀的“领航员”
- 为人比较细心,考虑周全
- 缺点:
- 编码手速较慢,有时候看得我好难受...
- 优点:
- 自我评价
- 优点:
- 执行力强,编码速度快,虽然有可能存在失误,但有优秀的队友兜着我
- 比较细心,在做复查的时候可以帮助队友发现一些小的手误
- 与人为善,和队友十分开心嘻嘻嘻
- 缺点:
- 头脑远不如队友清晰,思路有时候跟不上
- 优点:
PSP表格回填
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 20 |
· Estimate | · 估计这个任务需要多少时间 | 30 | 20 |
Development | 开发 | 720 | 890 |
· Analysis | · 需求分析 (包括学习新技术) | 90 | 120 |
· Design Spec | · 生成设计文档 | 60 | 30 |
· Design Review | · 设计复审 (和同事审核设计文档) | 30 | 20 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | 40 |
· Design | · 具体设计 | 120 | 120 |
· Coding | · 具体编码 | 210 | 360 |
· Code Review | · 代码复审 | 60 | 50 |
· Test | · 测试(自我测试,修改代码,提交修改) | 120 | 150 |
Reporting | 报告 | 90 | 100 |
· Test Report | · 测试报告 | 30 | 40 |
· Size Measurement | · 计算工作量 | 30 | 20 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 40 |
合计 | 840 | 1010 |