Loading

软件工程结对项目博客作业

1. GitHub项目地址

https://github.com/kilotron/Wordlist-Pair-

2. PSP表格

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

3. 利用Information Hiding, Interface Design, Loose Coupling方法设计接口

Information hiding is part of the foundation of both structured design and object-oriented design. In structured design, the notion of “black boxes” comes from information hiding. In object-oriented design, it gives rise to the concepts of encapsulation and modularity and it is associated with the concept of abstraction. (Code Complete 2nd)

信息隐藏在结构化设计和面向对象设计中都很重要,与封装、模块化、抽象的概念紧密相关。

Loose coupling means designing so that you hold connections among different parts of a program to a minimum. Use the principles of good abstractions in class interfaces, encapsulation, and information hiding to design classes with as few interconnections as possible. Minimal connectedness minimizes work during integration, testing, and maintenance. (Code Complete 2nd)

Loose coupling是减少程序各部分的联系,在设计接口时,需要将各模块相互之间的联系降到最低。

在设计接口时,我们尽量放宽函数的前置条件,同时让函数名简洁直观,尽量准确界定函数功能。例如有这样两个函数:

class WordGraph {
public:
    bool IsCyclic();
	Path * LongestPathBetween(char head, char tail, bool isWeighted);
}

对于第二个函数,调用时指定开头字母和结尾字母,以及计算单词链长度的方法(是否以单词长度作为权重),函数返回单词链。这就做到了Loose coupling,调用者关心的参数和结果比较明确,不需要依赖其他的函数或模块。对于第一个函数,它的功能是判断是否存在单词环,调用者不需关心内部实现(怎么计算是否存在环的),这就做到了Information hiding。

4. 计算模块接口的设计与实现过程

计算模块主要在WordGraph这个类中,同时还有与WordGraph相关的Edge、Node和Path类,前二者组成WordGraph内部数据结构,Path用于表示计算结果。

WordGraph对外提供5个接口:

  • WordGraph(char *words[], int len)用给定的单词列表构造WordGraph对象
  • bool IsCyclic()判断是否存在单词环
  • Path *LongestPathFrom(char start, bool isWeighted)计算给定开头字母的最长单词链
  • Path *LongestPathBetween(char start, char end, bool isWeighted)计算给定开头和结尾字母的最长单词链
  • Path *LongestPathTo(char end, bool isWeighted)计算给定结尾字母的最长单词链
  • Path *LongestPath(bool isWeighted)计算未给定开头结尾字母的最长单词链

参数isWeighted是计算单词链长度的方式,true为使用单词字母数作为权重,false则反之。

在此基础之上,定义核心模块的对外接口:

int gen_chain_word(char* words[], int len, char* result[], char head, char tail, bool enable_loop);
int gen_chain_char(char* words[], int len, char* result[], char head, char tail, bool enable_loop);

上面两个函数调用WordGraph的对外接口实现其功能。

5. 画出UML图显示计算模块部分各个实体之间的关系

为了清晰地显示类之间的关系,图中省略了类的成员。

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

在未指定开头结尾字母时,程序进行了多次重复搜索,改进的思路是利用已有的LongestPathFrom函数避免重复搜索,改进后程序减少了50%的执行时间。

下图是未指定开头和结尾字母,按字母数计算单词链长度,存在单词环,总共有60个单词的情况下前28s的性能分析图。

改进性能用了大约半个小时。

7. 看Design by Contract, Code Contract的内容

契约式设计(Design by Contract)是设计软件的一种方法,它用形式化可验证的方法来定义前置条件、后置条件和不变式。从一个被调用模块的角度来讲,contract包括expect, guarantee, maintain三个部分的内容,也就是在进入模块前,期望一些条件是成立的,模块执行结束后,保证一些条件是成立的,并且保持类的某些属性是一致的。其优点在于模块功能边界清晰,全责分明,出现问题时容易定位是谁出现问题,不过要提前制定contract需要时间,制定一个合适的contract也需要一定的经验和技巧。

Code Contracts for .NET是一个工具,可以执行前置条件、后置条件和不变式的静态或运行时检查,并且可以生产文档,对自动化验证DbC很有帮助。

在这次我们实现的计算模块中的这个接口就体现了DbC:int gen_chain_word(char* words[], int len, char* result[], char head, char tail, bool enable_loop);,例如words的长度和len保持一致,result数组需要在调用函数前分配好足够的空间,head和tail只能是0或者字母,这些是前置条件;如果存在单词环但enable_loopfalse则抛出异常,如果一切正常则将结果放到result数组中并返回result的长度,这是后置条件。测试时也是根据这些条件来测试的。

8.单元测试展示

有关异常的测试在下一部分给出。

8.1 计算模块测试

这部分测试计算的两个接口,构造测试数据的思路是根据是否指定开头结尾字母,计算单词链长度的方式,是否存在单词环,以及边界情况分别讨论。下面给出部分代码。

代码中,TEST_INITGEN_CHAIN_WORDGEN_CHAIN_CHARASSERT_RESULT_LENASSERT_RESULT是为了简化测试代码定义的宏。

  • TEST_INIT:声明测试需要使用的变量,准备单词列表
  • GEN_CHAIN_CHAR/GEN_CHAIN_WORD:计算模块对外接口的简化版
  • ASSERT_RESULT_LEN:单词链中单词的个数
  • ASSERT_RESULT(word, i): 单词链第i个单词应该是word
// 字母最多,不指定开头结尾
TEST_METHOD(TestMethod7)
{
    TEST_INIT(22,
        "Absorb", "bailout", "basic", "cat", "team",
        "calm", "cad", "hedonism", "dam", "moon",
        "damn", "dame", "happen", "earn", "nap",
        "each", "equip", "pack", "hasp", "hack",
        "hot", "AGoddamnSuperLongWordEndsWithT");
    GEN_CHAIN_CHAR(0, 0, false);
    ASSERT_RESULT_LEN(5);
    ASSERT_RESULT("agoddamnsuperlongwordendswitht", 0);
    ASSERT_RESULT("team", 1);
    ASSERT_RESULT("moon", 2);
    ASSERT_RESULT("nap", 3);
    ASSERT_RESULT("pack", 4);
}

// 单词数最多, 不指定开头和结尾
TEST_METHOD(TestMethod3)
{
    TEST_INIT(22,
        "Absorb", "bailout", "basic", "cat", "team",
        "calm", "cad", "hedonism", "dam", "moon",
        "damn", "dame", "happen", "earn", "nap",
        "each", "equip", "pack", "hasp", "hack",
        "hot", "AGoddamnSuperLongWordEndsWithT");
    GEN_CHAIN_WORD(0, 0, false);
    ASSERT_RESULT_LEN(10);
    ASSERT_RESULT("absorb", 0);
    ASSERT_RESULT("basic", 1);
    ASSERT_RESULT("cad", 2);
    ASSERT_RESULT("dame", 3);
    ASSERT_RESULT("each", 4);
    ASSERT_RESULT("hot", 5);
    ASSERT_RESULT("team", 6);
    ASSERT_RESULT("moon", 7);
    ASSERT_RESULT("nap", 8);
    ASSERT_RESULT("pack", 9);
}

8.2 输入参数测试

这部分根据输入参数的各种不同组合构造测试用例,检测程序是否正确解析。下面给出其中一个用例。

// pre.command 指定head
TEST_METHOD(TestMethod5) {
    Preprocess pre;
    int argc = 5;
    char ** argv = new char *[argc];
    for (int i = 0; i < argc; i++) {
    	argv[i] = new char[10];
    }
    strcpy(argv[0], "Wordlist.exe");
    strcpy(argv[1], "-w");
    strcpy(argv[2], "-h");
    strcpy(argv[3], "A");
    strcpy(argv[4], "wordlist.txt");

    pre.command(argc, argv);
    Assert::IsTrue(pre.kind == W);
    Assert::AreEqual("wordlist.txt", pre.filename);
    Assert::AreEqual('a', pre.head);
}

8.3 测试覆盖率

分两个模块分别测试,下图是参数解析和文件读取部分的测试覆盖率,总覆盖率为93%。

下图是计算模块的覆盖率,总覆盖率为91%。

9. 异常处理说明

异常类型 异常说明
输入文件异常 文件名非法或者文件不存在
输入参数异常 未输入参数,-h或-t选项后跟的不是单个字母,包含未定义的选项
单词环异常 存在单词环但未给出-r选项

9.1 输入文件异常

// 文件错误 异常测试
TEST_METHOD(TestMethod8)
{
	Preprocess pre;
	Assert::ExpectException<std::exception>([&]
	{
		pre.readfile("you asshole");
	});
}

场景:文件不存在。

9.2 输入参数异常

// pre.command 异常测试
TEST_METHOD(TestMethod6) {
    try {
        Preprocess pre;
        int argc = 1;
        char ** argv = new char *[argc];
        argv[0] = new char[10];
        strcpy(argv[0], "Wordlist.exe");
        pre.command(argc, argv);
        Assert::Fail();
    }
    catch (std::exception e) {

    }
}

场景:未输入参数。

9.3 单词环异常

// 有环但enable_loop是false,应该抛出异常
TEST_METHOD(TestMethod1)
{
	TEST_INIT(7, "gag", "fag", "glitz", "zaf", "jof", "fij", "lkkj");
	Assert::ExpectException<std::exception>([&]
	{
		GEN_CHAIN_WORD(0, 0, false);
	});
}

场景:给出的单词列表中存在环,但未给出-r选项。

10. 界面模块的详细设计过程

界面使用Qt实现的,风格上采用了默认的样式,没有进行调整。

有一个菜单栏,支持打开文件和查看帮助的操作。左边是输入输出界面,可以直接在Input对应的框里编辑,右边是计算的选项,用QRadioButtonQCheckBox实现-w -c -h -t -r五个选项。

右下方有三个按钮,Extract Words把输入框中的单词提取出来,然后分行显示在Input框中。Find根据上方参数的设定情况查找单词链,结果显示在Output框中,ExportOutput框中的文本导出到文件。

界面如下图。

布局如下图。采用手工编码的方式实现,使用了一些QHBoxLayout和QVBoxLayout。

布局的代码如下:

leftLayout = new QVBoxLayout;
rightLayout = new QVBoxLayout;
topRightLayout = new QHBoxLayout;
bottomRightLayout = new QHBoxLayout;
mainLayout = new QHBoxLayout;

// 左边的输入输出框
leftLayout->addWidget(inputLabel);
leftLayout->addWidget(inputTextEdit);
leftLayout->addWidget(outputLabel);
leftLayout->addWidget(outputTextEdit);

// 右上角的单词数最多和字母数最多单选框
topRightLayout->addWidget(maxLabel);
topRightLayout->addWidget(wordRadioButton);
topRightLayout->addWidget(charRadioButton);

// 右下角查找和导出按钮
bottomRightLayout->addWidget(findButton);
bottomRightLayout->addWidget(exportButton);

// 右边布局
rightLayout->addLayout(topRightLayout);
rightLayout->addLayout(headLayout);
rightLayout->addLayout(tailLayout);
rightLayout->addWidget(loopCheckBox);
rightLayout->addWidget(extractButton);
rightLayout->addLayout(bottomRightLayout);

mainLayout->addLayout(leftLayout);
mainLayout->addLayout(rightLayout);

11. 界面模块与计算模块的对接

合作小组的学号:

16061011

16061152

把我们小组记为A,合作小组记为B。下面分别说明Core B + GUI A和Core A + GUI B两种情况。一开始两种情况都不能正常运行,经过讨论我们发现使用Core.dll的方式不一致,A组使用动态加载的方式,B组采用的是静态加载的方式,解决方法是统一加载方式,对于两种加载方式我们都进行了尝试。

11.1 Core B + GUI A

这个组合采用动态加载方式。我们为B组的Core模块添加了def文件,将源代码重新编译后得到新的Core.dll。然后重新运行程序,能够正常使用。

11.2 Core A + GUI B

这个组合采用静态加载方式。如图,未修改前看不到B组的GUI。我们将A组的Core模块与B组的GUI放在同一个解决方案中,在Core的两个接口的函数的声明前添加__declspec(dllexport)修饰。未修改B组GUI源码前发现其中有几个Core A没有定义的异常,于是我们将异常种类统一后重新编译。

在测试过程中发现GUI B未提供单词去重的功能,Core A抛出了异常,GUI B显示了异常信息,整个程序没有崩溃。于是我们在Core A中实现了单词去重并重新编译。最后的运行截图如下。

12.结对过程

由于前期找不到合适的时间,为了保证进度,我们一开始采用的是先分工,再一起复审代码的形式。周末有共同的空闲时间时,我们再一起讨论和编写测试用例。总体来说比较顺利。上图是我们再新主楼结对编程的图片。

13. 结对编程

11.1 结对编程的优缺点

优点:

  • 两人可以随时交流,不断复审代码,因此代码的质量比较高。
  • 当有另一个人在身边紧密配合, 做同样一件事情的时候, 不容易开小差,因此会比平时更加认真 。
  • 两人互换角色适合在高强度编程时保持代码的质量。

缺点:

  • 程序员的代码、工作方式、技术水平都变得公开和透明,一些人可能不太习惯。
  • 对时间要求较高,当两个人找不到共同的时间时,结对编程实施起来就比较困难。
  • 两个人需要深入的了解和合作,否则不一致的代码习惯会影响结对编程过程。

11.2 结对中每个人的优缺点

对队友的评价

优点:

  • 积极热情,有责任心
  • 具有批判性思维
  • 注重细节

缺点:

  • 编码风格不太一致

队友对我的评价

优点:

  • 能力强,推进了程序的核心算法部分,对我帮助很大
  • 工作积极,总是提前完成相应内容
  • 时间安排合理,使得任务顺利完成

缺点:

  • 说话声音稍微有点轻,但不影响交流ε=ε=ε=( ̄▽ ̄)

14. PSP表格见2

posted @ 2019-03-14 17:29  Kilotron  阅读(326)  评论(3编辑  收藏  举报