结对项目--计算最长单词链

项目 内容
这个作业属于哪个课程 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进行最长链的寻找,把结果保存在私有变量vector ans中。
    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、命令行输入类

    对输入的单词文本进行处理,实现读入文件,写文件等操作。

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
posted @ 2019-03-14 23:47  ZACKERrr  阅读(420)  评论(2编辑  收藏  举报