0
点赞
收藏
分享

微信扫一扫

软工结对项目-最长单词链

月孛星君 2022-04-03 阅读 67
microsoft

软工结对项目-最长单词链

项目地址

项目地址:https://gitee.com/soft-pair-programming-fxj-lyyf/wordlist.git

估计时间与实际时间(独立)

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

利用Information Hiding,Interface Design,Loose Coupling设计接口(独立)

  • 信息隐藏:即将类的成员私有化,避免无意中对私有成员进行赋值。在本项目中,我们将Solution类中的graph字段进行了私有化,这样可以避免在其他地方对graph进行了更改导致错误,对graph的操作都通过graph暴露的方法来操作。

  • 接口设计:一个好的接口能够提供给后面的程序设计一个良好的框架,在这本项目里,我们对可能出现的需求都在Solution类中设计了相应的接口。通过这些接口,我们可以对输入的不同需求调用相应的方法,不必在意接口的具体实现,也为后续的core接口的制作和测试提供了便利。

  • 松耦合:我们设计时将字符读入与处理模块、算法模块、图模块等分开写,当代码有改动时,可以不用大规模的改动我们的代码,我们只用定位于一个出问题的模块,然后对其进行更改就好了,而且能做到不改变其它模块的服务。

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

计算模块指 core.dll,该模块实现了如下 4 个 API:

int gen_chains_all(char* words[], int len, char* result[])
int gen_chain_word(char* words[], int len, char* result[], 
        char head, char tail, bool allow_circ)
int gen_chain_word_unique(char* words[], int len, char* result[])
int gen_chain_char(char* words[], int len, char* result[], 
        char head, char tail, bool allow_circ)

我们对这些 API 进行了如表 2 的约定。关于 char *result[] 的问题,我的思考如下:

  • 最好采用一段连续的空间来存放输出,每一行输出中仅用 ‘\0’ 来分隔,调用者通过遍历 result 中的每一个元素,可以直接输出内容。此时,第 i 个元素( char * 类型指针)都指向该段内存中第 i 行输出的首地址
  • 内存需要申请者来释放,如果由 dll 来申请,则自己不可能释放,因此必须由调用者来申请
  • 申请的内存位置需要传入 dll API

因此,调用者申请大小为 128 MB (假定可以存入所有有效的输出)的堆空间,并将该段内存的首地址赋值给 result[0]。由此,每生成一行有效的输出,则在 result[i - 1] 的基础上加下一行输出的长度,得到 result[i];而输出的具体内容,只需要接在上一行输出的后面存储即可。

表 2 - core.dll API 约定

项目意义
words传入读入的单词,每一个元素是单词首的指针,全部为小写,每个单词以 ‘\0’ 结尾
len传入 words 中元素的个数
results保存计算结果。要求调用者开辟 128 MB 的内存,并将首地址赋值给 result[0]。正常的计算结果下,每一个元素均为一行输出的首地址,每行以 ‘\0’ 结尾。若计算过程中出现异常,整个数组不变(无法输出);若整体输出的长度大于 128 MB,则仅保存小于等于 128 MB 部分的完整的行(超出部分无法取得);若返回值大于 20000,则整个数组不改变(无法输出)
head传入单词链首字母,必须是小写。若不要求首字母,则传入 NULL
tail传入单词链尾字母,必须是小写。若不要求首字母,则传入 NULL
allow_circ是否允许存在循环
返回值大于等于 0 时,表示结果有效,此时 result 中每一个元素表示一个输出行;若结果为 -1,表示在不允许出现循环的要求下,输入文本可构成循环(异常);若结果大于 20000,则超出数量限制,results 数组整个不变

Solution 中有 4 个与 core 中 API 对应的 API(见下方),这是计算真正开始的地方。

//-n
int Solution::genAllChain(std::vector<std::string> &result)

//-w -h [head] -t [tail] -r no -h than head=='\0' no -t than tail=='\0'
int Solution::genMaxWordChain(std::vector<std::string> &result,
        char head, char tail, bool allowCircle)

//-m
int Solution::genMaxWordChainNotSame(std::vector<std::string> &result)

//-c -h [head] -t [tail] -r
int Solution::genMaxCharChain(std::vector<std::string> &result, 
        char head, char tail, bool allowCircle)

为了便于开发,我们对传入参数和返回值进行了约定,见表 3。

表 3 - Solution 接口设计约定

项目意义
result保存单词链的结果,以引用类型传入。每一个元素表示一行输出
head单词链首字母,必须是小写。若不要求首字母,则传入 NULL
tail单词链尾字母,必须是小写。若不要求首字母,则传入 NULL
allowCirc是否允许存在循环
返回值大于等于 0 时,表示结果有效,此时 result 中每一个元素表示一个输出行;若结果为 -1,表示在不允许出现循环的要求下,输入文本可构成循环(异常)

core 部分仅需要将输入参数转化为 Solution 的 API 可以接受的形式,并处理相关异常和返回值即可。具体而言,对接方式如下:

  • 将输入的单词数组(char ** 类型)全部转为 vector,其中大小写和重复问题,在传入 core 之前已经解决
  • 调用 Solution 构造函数,建立单词图 graph
  • 新建 vector result_vec 用以保存 API 输出结果
  • 调用计算 API,传入参数,并获取返回值
  • 判断返回值。返回值在 0 至 20000 时,开始将 result_vec 中的内容逐个拷贝到 result[0] 所指向的空间,并更新 result 数组,直到拷贝完成或该内存空间无法存入新行,然后返回总行数。若有非法成环情况,则返回错误编码;若输出行数过多,则仅返回总行数。

在 Solution 的 API 中,对参数进行分类,按需进行循环检查,然后调用 Graph 中具体的实现,或抛出异常。

以获取最大单词数的单词链(gen_chain_word)为例如,整个执行过程见图 2。该过程先转化输入单词为约定形式,然后实例化 Solution 并调用相应 API。Solution API 中,若允许成环,则直接调用 Graph API,否则先进行循环检查,再计算或返回异常。Solution API 返回后,core 根据返回值判断,正常情况下按照约定转化输出结果,否则返回异常值。

在这里插入图片描述

图 2 - 计算最大单词数单词链 API

UML 图(独立)

请添加图片描述

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

记录在改进计算模块性能上所花费的时间,描述你改进的思路,并展示一张性能分析图(由VS 2019的性能分析工具自动生成),并展示你程序中消耗最大的函数。(3’)

看 Design by Contract,Code Contract 的内容,并描述这些做法的优缺点,说明你是如何把它们融入结对作业中的。(独立)

当程序满足一些约定好的最基本需求时才进行运行,否则直接拒绝运行。

  • 优点:当其他模块调用该模块时,一旦不满足即会跳出,可以很快发现问题所在。
  • 缺点:稍微超出一点边界即不可运行,边界兼容性低,增加了工作量和工作难度。

在构建边时,只有小写字母可以被正确处理,一旦发现非小写字母程序将运行错误。

计算模块部分单元测试展示。

利用vs的单元测试模块,我们只需要填写要测试的函数和返回的正确答案即可。如下是我们的测试代码:

TEST_METHOD(TestMethod1) {
		    int len = 4;
		    char *words[] = {"woo", "oom", "moon", "noox"};
		    int ret = 6;
		    char *result[] = {
		            "woo oom",
		            "moon noox",
		            "oom moon",
		            "woo oom moon",
		            "oom moon noox",
                    "woo oom moon noox"
		    };
		    char *_result_buf = (char *) malloc(0x1000);
		    char **_result = (char **) malloc (sizeof (char *) * 100);
		    _result[0] = _result_buf;
		    int _num = gen_chains_all(words, len, _result);
		    Assert::AreEqual(ret, _num);
		    vector<string> ans_vec, ret_vec;
		    for (int i = 0; i < ret; i++) {
		        ans_vec.emplace_back(result[i]);
		        ret_vec.emplace_back(_result[i]);
		    }
		    sort(ans_vec.begin(), ans_vec.end());
		    sort(ret_vec.begin(), ret_vec.end());
		    for (int i = 0; i < ret; i++) {
		        Assert::AreEqual(ans_vec[i], ret_vec[i]);
		    }
		}

我们对每一种可能出现的参数组合都构造了相应的样例,同时也对一些需要报错的情况做了样例构造。最终有13组样例,下面是代码覆盖率:
在这里插入图片描述

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

计算部分最为显著的异常为输入数据非法构成循环的情况。我们定义错误编码

// core.h
const int CORE_ERROR_ILLEGAL_CIRCLE = -1;

这种异常情况最先由 Solution API 进行检测(以最大单词数量为例),在不允许循环的情况下,先检查是否有环,若有,则直接返回 -1。只有在正确的情况下,才进行后续计算。

// Solution.cpp
int Solution::genMaxWordChain(std::vector<std::string> &result,
        char head, char tail, bool allowCircle) {
    if (allowCircle){
        return graph.genMaxWordChainWithCircle(result, head, tail);

    }
    else {
        if (checkCircle()) return -1;
        return graph.genMaxWordChain(result, head, tail);
    }
}

返回异常值后,在 core.dll 中进行判断,若有异常,则输出异常提示,并直接将异常值传递给调用者。

// core.cpp
int gen_chain_word(char* words[], int len, char* result[], 
        char head, char tail, bool allow_circ) {
    Solution solution(convert_words(words, len));
    vector<string> result_vec;
    if (solution.genMaxWordChain(result_vec,head,tail,allow_circ) == -1) {
        fprintf(stderr, "There is a circle in the chain.\n");
        return CORE_ERROR_ILLEGAL_CIRCLE;
    }
    return convert_result(result_vec, result);
}

应对这种情况,我们设计了单元测试样例:在不允许循环的情况下,给出的单词文本可以构成循环。样例如下:

// UnitTest.cpp
TEST_METHOD(TestMethod12) {
	int len = 3;
	char* words[] = { "ab", "xyz", "ba" };
	int ret = CORE_ERROR_ILLEGAL_CIRCLE;
	char* _result_buf = (char*)malloc(0x1000);
	char** _result = (char**)malloc(sizeof(char*) * 100);
	_result[0] = _result_buf;
	int _num = gen_chain_word(words, len, _result, 0, 0, false);
	Assert::AreEqual(ret, _num);
}

界面模块的详细设计过程。

CLI

对于命令行界面,在用户体验方面最重要的是需要给与足够友好的使用提示和错误提示。因此,我们设计了如下的使用提示,当用户键入 -? 选项时即可给出提示。

Usage:
  Wordlist <function> <filename> [options]

Functions:
  -n             Get the total number of word chains.
  -w             Get the word chain with the most words.
  -m             Get the word chain with the most words, where the
                 first letter of each word cannot be repeated.
  -c             Get the word chain with the most letters.

Options:
  -h <head>      Specify the first letter of the chain.
  -t <tail>      Specify the last letter of the chain.
  -r             Allow implicit word circles.

对于开发者,很重要的工作是进行命令行参数的切分。对此,我们专门设计了 OptParser 模块来读取参数,该模块仿照 GNU Linux 开源库中的参数读取部分,但是更加适配 C++,可移植性更强。我们指定了对 -n -w -m -c -h -r 选项的读入以及相关参数提取,同时对参数的合法性进行检查。若用户给出参数有误,如缺少功能指定、缺少输入文件等,都会给与相应的错误提示,并给出上述的使用方法。例如,OptParser 中参数分割的代码如下:

int OptParser::next_opt() {
    argv_index++;
    if (argv_index == argc) {
        return 0;
    }
    if (argv[argv_index][0] == '-') {
        char c = argv[argv_index][1];
        if ('?' == c || 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') {
            if (options[c] == 0 && argv[argv_index][2] == 0) {
                param = "";
                return c;
            }
            if (options[c] == 1) {
                if (argv[argv_index][2] == 0) {
                    argv_index++;
                    if (argv_index == argc) {
                        fprintf(stderr, "Missing option for -%c.\n", c);
                        if (exit_on_error) {
                            exit(-1);
                        } else {
                            return -1;
                        }
                    }
                    param = string(argv[argv_index]);
                } else {
                    param = string(argv[argv_index] + 2);
                }
                return c;
            }
            if (options[c] == 2) {
                if (argv[argv_index][2] == 0) {
                    fprintf(stderr, "Missing option for -%c.\n", c);
                    if (exit_on_error) {
                        exit(-1);
                    } else {
                        return -1;
                    }
                }
                param = string(argv[argv_index] + 2);
                return c;
            }
        }
    }
    // param directly given
    param = string(argv[argv_index]);
    return 1;
}

调用者可在初始化 Parser 后,通过循环来读取其中的参数。具体代码太长,在此不便贴出,可参见 Gitee:src/opt_parser.cpp、src/Wordlist.cpp#L126。

当用户给出了合法的参数,CLI 程序将会寻找输入文件并进行检查。检查合法后,将会读入文件中的所有单词,将单词全部转为小写,并去重(见 Gitee:src/Wordlist.cpp#L68)。

完成上述步骤后,加载 core.dll,并根据参数调用相关 API。

API 调用完成后,读取返回值,判断异常情况。正常情况下,会将结果输出至 stdout 或 Solution.txt。若存在异常,则给与错误提示(本程序中,错误提示在 core.dll 中就已经输出至 stderr)。

GUI

在进行GUI设计时,我们首先对命令行参数进行了可视化的设计,使得GUI界面可完整实现所有正确的参数组合。最终界面设计如下:
在这里插入图片描述
用户可选择从文件中导入或手动复制到输入框中:
在这里插入图片描述
设置好相应的参数后点击计算即可得到答案,也可将答案导出保存至文件。
在实现上,我们采用了QT作为GUI框架,首先将上述的可视化界面通过QT的设计器摆好,之后对3个按钮分别进行点击函数的实现:
如计算按钮的点击函数:

void MyGUI::startCalc() {
    const int MAX_WORD_NUM = 10020;
    const int MAX_CHAIN_NUM = 20020;
    char head = ui->head->currentIndex()>0 ? ui->head->currentIndex() + 'a' - 1 : '\0';
    char tail = ui->tail->currentIndex()>0 ? ui->tail->currentIndex() + 'a' - 1 : '\0';
    bool isC = ui->iscircle->isChecked();
    int func = ui->type->currentIndex();
    QString s = ui->input->toPlainText();
    char* word_buf = (char*)malloc(0x4000000);
    char** words = (char**)malloc(sizeof(char*) * MAX_WORD_NUM);
    int n_words;
    char *result_buf = (char *) malloc(0x10000000);
    char **result = (char**) malloc (sizeof (char*) * MAX_CHAIN_NUM);
    result[0] = result_buf;
    if (!result) {
        QMessageBox::warning(nullptr, "111", "111");
    }
    int n_return;
    HMODULE core = LoadLibraryA("core.dll");
    n_words = Qstring2words(s,word_buf, words);
    if (func == 0) {
        if(head!='\0'||tail!='\0'||isC){
            QMessageBox::warning(this, "参数选择有误", "-n不可与-h、-t、-r共同使用");
        }
        else{
            n_return = gen_chains_all(words,n_words,(char**)(void*) result);
        }
    }else if(func == 1){
        n_return = gen_chain_word(words, n_words, result, head, tail,isC);
    }else if(func == 2){
        n_return = gen_chain_char(words, n_words, result, head, tail, isC);
    }else if(func == 3){
        if(head!='\0'||tail!='\0'||isC){
            QMessageBox::warning(this, "参数选择有误", "-m不可与-h、-t、-r共同使用");
        } else
        n_return = gen_chain_word_unique(words, n_words, result);
    }
    if(n_return<0) {
        QMessageBox::warning(this, "文本中有单词环", "非-r时不可包含单词环");
        return;
    }
    QString res;
    if(func == 0){
        res = QString::number(n_return);
        res.append('\n');
    }
    for (int i = 0; i < n_return; i++) {
        res.append(result[i]);
        res.append('\n');
    }
    ui->output->setPlainText(res);
    FreeLibrary(core);
    free(words);
    free(word_buf);
    free(result);
    free(result_buf);
}

实际上是将之前写好的core进行了调用,将qt中的Qstring转为core的输入格式即可,然后调用相关函数。之后再将结果显示在输出框中。

界面模块与计算模块的对接。

CLI

CLI 与计算模块的对接在前文均已提到,主要是满足了表 2 的约定。具体的细节为:

  • 读入单词时,开辟连续的空间(64 MB)存储单词内容,并新开辟一个 char *words[] 数组,每个元素存储单词首地址,并保证去重、转为小写,对于存在的首位字母等参数,也转为合法形式
  • 开辟 128 MB 空间存储结果,并另开一个 char *words[] 数组,将首地址赋给 result[0]
  • API 调用完成后,检查返回值。若返回值在 0 至 20000,则正常输出 result 中指向的单词链;否则,不予输出,并给出相应的错误提示

满足上述实现要求后,CLI 程序即可与 core.dll 成功对接。

GUI

GUI主要是调用了core中的方法,对接时满足表2的约定,与CLI类似,先开辟相应大小的空间,将参数、文本等传入计算模块,再获得结果。
另外错误处理的对接也是相同的,若发现计算模块返回错误值,则GUI也会跳出相应的提示。

描述结对的过程

结对编程采用线下的形式进行(图 3),地点通常在五教 305 或新主楼 G 座 5 楼。
在这里插入图片描述
在结对编程过程中,先商定需求,例如,采用 C++ 语言、CMake 框架。然后对每个人的开发内容进行分工(考虑到工作量实在太大,全程采用一个人写一个人看的方式难以如期完成,仅在关键部分采用这种方式),本人主要负责算法部分、GUI部分。然后约定各个模块对接的契约,如表 2、表 3 所示的接口对接规则,进行开发。在遇到问题或 bug 时,会二人共同解决,提高 debug 效率。

结对编程的优点和缺点。(独立)

结对编程:

  • 优点:可以有效减少一个人写代码时出现的错误,因为另一个队友将看着你完成代码。
  • 缺点:
    • 有可能一个人的实现细节和另一个人的不一样,可能导致一些分歧,浪费统一细节的时间(实际上两种细节都是正确的)
    • 两个人找可以讨论写代码的地方非常浪费时间,新主楼经常没有位置。
冯旭杰
优点1、对cpp非常熟悉,搭建总体框架时非常快速
2、熟练掌握cmake,解决了dll的生成等在我看来比较奇怪的问题
3、写代码效率高,结对时准时到场不迟到。
1、乐于学习新知识,喜欢挑战自己
2、写算法某种意义上比较熟练,可快速完成算法代码
3、按时完成ddl
缺点偶尔没看清题目要求(?)对cpp不熟悉,对cpp动态链接等知识非常欠缺,导致背大锅。
举报

相关推荐

0 条评论