【软工】结对编程做业

项目 内容
这个做业属于哪一个课程 2019BUAA软件工程
这个做业的要求在哪里 做业要求
我在这个课程的目标是 完成本次做业,同时熟悉结对编程
这个做业的帮助 熟悉了vs2017的部分操做,同时对结对编程有了比较深入的理解

1、本次做业项目github地址


项目地址html

2、开发前PSP表格


PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划
· Estimate · 估计这个任务须要多少时间 60
Development 开发
· Analysis · 需求分析 (包括学习新技术) 210
· Design Spec · 生成设计文档 90
· Design Review · 设计复审 (和同事审核设计文档) 120
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 30
· Design · 具体设计 300
· Coding · 具体编码 500
· Code Review · 代码复审 300
· Test · 测试(自我测试,修改代码,提交修改) 300
Reporting 报告
· Test Report · 测试报告 120
· Size Measurement · 计算工做量 30
· Postmortem & Process Improvement Plan · 过后总结, 并提出过程改进计划 30
合计 2030

3、接口设计


模块之间经过他们的api通讯,一个模块不须要知道另一个模块的内部状况,这就被称为信息隐藏或封装。封装提升了软件的可重用性,由于模块之间不紧密相连,最后封装也下降了构建大型系统的风险,即便整个系统不可用,可是这些模块多是有用的。c++

接口设计有六大原则:单一职责原则、里氏替换原则、依赖倒置原则、接口隔离原则、迪米特法则以及开闭原则。这些原则对接口设计有了很大的约束,本次咱们实现的做业没有使用继承,因此里氏替换原则天然不会违背,同时因为设计比较简单,其余的原则也都一一知足了。git

藕合度是度量一个代码单元在使用时与其余单元的关系。最理想,最松散的耦合,是一个单元无需其余代码单元特别的配合而可使用。这里的Loose Coupling实际上和单一职责原则有些相似,主要是为了确保每个类或方法的做用尽可能单一,避免耦合。github

在咱们的做业中,最核心的模块是core,内部有一个实例化的储存图主要信息的private对象graph,因此外部没法调用,这很好的实现了封装。同时,计算模块(core)和图模块(graph)是分开的,因此彼此不会干扰,各司其职,耦合度天然知足条件。算法

4、计算模块接口的设计与实现


咱们的计算模块是Core,同时储存图信息以及和图相关的运算与运算结果都存储在graph中,graph是在实例化Core对象时自动实例化的一个对象,其存储在Core 中。因为get_chain_char和get_chain_word的内部实现其实很类似,仅仅是边的权值不一样,因此咱们也在Core内部抽象出一个公共接口,这样外部在调用get_chain_word和get_chain_char的时候只须要调用这个公共接口就行了。具体的流程图以下:

算法大体思路:首先对问题进行建模:该问题能够抽象为在一个有向图中求最长路径的问题:每一个单词的首尾字母分别为有向图中的节点,而后一个单词就抽象为一条边,最长链即为最长路径。编程

  • 第一步是对输入的异常进行检测,若是输入的单词不合法或者指定的头尾不合法则报错。
  • 第二步是对输入的单词进行建图。这里有两种模式,若是是调用get_chain_word则权值是1,不然权值为单词的长度。同时,对于自环的状况咱们单独处理的,至关于后面拓扑排序的时候直接跳过了自环。
  • 第三步是进行拓扑排序。此步骤的目的是判断是否有环以及为后面的算法作准备
  • 第四步是根据上一步的排序结果选择不一样的算法。若是有环则进行暴力搜索,若是没环则根据排序结果从尾到头作一次dp便可。

独到之处windows

  • 节省空间。每条边只存了一个权值,即1或其单词长度。在一开始建图的时候就会加判断,节省了空间。
  • 代码复用。无论有环仍是无环都会先调用拓扑排序的算法,该算法即进行了排序也能够判断是否有环。同时,咱们的内部接口实现也作到了代码复用与减小代码之间的耦合。
  • 实现简洁。拓扑排序时咱们采用的是经典的dfs的写法,没有采用入度和出度的写法,实现方式更简洁,效率也不错,同时代码看上去也很简单。

5、UML图


因为vs2017能够自动导出类图,咱们最终生成的类图以下:api

6、计算模块接口部分的性能改进


【引自小伙伴的博客数组

这一部分中咱们花费了约3-4小时。咱们主要针对有环状况的算法进行改进。
因为在一个有向有环图中寻找最长的路径是一个NP问题,从算法自己的角度来看不管如何都逃不开NP这个坑,所以咱们使用了普通的DFS来进行。咱们优化的地方在于将算法中访存的消耗尽量下降。起初咱们使用了vector做为路径存储的数据结构,在通过一次性能分析后咱们发现递归部分中退栈操做不少,所以咱们将vector改成一维数组,将原先的pop_back变成栈顶指针的--操做,从而下降了时间消耗。数据结构

下图为有环状况下消耗CPU最多的函数,即DFS的主函数
image.png-109.6kB

咱们使用一个9000余词的文件对于无环状况进行了测试,结果发现算法在无环状况下表现良好,get_chain_word/char中消耗最多的部分其实是接口中进行malloc的部分。

image.png-86.7kB

7、Design by Contract, Code Contract


Design by Contract, 即契约编程:咱们在声明一个函数/方法的时候,对函数的输入和输出所具有的性质是有所指望和规定的。有时候这种性质会被咱们明确的写出来,有时候会被咱们忽略掉。这些指望和规定就是Contract。

其好处是责任的细化。每一个程序猿都只须要处理本身的契约范围负责。同时,有责任的时候也能很快的定位到我的。

在咱们的做业中,因为咱们是两我的共同开发和debug的,因此有些地方并无很严格的按照这个要求来作。

8、计算模块部分单元测试展现


【引自小伙伴的博客

单元测试范例
image.png-29.2kB

单元测试部分咱们分别对无环和有环以及各类异常状况进行了测试,共构造了19个单元测试。
构造测试的思路大致上是尽量覆盖各个分支。因为char和word两个接口在具体运行时仅仅是初始化权值不一样,所以测试中咱们更注重于各类特殊状况的测试,如开头和结尾的自环等。

单元测试的分支覆盖率为97%,其中包含了各类异常测试的覆盖。
testr.JPG-30.9kB

9、计算模块部分异常处理说明


异常一 输入的头或尾不是字母

在输入的头或尾不是字母的状况下会抛出异常,如下为测试样例:

void invalid_head()
    {
        char* words[] =
        {
            "aba",
            "avdc",
            "fewt"
        };
        char* results[100];
        int len = gen_chain_word(words, 3, results, 1, 0, false);
    }
        TEST_METHOD(WordsException9)
    {
        Assert::ExpectException<std::invalid_argument>([&] {exception_test::invalid_head(); });
        try
        {
            exception_test::invalid_head();
        }
        catch (const std::exception& e)
        {
            Assert::AreEqual("Core: invalid head or tail", e.what());
        }
    }

异常二 有环但没有指定-r参数

在检测到图中有环可是输入的参数中没有-r参数,会抛出异常:

void has_loop()
    {
        char* words[] =
        {
            "aba",
            "aca",
            "fewt"
        };
        char* results[100];
        int len = gen_chain_word(words, 3, results, 0, 0, false);
    }
    TEST_METHOD(WordsException8)
    {
        Assert::ExpectException<std::invalid_argument>([&] {exception_test::has_loop(); });
        try
        {
            exception_test::has_loop();
        }
        catch (const std::exception& e)
        {
            Assert::AreEqual("Core: loop deteced in words, you need to use -r to parse.", e.what());
        }
    }

异常三 输入的words数组中有空串

若是输入的words数组中某个字符串为空的话也会抛出异常,测试样例以下:

void empty_string()
    {
        char* words[] =
        {
            "",
            "avdc",
            "fewt"
        };
        char* results[100];
        int len = gen_chain_word(words, 3, results, 0, 0, false);
    }
        TEST_METHOD(WordsException6)
    {
        Assert::ExpectException<std::invalid_argument>([&] {exception_test::empty_string(); });
        try
        {
            exception_test::empty_string();
        }
        catch (const std::exception& e)
        {
            Assert::AreEqual("Core: empty string in words", e.what());
        }
    }

异常四 输入的串中有无效字符

若是输入的字符串中有无效字符,则会抛出该异常:

void invalid_char_test()
    {
        char* words[] =
        {
            "a12345",
            "avdc",
            "fewt"
        };
        char* results[100];
        int len = gen_chain_word(words, 3, results, 0, 0, true);
    }
        TEST_METHOD(WordsException5)
    {
        Assert::ExpectException<std::invalid_argument>([&] {exception_test::invalid_char_test(); });
        try
        {
            exception_test::invalid_char_test();
        }
        catch (const std::exception& e)
        {
            Assert::AreEqual("Core: invalid char in words", e.what());
        }
    }

异常五 没有足够单词

若是输入的words数组的长度len <= 1则一样会抛出异常,由于链的长度要求必须大于1,因此若是单词个数为负数或01的状况也会抛出异常,测试样例以下:

void not_enough_words()
    {
        char* words[] =
        {
            "a"
        };
        char* results[100];
        int len = gen_chain_word(words, 1, results, 0, 0, true);
    }
        TEST_METHOD(WordsException7)
    {
        Assert::ExpectException<std::invalid_argument>([&] {exception_test::not_enough_words(); });
        try
        {
            exception_test::not_enough_words();
        }
        catch (const std::exception& e)
        {
            Assert::AreEqual("Core: not enough words", e.what());
        }
    }

10、命令行模块


【引自小伙伴的博客

界面主要分为两大部分:命令行参数读取解析及文件读取。

在命令行参数解析部分,因为目前已有不少的开源库可供使用,本着不重复造轮子的原则咱们使用了一个较为轻量的头文件库cxxopts来处理命令行参数。这个库能够自动将各个参数的值读出并对不合法的状况抛出异常。但因为其涉及的不合法状况较为朴素,我对一些相对复杂的不合法输入进行了处理,例如同时输入-w和-c、或输入了两个-w的状况。

test2.JPG-34.8kB

最终我将命令行参数读取和分析封装在base类的parse_arguments函数中,函数经过参数将读取到的值返回。

在文件读取部分,我设计的思路是按字符从文件头开始向后扫描,并在出现特殊字符的位置断开,从而将文件中的合法单词分隔出来。
test3.JPG-38.2kB
注意到在读文件过程当中我对单词的长度也作了约束,如读到长度过长的单词则会抛出异常。最终将以上单词保存在base类成员中的数组内便可。

11、命令行与计算模块的对接


【引自小伙伴的博客

dll模块对接方面我使用了Base模块显式调用Dll的方法(即仅借助dll文件,不借助lib文件)。经过windows.h中的LoadLibrary和GetProcAddress实现。最终将调用的过程与开头读文件、解析参数等过程结合,造成完整的运行程序。

image.png-65.7kB

12、结对过程


因为咱们两人大多数时间都是在线上用teamviewer交流,加上前两次线下讨论模块设计的时候忘了拍照片,因此咱们提供咱们线上交流的截图。

咱们结对的过程其实仍是蛮顺利的。一开始咱们在大概设计好模块以后便各司其职,我负责处理计算部分,另外一个小伙伴写完了输入和异常。最后的dll生成和单元测试因为我不是很熟悉,因此咱们是连上teamviewer,而后小伙伴来操做,我提供一些帮助。因此整个过程其实蛮愉快的。就是咱们开始的时间有点晚了以及中间有些事情耽搁致使进度有点偏慢。不过此次结对编程确实是一次很棒的经历,收获颇丰。

十3、结对编程的优缺点


结对编程最大的优势在于,同一份代码通过两我的的复审以后代码的质量会有很大提高。同时,两人在写代码的过程当中不断交流思路,代码的架构也会更加丰富。

缺点在于若是两个配合很差的话可能形成1 + 1 < 2的后果,主要缘由是问题过于简单致使浪费时间以及问题过于难致使聚在一块儿讨论也是浪费时间。

成员 优势 缺点
庹东成 (1)对计算模块比较熟。 (2)对问题可以提出本身的见解并实践 (3)对待小伙伴很友善 对vs2017不太熟,致使后面生成dll和测试的时候进展缓慢
周博闻 (1)对vs2017很熟,以及操做熟练 (2)认真仔细,可以想到不少易忽略的点 (3)对待小伙伴很友善 对于题目要求有些地方不是很了解

十4、PSP表格回填


PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划
· Estimate · 估计这个任务须要多少时间 60 60
Development 开发
· Analysis · 需求分析 (包括学习新技术) 210 400
· Design Spec · 生成设计文档 90 40
· Design Review · 设计复审 (和同事审核设计文档) 120 90
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 30 10
· Design · 具体设计 300 180
· Coding · 具体编码 500 720
· Code Review · 代码复审 300 240
· Test · 测试(自我测试,修改代码,提交修改) 300 300
Reporting 报告
· Test Report · 测试报告 120 150
· Size Measurement · 计算工做量 30 30
· Postmortem & Process Improvement Plan · 过后总结, 并提出过程改进计划 30 30
合计 2030 2250

十5、模块松耦合


【引自小伙伴的博客

咱们与1610106一、16061118组互换了Core.dll进行测试。

咱们的dll文件能够在对方的界面模块下运行:


但对方的Core模块并不能被咱们的base模块调用。询问得知对方的Core以C#实现,当我使用dumpbin查看其中导出的函数时并不能查看到任何信息。

image.png-62.2kB 在上网查阅了一些资料后我了解到C++调用C#须要将dll转换为C#的类组件,貌似并无相似于c++这样直接调用的方法。

相关文章
相关标签/搜索