北航软工2023-结对编程作业

结对编程作业


项目

内容

这个作业属于哪个课程

2023年北航敏捷软件工程社区-CSDN社区云

这个作业的要求在哪里

结对项目-最长英语单词链-CSDN社区

我在这个课程的目标是

了解C++项目构造方法,使用单元测试工具开发高鲁棒应用程序

这个作业在哪个具体方面帮助我实现目标

对软件实例有初步分析和认知,学习结对编程方法

1.在文章开头给出教学班级和可克隆的 Github 项目地址(例子如下)。
  • 教学班级:周四班

2.在开始实现程序之前,在下述 PSP 表格记录下你估计将在程序的 各个模块的开发上耗费的时间。

PSP

Personal Software Process Stags

预估耗时(分钟)

实际耗时(分钟)

Planning

计划

30

30

·Estimate

·估计这个任务需要多少时间

30

30

Development

开发

1290

1650

·Analysis

·需求分析(包括学习新技术)

120

120

·Design Spec

·生成设计文档

30

60

·Design Review

·设计复审(和同事审核设计文档)

30

15

·Coding Standard

·代码规范(为目前的开发制定合适的规范)

30

15

·Design

·具体设计

120

100

·Coding

·具体编码

600

900

·Code Review

·代码复审

60

60

·Test

·测试(自我测试,修改代码,提交修改)

300

500

Report

报告

180

200

·Test Report

·测试报告

120

120

·Size Measurement

·计算工作量

30

20

·Postmortem & Process Improvement Plan

·事后总结,并提出改进计划

30

60

-

合计

1500

1880

3.看教科书和其它资料中关于 Information Hiding,Interface Design,Loose Coupling 的章节,说明你们在结对编程中是如何利用这些方法对接口进行设计的。
  • 信息隐藏(Information Hiding):信息隐藏指在设计和确定模块时,使得其他不需要这个模块信息的模块不可访问该模块内包含的特定信息(过程或数据)。

将核心代码(算法部分)放进core中,将文件读入,命令行处理放进另一个文件中,两者互相不可见。

  • 接口设计(Interface Design):接口作为软件系统不同部分衔接的约定,设计合理的接口可以使系统功能得到合理划分。

设计函数返回值类型,返回值的含义,错误码等,降低产生歧义的可能性。

  • 松耦合(Loose Coupling):降低模块之间的关联程度,修改一个模块时不会影响到其他模块

将整个系统拆分为输入、核心计算、输出模块,并通过设计接口规定了三个模块之间的交流方式,降低模块之间的依赖性。

4.计算模块接口的设计与实现过程。设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图?说明你的算法的关键(不必列出源代码),以及独到之处。

题目要求为求最长单词链,可以将单词看作点,能连在一起的单词看作边,得到一张图,问题转化为求图的最长路。共有3个主要函数,分别处理DAG中最长路,DAG中所有路径,一般图中的最长路。对于DAG中的最长路,采用DP的思路,维护当前点结束的最长路径长度,同时记录从那个点转移来的。对于后面两种情况,采用DFS枚举所有情况。在求解时带入了参数(首字母,尾字母等),避免了大量的分类讨论。

5.展示在所在开发环境下编译器编译通过无警告的截图

通过CMake构建命令行可执行文件成功且无警告。

通过CMake打包core.dll成功且无警告。

gui运行成功且无警告。

6.阅读有关 UML 的内容,画出 UML 图显示计算模块部分各个实体之间的关系(画一个图即可)。

7.计算模块接口部分的性能改进。记录在改进计算模块性能上所花费的时间,并展示你程序中消耗最大的函数,陈述你的性能改进策略。

我们项目中主要对-r参数的情况进行优化,在存在大量单词环时,搜索将占用大量CPU时间。使用测试数据如下:

aa axa aya aza ab axb ac axc ba bxa bb bxb byb bzb bc bxc ca cxa cb cxb cc cxc cyc czc cd

使用Visual Studio性能探查器分析如下图所示,计算总时间为31.626秒。可见circle_max函数消耗最大。

对于存在单词环的情况,问题会转化为求一般图中的最长路(每个点只能经过一次)。此问题为NP问题,需要使用指数级别的搜索来解决。此时可以对图进行一定的优化以降低搜索的次数。具体方法为:对于所有首字母和尾字母相同的单词,不再两两之间连边,而是只连出一个环,同时选择一个点连所有的入边,然后环中它的上一个点连所有的出边。这样在搜索时就可以搜到一个单词后,首先链接所有首尾字母相同的单词(例如ab-bxb-byb-bzb-bc)。

优化后,同样的数据计算总时间为12.911秒,同时可以看到circle_max函数所消耗资源大大降低。

8.阅读 Design by Contract,Code Contract 的内容,并描述这些做法的优缺点,说明你是如何把它们融入结对作业中的。

Design by Contract即契约式编程,其中需要软件设计者为软件定义正式的,精确的并且可验证的接口。具体来说就是描述每个模块的前置条件,后置条件和不变式。

优点:结构清晰,易于测试,编写时可以专注于这个模块。

缺点:设计需要大量精力,不太容易修改设计。

9.计算模块部分单元测试展示。展示出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路。并将单元测试得到的测试覆盖率截图,发表在博客中。要求总体覆盖率到 90% 以上,否则单元测试部分视作无效。

单元测试使用Google Test测试框架,一部分手工构造样例以求覆盖所有参数和异常情况,保证单元测试覆盖率,一部分使用随机生成数据保证功能正确性。

本部分主要对core暴露的接口以及cli解析参数进行了测试,尽可能全面覆盖所有参数情况,对返回值进行正确性检验,对单词列表进行合法性检验。

  • void answer_test(int index, char*words[], char*result[], int* rtn)读取输入参数以及测试数据,调用对应接口,并检验返回值是否正确。

void answer_test(int index, char*words[], char*result[], int* rtn) {
    string fileName = "../test/CoreTests/testfile" + to_string(index) + ".txt";
    ifstream ifile;
    ifile.open(fileName,ios::in);
    string line;
    vector<char*> buffer;
    while (getline(ifile, line))
    {
        // 读取测试参数和数据
    }
    // 调用对应接口
    switch (model) {
        case 0:
            tmp = gen_chains_all(words,len,result);
            break;
        case 1:
            // 调用对应接口
            ......
    }
    *rtn = tmp;
    // 返回值正确性检验
    ASSERT_EQ(tmp,realRtn);
    if (model == 0) {
        // 检验-n参数下,返回的单词链是否满足首尾相接
        chain_test_all(result,*rtn);
    }
    else {
        // 检验-w,-c参数下,返回的单词链是否首尾相接
        // 是否满足-h,-t,-j三个参数要求
        chain_test(result,*rtn);
        head_tail_ban_test(result,head,tail,banned,*rtn);
    }
}
  • void chain_test_all(char*result[], int len)以及void chain_test(char*result[], int len)检查计算所得单词链是否满足首尾相接的约束。

void chain_test_all(char*result[], int len) {
    for (int i = 0;i<len;i++) {
        char* s = result[i];
        int len1 = strlen(s);
        for (int j = 0;j<len1;j++) {
            for (int k = j+1;k<len1;k++) {
                if (s[k] == ' ') {
                    ASSERT_EQ(s[k-1],s[k+1]);
                    j = k + 1;
                    break;
                }
            }
        }
    }
}
  • void head_tail_ban_test(char*result[],char head,char tail,char banned,int len) 检查返回的单词链是否满足-h,-t,-j三个参数要求。

void head_tail_ban_test(char*result[],char head,char tail,char banned,int len) {
    if (len > 0) {
        if (head != '\0')
            ASSERT_EQ(result[0][0],head);
        if (tail != '\0') {
            int str_len = strlen(result[len - 1]);
            ASSERT_EQ(result[len-1][str_len - 1],tail);
        }
        if (banned != '\0') {
            for (int i = 0;i<len;i++) {
                ASSERT_NE(result[i][0],banned);
            }
        }
    }
}
  • void unique_test(char*result[],int len)检查单词链中的各单词是否满足唯一性要求。

void unique_test(char*result[],int len) {
    unordered_set<string> unique;
    for (int i = 0;i<len;i++) {
        ASSERT_TRUE(unique.find(result[i]) == unique.end());
        unique.insert(result[i]);
    }
}

10.计算模块部分异常处理说明。在博客中详细介绍每种异常的设计目标。每种异常都要选择一个单元测试样例发布在博客中,并指明错误对应的场景。

本次项目共设计了10种异常并进行测试。由于该部分单元测试构造思路为读取配置文件直接调用命令行的main函数,这里展示调用过程,此后的样例中仅给出配置文件,配置文件第一行表示异常码,第二行表示使用数据文件,之后表示参数。

void cli_test(int index) {
    char* argv[15];
    int argc = 0;
    string fileName = "../test/CliTests/config" + to_string(index) + ".txt";
    ifstream ifile;
    ifile.open(fileName,ios::in);
    string line;
    getline(ifile,line);
    int realRtn = stoi(line);
    getline(ifile,line);
    char*path = new char[line.length() + 20];
    strcpy_s(path,18,"../test/CliTests/");
    path = strcat(path,line.c_str());
    argv[argc++] = (char*)"Wordlist.exe";
    while (getline(ifile, line))
    {
        if (!line.empty())
        {
            char* ptr = new char[line.length() + 1];
            strcpy_s(ptr, line.length() + 1, line.c_str());
            argv[argc++] = ptr;
        }
    }
    argv[argc++] = path;
    int rtn = test_main(argc,argv);
    ASSERT_EQ(rtn, realRtn);
}
  • 参数不兼容(PARAM_CONFLICT)
  • 所给参数不兼容(如-n和-w同时使用),具体参数冲突如下表:

-n

-w

-c

-h

-t

-j

-r

-n

×

×

×

×

×

×

×

-w

×

×

×

-c

×

×

×

-h

×

×

-t

×

×

-j

×

x

-r

×

×

  • 单元测试样例

-1            // 命令行异常返回-1
chain.txt     // 使用文件chain.txt(无环数据)
-n              // 参数1
-w              // 参数2
              // -n 与 -w冲突。
  • 必要参数缺失(PARAM_LACK)
  • 只出现-h,-t,-j,-r四个参数,而没有-n,-w,-c三个必要参数。

  • 单元测试样例

-2                 
chain.txt                
-j                
a                
-h                
b
  • 数据存在单词环(WORD_CIRCLE)
  • 数据中存在单词环,但是并没有给出-r参数。

  • 单元测试样例

-3
circle.txt    // 使用数据circle.txt(带环数据)
-c
-h a
  • 参数格式错误(PARAM_FORMAT)
  • 指定-h,-t,-j三个参数时,其后没有紧跟单个字母(没有字符,字符不是字母,多个字符)。

  • 单元测试样例

-4
chain.txt
-w
-t
xx

-4
chain.txt
-w
-h
?

-4
chain.txt
-w
-j
-h
a
  • 使用未定义参数(PARAM_UNDEFINED)
  • 所给参数中出现为定义的参数,即出现除-n,-w,-c,-h,-j,-t,-r外的参数。

  • 单元测试样例

-5
chain.txt
-w
-g
  • 输入非文本文件(FILE_TYPE_WRONG)
  • 指定的文件不是文本文件,即文件格式不是".txt"

  • 单元测试样例

-6
chain.py
-w
  • 文件不存在(FILE_NOT_EXIST)
  • 指定输入文件不存在。

  • 单元测试样例

-7
chainx.txt
-w
  • 未选择文件(FILE_NOT_GIVEN)
  • 参数中不存在文件路径。

  • 单元测试样例

TEST(TestCase,FileNotSelected){
    char* argv[15];
    int argc = 0;
    argv[argc++] = (char*)"Wordlist.exe";
    argv[argc++] = (char*)"-w";
    argv[argc++] = (char*)"-h";
    argv[argc++] = (char*)"a";
    ASSERT_EQ(test_main(argc,argv), -9);
}
  • 参数重复指定(PARAM_DUPLICATE)
  • 重复指定-h,-t,-j三个参数的字母。

  • 单元测试样例

-10
chain.txt
-w
-t
a
-t
s
  • 答案过长(RESULT_TOO_LONG)
  • 计算结果数量超过20000。

  • 单元测试样例

bc cc cd de df ee ef eg ff fg gg gh gi hh hi ii ij jk jl jj kk kl ll lm ln mm mn nn
11.界面模块的详细设计过程。在博客中详细介绍界面模块是如何设计的,并写一些必要的代码说明解释实现过程。
  • CLI

CLI划分为四个模块:参数解析模块、文件读取模块、函数调用模块、输出模块。

参数解析模块遍历命令行参数(即main函数中的argv),并检测用户输入的参数是否存在问题,及时终止程序并及时反馈给用户。

int read_para(int argc,char *argv[],int* qall)
{
    // 记录-h,-t,-j参数是否已经出现过
    bool flagH = false,flagT = false,flagJ = false; 
    for(int i=1;i<argc;i++)                       // 遍历参数列表
    {
        string ag=argv[i];
        if(ag=="-w")                                // -w参数
        {
            if(checkp(2)<0)                         // 异常检测
                return PARAM_CONFLICT;              // 返回异常码
            arg[2]=1;                               // 记录已有的参数
            op=0;                                   // 记录操作类型,调用对应接口
        }
        else if(ag=="-c")
        {
            if(checkp(3)<0)
                return PARAM_CONFLICT;
            arg[3]=1;
            op=1;
        }
        else if ......
}

文件读取模块读取参数解析后打开的文件,进行单词划分并转换为小写,记录下这些单词单词后返回一个二维向量。

while(getline(readFile,s))
{
    string now;
    for(int i=0;i<s.size();i++) // 对文件每一行字符进行单词划分
    {
        if(!isletter(s[i]))
        {
            if(now.size()>0)
                res.push_back(now);
            now="";
        }
        else
            now+=(s[i]<'a')?(s[i]+'a'-'A'):s[i];
    }
    if(now.size()>0)
        res.push_back(now);
}

函数调用模块根据前两个模块得到的参数信息已经单词表,调用core模块暴露的接口,并判断接口返回值是否异常(存在单词环或计算结果过长)。

输出模块将函数调用得到的结果,输出到solution.txt文件中。

void print_ans(int len,char* result[],int operate)
{
    // 打开/创建solution.txt文件,输出答案
    ofstream fout("solution.txt");
    if(operate==1)
        fout<<len<<"\n";
    for(int i=0;i<len;i++)
        fout<<result[i]<<"\n";
    fout.close();
}
  • GUI

GUI使用python的pyqt库进行开发。共可划分为六个模块:数据输入模块、参数选择模块、输出模块、接口调用模块、异常窗口模块、主界面模块。

数据输入模块利用qt提供的QTextEdit和QFileDialog组件,支持用户通过导入文件进行单词输入、通过界面直接输入,该模块记录下单词文本,并提供给接口调用模块进行调用。

​# 读取文件
def getFile(self):
    # 指定文件类型为.txt文件
    filename, t = QFileDialog.getOpenFileName(self, "打开文件", '', '文本文件(*.txt)')
    # 防止取消打开文件后闪退
    if filename:
        if not filename.endswith(".txt"):
            WarningView("请选择txt文件输入")
        else:
            self.filePath.setText(filename)
            with open(filename, 'r', encoding='utf-8', errors='ignore') as f:
                data = f.read()
                self.inputBox.setText(data)

# 返回记录的文本
def getInputData(self):
    return self.inputBox.toPlainText()

参数选择模块主要使用qt提供的QRadioButton,QComboBox以及QCheckBox进行编写,记录下用户对所要求7个参数的选择情况。同时通过逻辑判断以及按钮的setEnable方法避免用户选择冲突的参数。

# 控制额外参数-h,-t,-j,-r是否可选
    def setAdditionEnabled(self, isTrue: bool):
        self.beginCharBox.setEnabled(isTrue)
        self.endCharBox.setEnabled(isTrue)
        self.banCharBox.setEnabled(isTrue)
        self.circleBtn.setEnabled(isTrue)
        if not isTrue:
            self.beginCharBox.setCurrentIndex(0)
            self.endCharBox.setCurrentIndex(0)
            self.banCharBox.setCurrentIndex(0)
    
    # 选择-n参数时,额外参数无法选择
    def clickAllBtn(self):
        self._model = 0
        self.setAdditionEnabled(False)

    # 选择-w或-c参数时,额外参数可供选择  
    def clickWordBtn(self):
        self._model = 1
        self.setAdditionEnabled(True)

    def clickLetterBtn(self):
        self._model = 2
        self.setAdditionEnabled(True)

输出模块同样利用qt的QTextEdit和QFileDialog组件,展示计算结果,并为用户提供导出结果的功能。

 # 将结果展示到窗口
    def setOutputView(self, output):
        self.outputView.setText(output)

    # 导出计算结果
    def export(self):
        filename, t = QFileDialog.getSaveFileName(self, '导出结果', 'ans', "文本文件(*.txt)")
        if filename:
            with open(filename, 'w') as f:
                f.write(self.outputView.toPlainText())

接口调用模块使用ctypes库导入计算模块的动态链接库,通过ctypes实现的一系列类型转换方法,将python的数据类型包装为C类型作为函数调用参数。以gen_chains_all方法为例:

# 通过WinDLL方法导入动态链接库
dll = WinDLL("../bin/core.dll")

# int gen_chains_all(char* words[], int len, char* result[]);
def gen_chains_all(words):
    length = len(words)
    # 使用c_char_p创建指针数组并通过encode转换为二进制序列
    words_ptr = (c_char_p * length)()
    for i in range(length):
        words_ptr[i] = words[i].encode('utf-8')
    # 使用c_int创建c的int类型
    c_len = c_int(length)
    # 与words同理
    c_rst_ptr = (c_char_p * 20005)()
    # 调用core.dll对外提供的gen_chains_all接口
    cnt = dll.gen_chains_all(words_ptr, c_len, c_rst_ptr)
    # 根据返回结果判断是否有异常
    if cnt > 20000:
        rst = []
        WarningView("结果过长,请检查数据")
    elif cnt >= 0:
        rst = [str(cnt).encode('utf-8')]
        for i in range(1, cnt + 1):
            rst.append(c_rst_ptr[i - 1])
    else:
        rst = []
        WarningView("存在单词环,请检查数据或允许单词环")
    return rst

异常窗口模块使用qt的QMessageBox组件,当其他模块判断出现异常时(如文件类型,文件不存在等),可直接创建一个WarningView对象,及时给用户反馈。

class WarningView(QWidget):
    # msg表示反馈给用户的信息
    def __init__(self, msg):
        super().__init__()
        warn = QMessageBox()
        warn.setText(msg)
        warn.setIcon(QMessageBox.Critical)
        warn.setWindowTitle("异常提示")
        warn.exec_()

主界面模块将各模块进行布局,统一放置在一个界面上,为简化组件间通信问题,在主界面模块额外增加开始计算按钮以调用接口,并记录运行时间。

12.界面模块与计算模块的对接。详细地描述 UI 模块的设计与两个模块的对接,并在博客中截图实现的功能。
  • core模块

core模块通过__declspec(dllexport)将三个计算接口声明为导出函数,同时由于计算模块代码使用C++编写,为防止函数重载还需要声明extern "C"让函数使用C编译方式。同时为便于gui交换,我们提供的接口与课程作业建议一致。

由于项目使用CMake构建,为打包成动态链接库,需要在CMakeLists.txt中添加如下配置。

# 表示使用core.cpp和core.h文件导出dll
add_library(core SHARED src/core.cpp src/core.h)
  • CLI

CLI通过extern "C" __declspec(dllimport)导入dll的函数,解析参数以及划分单词后在对接部分直接调用对应接口,并对返回值进行检验。

以下功能演示为部分异常反馈,以及正常使用情况。

  • GUI

GUI的设计与对接方法在上文中已经详细描述,通过python的ctypes库实现动态链接库导入以及python数据类型向C数据类型的转换。

以下为功能展示。

计算所有单词链(此时禁用额外参数):

计算单词数量最多的单词链,并指定首字母为m:

输入数据中存在单词环且用户没有允许:

计算结果过长:

将结果以文本文件导出:

  • 附加任务:交换核心模块与界面模块
  • 合作小组队员信息:

  • 由于合作小组在存在单词环以及计算结果过长两种异常情况下,返回值与我们不相同,在处理异常时需要进行适当修改,修改异常码判断后一切功能正常。

存在单词环以及计算结果过长异常提示:

正常运行结果:

13.描述结对的过程,提供两人在讨论的结对图像资料(比如 Live Share 的截图)。关于如何远程进行结对参见作业最后的注意事项。

由于宿舍离得很近,直接线下结对编程。

14.说明结对编程的优点和缺点。同时描述结对的每一个人的优点和缺点在哪里(要列出至少三个优点和一个缺点)。

吴湛宇

  • 优点

  • 对图相关算法较为了解

  • 编写代码能力强

  • debug能力强,能快速修复bug

  • 缺点

  • 对Cmake,以及其他框架等不熟悉

王永瑶

  • 优点

  • 自学能力强,能快速掌握新知识/技能

  • 思维严谨,能找到很细小的bug

  • 熟悉Cmake等使用方法

  • 缺点

  • 对算法部分不太了解

15.在你实现完程序之后,在附录提供的PSP表格记录下你在程序的各个模块上实际花费的时间。

见2.在开始实现程序之前,在下述 PSP 表格记录下你估计将在程序的 各个模块的开发上耗费的时间。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值