结对项目--计算最长单词链
项目 | 内容 |
---|---|
这个作业属于哪个课程 | 2019春季计算机学院软件工程(罗杰) |
这个作业的要求在哪里 | 作业要求 |
我在这个课程的目标是 | 完成结对编程 |
这个作业在哪个具体方面帮助我实现目标 | 为团队合作打基础 |
Github地址
https://github.com/zackertypical/WordChain
PSP预估时间
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) |
---|---|---|
Planning | 计划 | 60 |
·Estimate | 估计这个任务需要多少时间 | 60 |
Development | 开发 | 57*60 |
·Analysis | ·需求分析 (包括学习新技术) | 8*60 |
·Design Spec | · 生成设计文档 | 4*60 |
·Design Review | · 设计复审 (和同事审核设计文档) | 2*60 |
·Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 1*60 |
·Design | · 具体设计 | 5*60 |
·Coding | · 具体编码 | 24*60 |
·Code Review | · 代码复审 | 8*60 |
·Test | · 测试(自我测试,修改代码,提交修改) | 5*60 |
Reporting | 报告 | 5*60 |
·Test Report | · 测试报告 | 2*60 |
·Size Measurement | · 计算工作量 | 60 |
·ostmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 2*60 |
合计 | 63*60 |
接口设计原则
Information Hiding
-
为了实现良好的封装,需要从两个方面考虑:
1、将对象的属性和实现细节隐藏起来,不允许外部直接访问。 让使用者只能通过事先预定好的方法来访问数据,从而可以在该方法里加入控制逻辑,限制对属性的不合理访问。
2、把方法暴漏出去,让方法来操作或访问这些属性。
我们在设计DFS图的类时,所有数据成员都无法被外部直接访问,例如图的权重数组,邻接表数组,通过外部的接口对图中节点进行权重的修改,进行边的插入的操作,实现了信息的隐藏。
在计算最长路径的时候,也仅提供了输出的接口,无法对图的私有成员进行操作,所有计算过程在类里私有函数完成,外部仅访问得到结果的接口。
Interface Design
将需求抽象成一个个独立的接口/抽象类,然后被继承或委托/组成的形式来实现或拓展新的具体或更加强大完善的抽象,通过层层封装、继承,最后就会实现运行时多态的特性,从而提高代码的灵活性。
良好的接口需要有单一职责性和可拓展性。在本次项目中,我们利用继承与多态的思想,建立了一个图的基类,其他类都实现这个基类提供的方法,例如修改结点权值,插入边等操作,对于结果的读取,实现findAns()函数,但不同的类该接口的实现方法不一样。
比如有首尾字母约束的类,实现同样的findAns()接口,无论是怎样的参数组合,最终都要通过这个接口来访问结果。这样降低的模块之间的耦合度,提高了代码复用,提高了模块的单一性。
同时Core计算模块也实现了对外的接口,需要传入word还有一些参数,返回result结果,降低了耦合性。
Loose Coupling
软件工程中对象之间的耦合度就是对象之间的依赖性。对象之间的耦合越高,维护成本越高。因此对象的设计应使类和构件之间的耦合最小。
对于不同的参数类型,我们构建了继承于基类的子类,例如实现约束首字母的类,需要单独有数组来存单词是否符合首字母约束,而约束尾字母也可以用该类进行计算,只需要在结果输出以后把数组反转即可。
对于首尾字母都有约束的情况也单独有一个子类来完成功能,继承自只有首字母约束的类。
在图内实现dfs,判断是否有环,最终输出结果都是单独实现的函数,每个方法完成一个功能,降低了函数之间的耦合性。
Core的接口设计和实现过程
1、代码组织:
-
类的设计
1、接口
需要对传入的参数进行解析,实例化Core核心计算类和DFS图,经计算后返回结果
2、core类
通过core接口对图进行操作。关键函数如下:
- void insertChain(char * words[], int len);
进行单词链的去重操作和排序。 - void setHeadTail(DFSHeadTailGraph &graph, char tail);
对图的数组进行操作,指定头尾节点的约束。 - void insert_weighedEdge(DFSGraph &graph);
对图的边进行插入操作,赋予边权重。 - void getresult(char *result[], vector
& ans);
通过图内部函数的计算得到结果。
3、图类
对建好的图进行dfs计算出最终的结果链,由core打印结果,本身不存储单词信息,只存储节点的编号。
关键函数如下:
** 私有函数 **
void findAnsChain();
调用dfs进行最长链的寻找,把结果保存在私有变量vectorans中。
int dfs(int index);
对有环图的dfs。
int dpDfs(int index);
对无环图的dfs。** 公有函数 **
void insertEdge(int i, int j);
对图进行边的插入。
void changeVecWeigh(int i, int weight);
改变图的节点权重。
const vector& getAnsChain();
外部访问得到最长链的节点编号数组。
bool hasCircle();
外部进行访问,可以得到图是否有环的信息。4、Exception类
对各种异常进行处理,包括参数异常,对图的操作异常等。
5、命令行输入类
对输入的单词文本进行处理,实现读入文件,写文件等操作。
- void insertChain(char * words[], int len);
2、算法关键
有环的情况要比没有环的复杂度高很多,所以算法第一步要判断是否有环,如果有环,进行普通的深度优先遍历的方法。没有环的话开一个dp数组进行记忆化搜索,性能会提高很多。
对于有首尾字母约束的情况下,没有单独在一个类里面实现,而是通过类的继承来降低模块的耦合性。
UML图
计算模块接口部分的性能改进
性能分析结果:
在小文本数据处理时,并没有遇到很大的性能瓶颈,于是我们利用了大文本进行测试。发现在处理图节点之间连边的函数性能消耗很大,根据VS的性能分析工具,可以看到是在string的处理上进行索引的部分消耗较大,我们使用的是iterator去访问string的头和尾字母。于是最后改成了用下标访问,速度有所提升。
在单词链有隐环的情况下,dfs耗费的时间的确很大,并没有找到改进的方法。
改进之后结果:
Design by Contract, Code Contract
一般认为在模块中检查错误状况并且上报,是模块本身的义务。而在契约体制下,对于契约的检查并非义务,实际上是在履行权利。一个义务,一个权利,差别极大。例如:
if (dest == NULL) { ... }
这就是义务,其要点在于,一旦条件不满足,我方(义务方)必须负责以合适手法处理这尴尬局面,或者返回错误值,或者抛出异常。而:
assert(dest != NULL);
这是检查契约,履行权利。如果条件不满足,那么错误在对方而不在我,我可以立刻“撕毁合同”,罢工了事,无需做任何多余动作。这无疑可以大大简化程序库和组件库的开发。
契约所核查的,是“为保证正确性所必须满足的条件”,因此,当契约被破坏时,只表明一件事:软件系统中有bug。其意义是说,某些条件在到达我这里时,必须已经确保为“真”。如果在我这里发现契约没有被遵守,那么表明系统中其他模块没有正确履行自己的义务。
一般来说,在面向对象技术中,我们认为“接口”是唯一重要的东西,接口定义了组件,接口确定了系统,接口是面向对象中我们唯一需要关心的东西,接口不仅是必要的,而且是充分的。然而,契约观念提醒我们,仅仅有接口还不充分,仅仅通过接口还不足以传达足够的信息,为了正确使用接口,必须考虑契约。
契约式编程的优点:实现面向对象的目标:可靠性、可扩展性和可复用性。
缺点: 如果异常在程序运行过程中才能够检测出来的话可能导致一些错误。
在本项目中,我们在计算模块中实现了Core接口,并且定义了传入参数的规范,所以可以采用契约式编程,如果传入的参数不合法,或者传入的不是符合规范的字符,说明调用者没有遵循契约调用参数,可以直接assert。在执行无错误程序期间,不应违反契约条件。
在单元测试当中,我们所用的也都是断言。
计算模块部分单元测试展示
1.对图模块的公开类以及公开类里面的公开方法添加单元测试。对于构造函数和公共属性进行单元测试。我们创建了一个测试图模块的单元测试类进行测试。
思路:对图进行构建,改变节点的权重和边的信息,然后寻找图的最长路,看是否和正确结果相同。
部分代码展示:
TEST_METHOD(TestHeadTailGraph)
{
DFSHeadTailGraph g(4);
for (int i = 1; i <= 4; i++)
{
g.changeVecWeigh(i, 1);
}
g.setHeadSingle(3);
g.setTailSingle(1);
g.insertEdge(3, 2);
g.insertEdge(2, 1);
Assert::AreEqual(2, g.getEdgeNum());
vector<int> ans = g.getAnsChain();
Assert::AreEqual(3, (int)ans.size());
}
TEST_METHOD(LoopGraph)
{
DFSGraph g(4);
for (int i = 1; i <= 4; i++)
{
g.changeVecWeigh(i, 1);
}
g.insertEdge(3, 4);
g.insertEdge(4, 3);
Assert::AreEqual(true,g.hasCircle());
}
2、对不同参数组合的测试
思路:对于所有参数组合,可以进行分析,寻找最长单词链,最长字母链,是否有环,是否有首尾字母的约束,一共有 2*2*4 = 16 情况,分别构造测试数据进行测试。
测试数据的构建:
- 对于边界条件,比如只输入一个单词,或者没有找到单词链的情况,都需要单独构造测试数据。
- 所有单词都互相能构成链的情况,比如 “aaaaa aaa aa a”的情况
- 最长单词链和最长字母链同时存在但结果不同的情况。
- 常规测试数据,随机生成。
- 大文本测试数据。
部分代码展示:
TEST_METHOD(HeadTest_Loop)
{
char *words[4] = { "cddd","dddc","aac","bad" };
char *result[4];
int ans = gen_chain_char(words, 4, result, 'a', 0, true);
Assert::AreEqual(3, ans);
string str;
for (int i = 0; i < ans; i++)
{
str.append(result[i]);
}
Assert::AreEqual((string) "aaccddddddc", str);
}
TEST_METHOD(TailTest_Loop)
{
char *words[4] = { "kzz","kdd","ak","ka" };
char *result[4];
int ans = gen_chain_char(words, 4, result, 0, 'z', true);
Assert::AreEqual(3, ans);
string str;
for (int i = 0; i < ans; i++)
{
str.append(result[i]);
}
Assert::AreEqual((string) "kaakkzz", str);
}
TEST_METHOD(HeadTailTest_Loop)
{
char *words[13] = { "abcd","defg","gkbb","bmmm","mjjj","jooo" ,"bg","gb"};
char *result[6];
int ans = gen_chain_word(words, 8, result, 'd', 'j', true);
Assert::AreEqual(6, ans);
string str;
for (int i = 0; i < ans; i++)
{
str.append(result[i]);
}
Assert::AreEqual((string) "defggkbbbggbbmmmmjjj", str);
}
3、单元测试覆盖率展示
单元测试覆盖率结果如下,覆盖率达到98%。
计算模块异常处理说明
1、图模块的异常种类
在公有方法中,插入边和修改结点权值的函数需要判断是否溢出边界,如果是要抛出异常。
TEST_METHOD(Vertex_insert_edge_outofrange)
{
try
{
DFSGraph g(3);
g.insertEdge(5, 6);
}
catch (exception &e)
{
Assert::AreEqual(edge_out_of_range_error, e.what());
}
}
TEST_METHOD(Vertex_change_weight_outofrange)
{
try
{
DFSGraph g(3);
g.changeVecWeigh(4, 8);
}
catch (exception &e)
{
Assert::AreEqual(vertex_out_of_range_error, e.what());
}
}
2、Core模块输入无法识别的单词
TEST_METHOD(Core_words_unrecognized)
{
try
{
Core core;
char *words[3] = { "aa123","32432","333" };
core.insertChain(words, 3);
}
catch (exception &e)
{
Assert::AreEqual(m_word_error, e.what());
}
}
3、在core的接口部分,如果出现len超出最大范围,或者head和tail不在指定的字母范围内,则要抛出异常
TEST_METHOD(Interface_check_head_parameter)
{
try
{
checkParameter(10, 'A', 0);
}
catch (exception &e)
{
Assert::AreEqual(m_headchar_error, e.what());
}
}
TEST_METHOD(Interface_check_tail_parameter)
{
try
{
checkParameter(10, 0, 1);
}
catch (exception &e)
{
Assert::AreEqual(m_tailchar_error, e.what());
}
}
TEST_METHOD(Interface_check_len_parameter)
{
try
{
checkParameter(1000000, 0, 1);
}
catch (exception &e)
{
Assert::AreEqual(m_len_error, e.what());
}
}
4、core部分,如果没有选择enable_loop但是单词链中出现隐环,抛出异常
TEST_METHOD(Interface_check_loop)
{
try
{
char *words[2] = { "abb","baa" };
char *result[2];
int ans = gen_chain_word(words, 2, result, 0, 0, false);
Assert::AreEqual(ans, 0);
}
catch (exception &e)
{
Assert::AreEqual(m_loop_error, e.what());
}
}
界面模块的详细设计过程
界面模块我们使用了VS的MFC框架来进行搭建,主要是对用户的输入进行响应,调用我们Core模块的dll接口来进行结果的输出。
-
首先需要进行需求分析,用户需要哪些交互的模块,需要输入文本框,选项的按钮,文件名的文本框,最终的确认操作按钮,导出文件按钮,结果展示的文本框等。
-
接下来给每个ui进行代码编辑,响应用户的操作。
-
对接dll接口进行测试。
部分代码展示:
void CWordChainGUIDlg::OnBnClickedOk()
{
UpdateData(true);
char *words[MAX];
int chainlen;
if (m_inputFile != "")
{
bool isread = read_file(m_inputFile, m_inputWords);
if(!isread)
{
throw exception("file not found!");
}
chainlen = dealInput(words, m_inputWords);
}
else
chainlen = dealInput(words, m_inputWords);
char *result[MAX];
char head = m_headChar.GetAt(0);
char tail = m_tailChar.GetAt(0);
if ((head != 0)&&((head <= 96) || (head >= 123)))
throw exception("head charactor must be lower alphabet");
if ((tail != 0)&&((tail <= 96) || (tail >= 123)))
throw exception("tail charactor must be lower alphabet");
//printf("%s", m_inputWords);
if (m_isLongestWord)
{
m_answer = gen_chain_word(words, chainlen, result, head, tail, m_enableLoop);
}
else
{
m_answer = gen_chain_char(words, chainlen, result, head, tail, m_enableLoop);
}
CString str;
for (int i = 0; i < m_answer; i++)
{
str += result[i];
str += "\r\n";
delete[]result[i];
}
m_wordAnsChain = str;
INT_PTR nRes;
AnswerDisplayDlg ansDlg;
ansDlg.m_ansLength = m_answer;
ansDlg.m_wordStr = str;
nRes = ansDlg.DoModal();
UpdateData(false);
for (int i = 0; i < chainlen; i++)
{
delete[]words[i];
}
if (IDCANCEL == nRes)
return;
}
界面模块与计算模块的对接
ui | 功能 |
---|---|
单词输入框 | 可以支持输入单词文本,并且对单词文本进行自动分割处理,和文件输入格式相同 |
首字母输入框 | 如果没有内容则默认为0,可以支持输入小写字母,如果输入不合理会有错误框弹出提示 |
尾字母输入框 | 如果没有内容则默认为0,可以支持输入小写字母,如果输入不合理会有错误框弹出提示 |
单词链选项 | 选择最长单词数目或者最长字母数目 |
是否允许单词链隐环 | 默认不允许,如果选择则允许 |
指定输入文件 | 如果不输入则默认从单词输入框读取,输入文件名则从文件读取,如果找不到文件则会有错误框弹出提示 |
生成按钮 | 设置完后生成单词链,会有新窗口弹出 |
导出文件按钮 | 可以填写文件名后导出文件 |
结对编程
优点:
最大的优点是在于两个人之间可以随时的复审和交流,程序各方面的质量取决于一对程序员中各方面水平较高的那一位。这样,程序中的错误就会少得多,程序的初始质量会高很多,这样会省下很多以后修改、测试的时间。
以下摘自博客
(1)在开发层次,结对编程能提供更好的设计质量和代码质量,两人合作能有更强的解决问题的能力。
(2)对开发人员自身来说,结对工作能带来更多的信心,高质量的产出能带来更高的满足感。
(3)在心理上, 当有另一个人在你身边和你紧密配合, 做同样一件事情的时候, 你不好意思开小差, 也不好意思糊弄。
(4)在企业管理层次上,结对能更有效地交流,相互学习和传递经验,能更好地处理人员流动。因为一个人的知识已经被其他人共享。
缺点:
结对的两个人需要时间磨合,没有尝试过这种模式的人也需要时间去适应。
对于需要研究的项目不适合结对编程。
一些比较简单的测试验证工作,如果需要花较长的时间,结对会造成时间的浪费。
自我评价
优点:执行力较强,态度良好,有合作精神,注意力比较集中,能够较好地统筹规划时间。
缺点:编程能力较弱,对于语言和算法掌握不熟练,花费大量的时间进行学习。
评价队友
优点:能够细心发现bug,态度良好,有合作精神,在合作的过程中能相互学习、相互磨合。
缺点:执行力较弱。
与其他小组的松耦合测试
-
本组学号:16021160 15061078
-
合作小组学号:16061109 16061097
-
出现的问题:
1、在测试另一个小组的dll时,我在文件中写入了中文字符,导致程序没有正常退出,该小组没有对文本的内容进行详细地异常分析,导致程序异常退出。
2、在该小组测试我们dll的时候,发现程序中的bug,即对于所有单词都能构成首尾链的情况输出异常,我们组对自己的bug进行了改进。
3、对方小组使用类封装的dll,分析编译的时候出现warning提示,接口调用需要类的实例化客户端,所以我当时测试的时候是实例化了该Core类,不能直接调用方法进行测试。
PSP表格实际消耗
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 2*60 |
·Estimate | 估计这个任务需要多少时间 | 60 | 2*60 |
Development | 开发 | 57*60 | 71*60 |
·Analysis | ·需求分析 (包括学习新技术) | 8*60 | 9*60 |
·Design Spec | · 生成设计文档 | 4*60 | 2*60 |
·Design Review | · 设计复审 (和同事审核设计文档) | 2*60 | 1*60 |
·Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 1*60 | 1*60 |
·Design | · 具体设计 | 5*60 | 6*60 |
·Coding | · 具体编码 | 24*60 | 36*60 |
·Code Review | · 代码复审 | 8*60 | 12*60 |
·Test | · 测试(自我测试,修改代码,提交修改) | 5*60 | 5*60 |
Reporting | 报告 | 5*60 | 5*60 |
·Test Report | · 测试报告 | 2*60 | 3*60 |
·Size Measurement | · 计算工作量 | 1*60 | 1*60 |
·ostmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 2*60 | 1*60 |
合计 | 63*60 | 78*60 |