结对作业-最长英文单词链
项目 | 内容 |
---|---|
这个作业属于哪个课程 | 2023年春季软件工程(罗杰 任健) |
这个作业的要求在哪里 | 个人作业-软件案例分析 |
我在这个课程的目标是 | 学习软件工程方法,提升解决复杂工程问题的能力 |
这个作业在哪个具体方面帮助我实现目标 | 通过分析对比不同软件的设计,从用户的角度理解一个软件产品的关注要点,帮助我们更好理解软件工程设计方法、原则 |
文章目录
1、教学班级与项目地址
- 教学班级:周四上午
- 命令行项目地址:https://gitee.com/wit23/word-chain
- Gui项目地址:https://github.com/WIT23/pair_work_gui
2、PSP表
SP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 30 |
· Estimate | · 估计这个任务需要多少时间 | 30 | 30 |
Development | 开发 | 1460 | 1590 |
· Analysis | · 需求分析 (包括学习新技术) | 150 | 120 |
· Design Spec | · 生成设计文档 | 90 | 60 |
· Design Review | · 设计复审 (和同事审核设计文档) | 60 | 60 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 60 | 90 |
· Design | · 具体设计 | 60 | 60 |
· Coding | · 具体编码 | 800 | 900 |
· Code Review | · 代码复审 | 120 | 150 |
· Test | · 测试(自我测试,修改代码,提交修改) | 120 | 150 |
Reporting | 报告 | 350 | 350 |
· Test Report | · 测试报告 | 30 | 30 |
· Size Measurement | · 计算工作量 | 20 | 20 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 300 | 300 |
合计 | 1840 | 1970 |
3、接口设计方法
Information Hiding
Information Hiding是结构化设计和面向对象设计的基础,核心思想是代码模块应该定义良好的接口来封装,这些模块的内部结构应该是程序员的私有财产,外部不可见。
- 类中数据应用private修饰,只能通过函数进行访问
- 类与类之间通过接口类访问
- 多层程序结构中,在层与层之间加入接口层
在结对作业的实现中,我们参考了information hiding的设计思想隐藏复杂度。在输入层和计算层之间,我们设置了gen_chains_x
等系列函数,将更加核心的深度优先搜索算法隐藏,调用者只需要将规定的函数gen
函数中,不必关注内部的图算法如何实现。
Interface Design
Interface Design即接口设计,在面向对象课程中也有提及,主要包括:
- 单一职责原则(Single Responsibility Principle)
- 开闭原则(Open Closed Principle)
- 里氏替换原则(Liskov Substitution Principle)
- 迪米特法则(Law of Demeter)
- 接口隔离原则(Interface Segregation Principle)
- 依赖倒置原则(Dependence Inversion Principle)
在结对作业的实现中,我们参照了单一职责原则和迪米特法则和开闭原则。
- 单一原则:将整个任务分为了输出、计算、输出三个相互独立的模块。
- 迪米特法则:core模块只向外提供三个gen_chain_…的函数,内部复杂的逻辑结构对调用者透明。
- 开闭原则:计算模块提供了扩展的方法,但是对修改关闭。在第一阶段到第二阶段到迭代开发过程中,就是在尽量不修改原有代码的情况下进行扩展。
Loose Coupling
Loose Coupling即松耦合,在面向对象设计与构造课程中老师就反复强调过“高内聚、低耦合”的设计方法,各个模块内部聚合程度高,减少对其他模块的依赖。这种设计方法和提高程序模块的可复用性、可移植性。
在结对作业的实现中很好的遵循了这一原则~~(作业要求之一)~~。我们将整个项目拆分为输入模块、输出模块以及复杂计算的核心模块。在Gui开发中,只需要和Core模块的动态链接库文件对接,即可完成命令行程序的可视化,不受输入模块、输出模块的影响。同时,在和其他小组交换Core模块时,也能够很迅速的完成前后端互换。
4、计算模块接口设计与实现
接口设计
在学习了前文提到的接口设计方法后,我们对计算模块(Core层)的接口进行了重新设计。我们在输入输出层和Core层之间加了一个中间层,包括三个gen
函数:
int gen_chains_all(const char *words[], int len, char *result[]);
int gen_chain_word(const char *words[], int len, char *result[],
char head, char tail, char reject, bool enable_loop);
int gen_chain_char(const char *words[], int len, char *result[],
char head, char tail, char reject, bool enable_loop);
中间层的存在具有着如下作用:
- 隐藏算法内部复杂度
- 提供标准化接口
- 方便交换前后端Core
中间层会将参数传递到一个engine
函数中。engine
函数会根据参数,进行检查并抛出异常,随后使用DFS深度优先搜索结果。
核心算法
生成单词链的核心算法主要基于深度优先搜索,函数签名如下:
int DFS(char* result[], //结果指针
char head, //首字母
char tail, //尾字母
char reject, //reject字母
bool weighted) //是否求解最长字符串
具体思路:遍历所有满足条件的单词,通过带回溯的递归进行深度搜索,当无法继续搜索时,判断当前经过的路径是否满足单词链的定义和参数要求,若满足则使用vector_to_result
保存结果,生成相应单词链。
5、编译器无警告截图
给出main文件和core文件的编译器无警告截图:
6、UML实体图
实际上,我们的程序更像是披着C++外皮的C语言
,除了使用了C++的容器类外,其他基本都是C语言的写法,没有使用到类和对象的概念。因此,下面给出Core核心函数的调用链图:
7、计算模块接口部分的性能改进
我们并没有在算法中做过多的性能优化,只是对对数据结构进行了一定的改进。在程序中,我们使用了较多的容器Vector等而非数组和结构体,提升计算速度的同时减少了代码工作量。
我们在一个全连通的单词序列(324个单词)中,使用-w和-r参数,计算程序花费的时间:
可以发现,程序总体的计算速度还是十分可观的。主要的时间花费在gen_chain_word
以及下层的engine
函数中,这两个函数承担了DFS递归调用的工作,因此会有很多耗时集中于此。
8、Design by Contract & Code Contract
Design by Contract即契约式编程(后文简称DbC)。这种方法要求软件设计者为软件模块定义正式、精确、可验证的接口。DbC包含三个关键字:
- 前置条件:调用者调用函数时必须传递正确的参数,否则函数不予执行直接返回
- 后置条件:函数保证能够结束,不会陷入循环
- 不变式:在函数的内部处理过程中,不变式可以为变,但在函数结束后,控制返回调用者时,不变式必须为真。这对于面向对象设计来说,类的任何实例,在调用函数之前和之后,不变式的值是不变的。
优点
- 在DbC中,使用者和被调用者地位平等,双方都要必须履行的义务和使用的权利,保证了双方的代码质量,提高了软件工程的效率和质量。
- 在开发过程中就严格按照契约执行,减少了后期测试和bug修复的压力。
- 提高了代码的可读性,更加利于团队开发。
缺点
- 对程序语言有一定要求,不是所有程序语言都很好地支持Assert机制。
- 带来了更大的更大的工作量,需要花费大量的时间制定合理、有效的契约。
- 如果后期需要对规格进行修改,会引起大量代码的改变,不利于维护。
实践
我们的结对编程作业是个相对较小的项目,团队成员也只有两人,因此,并没有花费太多的时间在契约指定上,只是采用了一些简单的规范来约束编码:
- Core模块:核心的算法模块,采用的是课程组简易的几个接口,我们在编码时规定外部调用这些函数时传递的参数必须要满足接口要求。模块内部首先会对参数进行检查,不符合要求会直接抛出异常。
- 结对作业主要包括输入模块、Core模块、输出模块,模块之间的输入、输出格式在模块分离时得到确定,模块之间的数据交互必须满足数据的正确性,否则抛出异常。
9、计算模块部分单元测试
在Clion导入了GoogleTest进行对Core模块中的三个核心函数进行单元测试。
测试样例构造
- 基础样例:主要为课程组**“结对需求”**文档中的样例。
- 普通样例:
- 链内有环的单词链
- 链内存在复合环的单词链
- 一条单词链和多个孤立的单词
- 两条长度的单词链 (+孤立单词)
- 两条有交点的单词链
- 两条长度不同的单词链(+孤立单词)
- 边界样例:
- 没有任何单词
- 只有一个单词
- 无法构成单词链的序列
- 所有单词组成的单词链
- 强度/运行时间测试:长文本测试
测试结果展示
Core模块的覆盖率为96%:
核心测试函数
bool cmp(char *x, char *y) {
if (strcmp(x, y) <= 0) {
return true;
} else {
return false;
}
}
void test_method(char arg,
const char *words[],
char *standard_results[],
int len_words,
int len_results,
char head,
char tail,
char reject,
bool enable_loop) {
char *results[100];
for (auto &result: results) {
result = nullptr;
}
int len;
switch (arg) {
case 'n':
len = gen_chains_all(const_cast<char **>(words), len_words, results);
break;
case 'w':
len = gen_chain_word(const_cast<char **>(words),
len_words, results, head, tail, reject, enable_loop);
break;
case 'c':
len = gen_chain_char(const_cast<char **>(words),
len_words, results, head, tail, reject, enable_loop);
default:
break;
}
ASSERT_EQ(len_results, len);
sort(results, results + len_results, cmp);
sort(standard_results, standard_results + len_results, cmp);
for (int i = 0; i < len_results; i++) {
EXPECT_STREQ(results[i], standard_results[i]);
}
}
10、计算模块部分异常处理
在实际的程序中,异常处理并不仅局限在Core模块中,一些必要的异常处理我们将其放在输入输出模块中。下面分别介绍IO模块、Core模块、Gui模块中的异常处理。
IO模块
-
命令行参数异常
通过抛出
invalid_argument
异常处理:- 参数重复
- 无参数/无必选参数
- 非字母参数
- -n不兼容其他参数
throw invalid_argument("no option specified"); throw invalid_argument("missing -n, -w or -c"); throw invalid_argument("conflicting option combinations"); throw invalid_argument("cannot combine -n with other options"); throw invalid_argument("multiple text files given: \"" + filename + "\" and \"" + arg + "\""); throw invalid_argument("invalid option -- '" + arg.substr(1) + "'"); throw invalid_argument("argument of option " + arg + " should be an alphabet, " + arg2 + " received"); throw invalid_argument("argument of option " + arg + " should be a single alphabet, " + arg2 + " received"); throw invalid_argument("option requires an argument -- '" + arg.substr(1) + "'"); throw invalid_argument("option duplicated -- '" + arg.substr(1) + "'");
测试样例
WordChain.exe test.txt WordChain.exe -n - w test.txt WordChain.exe -n test.txt test2.txt WordChain.exe -h a test.txt WordChain.exe -n -h test.txt WordChain.exe -n -n a test.txt
-
输入文件异常
通过抛出
runtime_error
异常处理:- 多文件路径
- 空文件
- 找不到该文件
- 文件不可读、非常规文件
throw runtime_error(filename + ": File does not contain words"); throw runtime_error(filename + ": File does not contain words"); throw runtime_error(filename + ": Cannot open as read-only"); throw runtime_error(filename + ": Not a regular file"); throw runtime_error(filename + ": No such file");
通过输入不存在的文件名进行测试。
Core模块
通过抛出logic_error
处理
-
首尾字母约束不合法(这部分在IO模块中已被处理)
-
单词文本隐含单词环:
测试样例:
words = {"pad", "dc", "cp"} / {"pad", "dc", "cap", "pap"} gen_chain_word(words, 3, ...) gen_chain_char(words, 4, ...)
Gui模块
Gui是使用python语言编写的,使用raise Exception
的方式抛出异常,并编写了error_info_display函数将错误信息以红色字体输出到“模拟控制台中“。为了简化gui的工作,gui中主要负责以下几个异常。
self.error_info_display("参数-n不支持任何可选参数")
self.error_info_display("txt文件不存在")
self.error_info_display("请选择唯一的必选参数")
由于ctypes不能接受来自c++的动态链接库抛出的异常,所有的异常都会导致程序崩溃。所以,在这里,我们假设输入的文本不会存在非法成环。
11、Gui模块的详细设计过程
为了提升使用者的用户体验,我们在完成Core核心的编写和测试后,使用python的pyqt5包设计了Gui页面,将程序的核心功能以图形化可交互的形式呈现出来。
页面设计
页面设计主要包括三个部分:
- UI绘制:将VBoxLayout和HBoxLayout嵌套到GridLayout中,将整个Gui页面分为了运行、参数、输入、输出四个部分,使用了qt中的QLabel, QPushButton, QLCDNumber, QLineEdit, QTextEdit, QWidget, QFileDialog等组件实现基本功能。
- 绑定事件:pyqt5包中提供了回调函数机制,对参数部分和运行按钮部分进行监听,接受用户请求。
- 结果反馈:使用了QTextEdit组件模拟控制台,错误/警告信息会以在组件中以红色文字显示,正常输出则会以黑色文字显示。
页面展示
整个Gui页面分为四个部分:
- 最上层为运行部分,包括运行按钮和显示运行时间的LED组件。
- 中间为参数部分,分为必选参数和可选参数两行。
- 下层为“控制台部分”,包括输入文件、输出文件路径的选择和输入、输出控制台。用户可以选择在“输入控制台”输入单词,也可以在上方的LineEdit中导入文件路径。输出部分同理,但无论是否导出文件,都会在“输出控制台”中显示结果。
12、Gui模块与计算模块的对接
对接过程
实际上,Gui模块取代了命令行程序中的输入和输出模块,保留了Core模块。所以,对接工作主要是将用户在Gui中输入的数据传递到core.dll动态链接库中的函数,再将结果反馈到Gui中。主要流程图:
核心代码
由于Gui是使用python语言编写的,调用dll动态链接库并不方便,尤其是参数还有指针的情况下。所以在此,给出使用python语言的ctypes包调用dll库的核心代码:
from ctypes import *
def core(arg, words, head, tail, reject, enable_loop):
results = []
dll = CDLL(dll_path)
lens = len(words)
c_result = (c_char_p * lens)
c_words = (c_char_p * lens)
c_len = c_int(lens)
for i in range(lens):
c_words[i] = c_char_p(str(words[i]).encode())
if arg == 'n':
count = dll.gen_chains_all(c_words, c_len, c_result)
else:
c_head = c_char(head.encode())
c_tail = c_char(tail.encode())
c_reject = c_char(reject.encode())
c_enable_loop = c_bool(enable_loop)
if arg == 'w':
count = dll.gen_chain_word(c_words, c_len, c_result,
c_head, c_tail, c_reject, c_enable_loop)
else:
count = dll.gen_chain_char(c_words, c_len, c_result,
c_head, c_tail, c_reject, c_enable_loop)
for i in range(count):
results.append(c_result[i].value)
return count, results
13、结对过程描述
我们在这次结对编程中主要采用线上的方式进行。在交流完前期的项目分析和接口设计后,我们分工进行编码,对于编码、测试中出现的问题,我们也及时通过微信进行交流。我们两个人的分工较为明确,刘骁同学主要负责GUI页面的编写和单元测试,高培森同学主要负责模块化和异常处理以及具体的算法实现。
14、优缺点
结对优缺点
- 优点
- 结对双方可以进行优势互补,例如一人擅长算法,则可以让Ta承担更多的算法开发工作
- 前期的交流讨论能够让整体设计考虑得更加全面和充分,易于产生更优的方案,也降低了后期重构的概率。
- 结对编程,一人开发,一人不间断复审,可以很好地提高代码质量,降低bug出现的概率。
- 结对编程也能够提高代码的可读性,实现的代码会是双方都认可、都可读的。
- 缺点
- 正如教材中提到的:“我习惯一个人写程序,不喜欢被人盯着工作,这样我不自在,无法工作”,结对中也确实存在这一问题。
- 两人如果沟通出现分歧,或在编程中双方的思想存在不一致,反而会影响整体的效率。
- 结对编程中很多的思想、细节是需要两个人点头同意的,而不是像个人编程那样自己觉得可以那就这样写/改,有时反而会过犹不及,反而降低的整体效率。
- 如果结对双方的水平差距过大,则会产生大多数任务均由一个人完成的情况出现。
结对双方优缺点分析
刘骁 | 高培森 | |
---|---|---|
优点 | -熟悉使用git团队开发的流程 - 熟悉面向对象编程,上手较快 - 能够较好地协调两人的任务 | -算法能力较强,bug较少 -沟通能力强,协调任务流畅 -熟悉面向对象编程,任务上手较快 |
缺点 | - 欠缺考虑,容易出现bug - 构造测试样例的能力有待提升,测试花费了较多时间 | -有时会忽略题目要求的细节 |
15、实际花费时间
此部分已在第二部分的psp表中完成。
16、互换前后端
合作小组
肖圣鹏 | 20373358 | 咸永飞 | 19376156 |
---|
对接
我们互换了Core动态链接库,在Gui中进行运行。因为是采用了相同的gen
系列接口,所以我们的Gui代码几乎不需要做出任何改动,可以顺利跑通程序,完成正确性测试。
但是,由于我们的Core核心的头文件有所差异,导致我们需要为对方调整头文件,以适应其Gui程序的调用,并重新编译。(C/C++不跨平台的缺点之一)