软件工程第三次作业 结对项目-最长英语单词链

软件工程第三次作业 结对项目-最长英语单词链

项目 内容
这个作业属于哪个课程 2023北航敏捷软件工程
这个作业的要求在哪里 结对项目-最长英语单词链
我在这个课程的目标是 学习现代软件开发模式与流程,提高个人能力与团队写作能力
这个作业在哪个具体方面帮助我实现目标 通过参与结对编程,在实践与合作中领悟软件工程真谛

1. 项目地址

github地址:pangrj/LongestWordChain: 2023 SoftwareEngineering Course in BUAA (github.com)

2. PSP表格

见第15节

3. 接口设计方法

3.1. Information Hiding

​ 信息隐藏很像面向对象的开发思路,是将计算机程序中最有可能发生变化的设计决策隔离开来的原则,从而保护程序的其他部分在设计决策发生变化时不被广泛修改。这种保护包括提供一个稳定的接口,保护程序的其余部分不受实现(其细节可能会改变)的影响。

​ 本次作业中,从文件的角度实现了信息隐藏,计算核心对外只提供了三个接口,隐藏了这些接口的具体实现,外部函数只能通过调用这些已经规定好的接口来计算,而不知道这些接口的具体实现方式。

3.2. Interface Design

​ 接口用于模块之间的交互。精心设计的接口可以避免“抽象泄露”,提升编码和运行效率,给人良好的开发和使用体验。

​ 本次我们按照课程组定义的接口进行设计,也方便后续松耦合与其他组进行交换测试。

int gen_chains_all(char* words[], int len, char* result[]);

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

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

3.3. Loose Coupling

​ 松耦合的设计理念解开了模块之间的紧密联系,既方便对核心模块进行集中测试,在出问题时也只用修改对应的模块,不会牵扯到整个项目。

4. 计算模块实现

​ 首先是图的结构:每一个单词Word作为图的节点,对两个单词a、b,若a的尾字母与b的首字母相同,则在ab之间连一条a指向b的有向边。

​ 计算模块core实现了如下三个API:

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

​ 其中,words存放输入的单词数组,len为单词数组长度,result用来存放结果单词链,head为规定单词链首字母,tail为规定单词链尾字母,reject为规定单词不允许首字母,以上三个为 '\0' 时表示不做要求,且初始值为 '\0'。enable_loop用来约束是否允许存在环。

​ 我们采用拓扑排序的方式判断有无环,同时求出拓扑序用于后续。

​ 求全部单词链与带环情况采用暴力DFS运算,若不带环即图为DAG,此时采用动态规划快速求解。dp[i]表示以i节点为最后单词的单词链的最长长度,用rec来记录前驱结点。则初始化dp[i]为点权,当word情况下为1,char情况下为单词长度。状态转移方程为 dp[j]=max(dp[j], dp[i] + weight[j]),其中i为j的前驱结点。

​ head、tail、reject的处理方式则使用noUse数组进行标记。为了最好的提高性能,reject在最开始便处理,标记相应单词noUse为true,相当于降低图的度;head则在开始调用函数时处理,减少了DFS的次数或动态规划的开始点;tail则只能在计算过程中处理。

5. 编译结果

6. 计算模块UML图

​ 我们采用面向过程的求解思路,以下是我们核心计算模块的函数调用UML图

7. 计算模块性能

我们采用运算较为复杂的-c -r来进行测试

可以看到dfs占用了大量的时间,于是我们针对dfschar进行优化,将可以放到循环外的语句外提,减少了循环节运算语句的数量。

8. Design by Contract 与 Code Contract

​ Design by Contract:要求软件设计者为软件组件定义正式的,精确的并且可验证的接口,这样,为传统的抽象数据类型又增加了先验条件、后验条件和不变式。

  • 优点:
    • 因为程序的行为已经被规定好了,开发者只需要根据契约进行开发,不需要考虑整个项目,使得开发更简单也更专注
    • 因为程序的行为必须要符合契约,所以只需要根据契约设计单元测试即可,这样方便进行测试
  • 缺点:
    • 契约必须要设计周全,否则得到的程序一定是错误的,并且问题难以靠单元测试发现,这只会浪费设计者和开发者的时间
    • 设计契约需要花费很多时间,并且设计时也不一定能考虑到实际开发中可能遇到的问题,可能开发效率不如直接进行开发

本次作业中,我们对core的三个API的设计与参数进行严格定义,但是并未采取形式化的表述,而是通过更为通俗的表述。并且通过维护文档function design.md 进行沟通。

​ Code Contract是VS提供的一种与语言无关的代码契约插件。能够帮助进行运行时检查、静态检查与文档生成。优点是可以帮助开发人员履行契约,避免了多余精力的耗费。我们编码过程主要在Clion完成,没有使用此查件。

9. 计算模块单元测试

我们使用Google的gtest作为单元测试的模板,分别对计算核心和异常处理进行了测试。

9.1 覆盖率

9.2 计算核心测试与结果

我们给计算核心设计了24个单元测试并全部正确通过

为了方便测试,我将重复的部分提取成函数方便快速设计测试

void test_gen_chain_word(const char* words[], int len, const char* ans[], int ans_len, char head, char tail, char reject, bool enable_loop) {
    char** result = (char**)malloc(10000);
    int out_len = gen_chain_word(words, len, result, head, tail, reject, enable_loop);
    ASSERT_EQ(ans_len, out_len);
    for (int i = 0;i < ans_len;i++) {
        if (result != nullptr) ASSERT_EQ(strcmp(ans[i], result[i]), 0);
    }
    free(result);
}

对应的测试单元

TEST(gen_chain_word, example_w) {
    const char* words[] = {"algebra", "apple", "zoo", "elephant", "under", "fox", "dog", "moon", "leaf", "trick", "pseudopseudohypoparathyroidism"};
    const char* ans[] = {"algebra", "apple", "elephant", "trick"};
    test_gen_chain_word(words, 11, ans, 4, 0, 0, 0, false);
}

9.3 异常测试与结果

我们给所有异常情况设计了14个单元测试,覆盖了所有异常,详细情况见下一章节

10. 计算模块异常处理

本程序设计了如下14种异常

异常情况 具体异常描述 报错输出
没有正确输入参数 检测到-n, -w, -c, -h, -j, -r以外的参数 "this argument is invalid!"
重复输入参数 同一个参数检测到两遍 "arg: " + arg + " repeat!"
-n 与其他参数冲突 检测到-n与其他参数一同使用 "-n can't be used with other arguments!"
-w与-c冲突 检测到-c与-w一同使用 "-w can't be used with -c!"
-h, -t, -j后续参数错误 检测到-h, -t, -j后续参数不是单个字母 "arg: " + arg + " following argument is wrong!"
-h, -t, -j后续参数缺失 检测到 -h, -t, -j后续没有参数 "arg: " + arg + " missing following argument!"
功能参数缺失 没有检测到-n, -w, -c "need -n or -w or -c!"
没有获取到合法的文件名 没有文件名或者文件名不以.txt结尾 "don't get valid filename!"
重复输入文件名 检测到多个文件名 "filename is repeat!"
打开文件失败 该文件不存在或不可读 "opening file fail!"
输入超过10000个词 无环情况下输入超过了10000个词 "words are more than 10000!"
有环情况下输入超过100个词 有环情况下输入超过了100个词 "words are more than 100 when chains has circle!"
输出超过20000行 result结果大于20000 "results are more than 20000 chains!"
在不允许有环的情况下输入有环图 没有检测到-r但是图是有环图 "the chain has circle without -r!"

以以下几组异常处理逻辑与对应的单元测试为例

该段代码包含了 -h, -t, -j后续参数错误-h, -t, -j后续参数缺失重复输入参数这三个异常的抛出过程

if (is_h == false) {
                    is_h = true;
                    int i_next = i + 1;
                    if (i_next == argc) {
                        throw invalid_argument("arg: " + arg + " missing following argument!");
                    }
                    string arg_next = argv[i_next];
                    if (arg_next.length() == 1 && isalpha(arg_next[0])) {
                        h_char = tolower(arg_next[0]);
                    } else {
                        throw invalid_argument("arg: " + arg + " following argument is wrong!");
                    }
                    i++;
                } else {
                    throw invalid_argument("arg: " + arg + " repeat!");
                }

在main函数中捕获异常并处理

int main(int argc,char* argv[]) {
    try {
        main_serve(argc, argv);
    } catch (invalid_argument const& e) {
        cerr << e.what() << endl;
    }
    catch (logic_error const& e) {
        cerr << e.what() << endl;
    }
    catch (runtime_error const& e) {
        cerr << e.what() << endl;
    }
    return 0;
}

对应的单元测试

10.1 -h, -t, -j后续参数错误

TEST(main_serve, example_h_wrong) {
    try{
        const char* args[] = {"Wordlist.exe", "-w", "-h", "aa", "test.txt"};
        main_serve(5, args);
    } catch(invalid_argument const &e){
        ASSERT_EQ(0, strcmp("arg: -h following argument is wrong!", e.what()));
        return;
    }
}

10.2 -h, -t, -j后续参数缺失

TEST(main_serve, example_h_miss) {
    try{
        const char* args[] = {"Wordlist.exe", "-w", "-h"};
        main_serve(3, args);
    } catch(invalid_argument const &e){
        ASSERT_EQ(0, strcmp("arg: -h missing following argument!", e.what()));
        return;
    }
}

10.3 重复输入参数

TEST(main_serve, example_repeat_arg) {
    try{
        const char* args[] = {"Wordlist.exe", "-n", "-n", "test.txt"};
        main_serve(4, args);
    } catch(invalid_argument const &e){
        ASSERT_EQ(0, strcmp("arg: -n repeat!", e.what()));
        return;
    }
}

11. 界面模块设计

​ 界面模块采用vue设计开发,所采用的技术栈如下:

  • Vue 3
  • Element - Plus
  • 注册表脚本reg文件
  • XMLHttpRequest

​ 设计风格以简约为主,效果如下:

​ 功能实现:

  • 输入可以通过文件导入,也可以手动输入,且两者可同步使用,导入文件后可自动更新输入框。展示如下:

  • 功能设定选择通过按钮实现,且选择首字母限制后方可进行输入。基本异常情况也会给出错误提示:

  • 结果展示如下:

12. 界面模块与计算模块对接

​ 对接我们采用的vue+cli的方式,熟悉分析选项异常,随后处理好输入输出后,调用程序。调用程序通过html中的a元素的href属性,首先通过reg文件注册url,后通过href调用本地exe文件,达到调用程序的目的。

附加任务- 交换核心:

​ 与我们交换核心的小组学号为20373284、19375201。

​ 运行效果如下:

​ 且我们的API设计完全相同,故较容易实现互换。

13. 结对过程

主要在新主楼F座3楼进行结对编程。

14. 结对编程

14.1. 优点

  • 结对编程能使两人互补,弥补自己不会的技术,合力做出高质量产品
  • 结对编程还能起到互相督促的作用,防止划水
  • 结对编程两人审视一份代码,也更容易发现代码的问题,快速debug

14.2. 缺点

  • 结对编程过程中需要不断与队友沟通,有时会导致一定的效率下降
  • 结对编程会带来一定的压力
  • 在校内开展结对,有场地、时间的限制

14.3. 队员优缺点分析

​ 本次结对的队员为庞睿加同学。

队员 优点 缺点
庞睿加 1. 对基本算法熟悉
2. 对Vue前端相对熟练
1. 对于配置环境工作很头疼
2. 高级算法知识相对不够充分,
3. 当陷入难题后工作效率降低
朱彦安 1.学习新工具较快
2.测试较为全面,发现bug与debug相对熟练
1.不懂前端知识
2.比较拖沓

15. PSP表

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 60 60
· Estimate · 估计这个任务需要时间 60 60
Development 开发 2250 2300
· Analysis · 需求分析(包括学习新技术) 300 400
· Design Spec · 生成设计文档 60 60
· Design Review · 设计复审 120 120
· Coding Standard · 代码规范 30 30
· Design · 具体设计 120 120
· Coding · 具体编码 900 1200
· Code Review · 代码复审 240 240
· Test · 测试(自我测试,修改代码,提交修改) 300 420
Reporting 报告 180 180
· Test Report · 测试报告 120 120
· Size Measurement · 计算工作量 30 30
· Postmortem & Process Improvement Plan · 事后总结,并提出过程改进计划 30 30
合计 2490 2760
posted @ 2023-03-19 23:44  silhouette-  阅读(36)  评论(0编辑  收藏  举报