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


    项目 内容
    这个作业属于哪个课程 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 += "
    ";
    			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
  • 相关阅读:
    POJ 1837 (DP)
    POJ 2255(分治递归)
    红球多于白球的概率(分治递归)
    HDOJ 4039 (Data_Structure)
    管道问题(prim)
    寻找给定区间内第K小的数(分治递归)
    大数加法
    1000元购物券 (分治递归)
    POJ 1308(并查集) (Data_Structure)
    我的2012年还不是世界末日
  • 原文地址:https://www.cnblogs.com/zackerzhuang/p/10527520.html
Copyright © 2020-2023  润新知