单词链项目
项目 | 内容 |
---|---|
这个作业属于哪个课程 | 2023年北航敏捷软件工程 |
这个作业的要求在哪里 | 结对项目-最长英语单词链 |
我在这个课程的目标是 | 学习现代化的软件开发方法 |
这个作业在哪个具体方面帮助我实现目标 | 对结对编程进行实践,对软件开发方法有了新的认识 |
项目地址
-
教学班级:周四班
-
项目地址:https://github.com/WAN-M/pair_programming2023.git
-
结对成员:19375035
PSP
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 50 |
· Estimate | · 估计这个任务需要多少时间 | 60 | 50 |
Development | 开发 | 2610 | 2740 |
· Analysis | · 需求分析 (包括学习新技术) | 120 | 150 |
· Design Spec | · 生成设计文档 | 120 | 90 |
· Design Review | · 设计复审 (和同事审核设计文档) | 60 | 90 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | 10 |
· Design | · 具体设计 | 600 | 180 |
· Coding | · 具体编码 | 900 | 900 |
· Code Review | · 代码复审 | 180 | 120 |
· Test | · 测试(自我测试,修改代码,提交修改) | 600 | 1200 |
Reporting | 报告 | 210 | 360 |
· Test Report | · 测试报告 | 120 | 300 |
· Size Measurement | · 计算工作量 | 60 | 30 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 30 |
合计 | 2880 | 2820 |
接口设计相关方法
Information Hiding
Information Hiding指“信息隐藏”,类似面向对象设计中“封装”的思想。使用Information Hiding的设计会使程序转化成一个个对象之间的关系,将高层次复杂的问题分解后逐步击破。
在我们的项目设计中有以下方面使用了Information Hiding:
- 建图用图采用面向对象方式,Graph对象拥有很多Node对象,而Node对象存储Edge对象知道与自己相连的Node。它们只需要完成自己的功能,对外暴露接口即可。
- 我们整个项目具有core,GUI,CLI模块,它们彼此使用统一接口交互,不关心彼此实现方式。
Interface Design
Interface Design指“接口设计”。接口设计是模块之间Information Hiding的一种方式,通过统一的接口达到模块之间的信息隐藏,使功能与具体实现方式分离,提高项目的可扩展性。
同样,我们的项目中各模块之间的交互采用了Interface Design。
Loose Coupling
Loose Coupling指“松耦合”。松耦合是满足Information Hiding和Interface Design之后的必然好处。
计算接口的设计与实现
我们根据课程组的接口设计建议,dll提供如下三个接口:
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);
参数说明:dll默认传入去重后的char **类型的words,以及其他需要的参数,若head,tail和reject未指定则传入0。
算法设计&&实现
对于本次单词链相关任务,单词之间以首尾字母构成关系,具有明显有向图的特征。因此我们将单词链任务转换成有向图上的算法,实现起来直观简洁。
我们小组尝试了两种建图方式,一种是每个单词作为结点,另一种是每个字母作为结点,边为单词。其实我们感觉两种建图方式对于不优化的搜索效率基本一样,但后者有一个明显的优势,也是我们算法实现中贪心策略体现的重要部分:在结点为字母的图中,点到点的搜索优先选择权值最大的。比如abcd de dddde这一样例,显然在搜索d到e的路径时,可以直接选择最长的ddde,而不用在考虑较短的de。这一性质在实际搜索时可以剪枝相当一部分,不用搜索各种路径,大幅提升性能。我们同样采用简洁直观的实现方式:对于每个结点,用容器(我们使用的list)分开存储其到下一结点的边,存完所有边后对容器按权值从大到小排序,且维护一个指针指向容器开始。在搜索时,每次只遍历指针指向的边,遍历一条边后指针后移,回溯时指针前移,这样可保证搜索正确性且每次都只遍历当前最长边。
搜索时要不断维护当前结果和已知最长结果,同样为了减少中间结果拷贝开销,我们以string &传递,每次修改同一string。
在具体实现以字母为点的算法时,考虑到可以以多个点为起点或多个点为终点,我们在图中引入虚点SOURCE和TARGET作为超级源、超级汇。这样就确保了起点终点唯一,算法代码实现起来更为简洁。
所有单词链算法是我们比较纠结的点。若是只需要输出所有单词链个数,动态规划可以以O(n)的复杂度求解。但要输出所有路径则让人很难权衡。如果同样根据动态规划思路求解,则需要保存每个点到终点的单词链,这样对于新的路径产生时,可以维持O(n)的复杂度输出。但这样的内存复杂度则提升到指数级。因此,我们认为采取遍历所有路径一条一条输出的算法,将时间复杂度提高而非“以空间换时间”。
采用以字母为结点的建图方式还有一大好处,即无论图有环无环,在保证每次搜索搜最长边的前提下,都可采用同一搜索策略完成算法。而以单词为结点的建图方式,我们根据性能需要还图分为有环图和无环图分别处理,有环图暴搜,无环图通过负权边用spfa求得最长路,实现起来更繁琐复杂。
结构设计
由于我们小组最后采用基于以字母为结点的图的算法,这里只介绍相关设计。
首先,输入的参数个数较多,且-n和-w-c所需的参数不同,因此各参数在算法中如何传递需要解决。此外,建好的图也需要在算法直接流动,图的对象由谁保管也需要慎重设计。考虑到每次调用dll时,参数只有一种组合,图只有一个,我们小组决定在项目中以单例模式建立Global类,其中包含Graph对象和Parameter对象。这种设计相当于将图和参数信息设计为全局变量,项目任何地方需要可以直接查看。
算法实现部分我们融合在Solver类里。Solver类具有public静态方法solve供外界调用,然后Solver根据全局参数信息调用对应函数求解。
项目UML图如下:
计算模块接口部分的性能改进
由于我们在最初版中采用的以单词为结点的建图方式,后来在修改算法时需要大幅改动项目结构,耗时较多。加上后续不断确保正确性,我们在性能改进部分总耗时大约为15小时。
我们小组的性能改进策略如下:
- 采用以字母为结点的方式建图,同时确保每次遍历只遍历点对点间的最长边的贪心策略。
- 容器中存储指针,以减少拷贝开销。
- 搜索时最先走完自环。
-
优化前算法
最初版中采用的以单词为结点的建图方式,采用
Visual Studio性能探查器
,数据为随机生成与固定数据相结合。
排名靠前的为自动数据生成器相关代码,因此大部分CPU占用由自动数据生成器与系统调用代码组成,算法已经有较好的性能,但是对节点非常多的数据会非常吃力。
-
优化后算法
相比之下,CPU使用量也有所降低。
经过对a-e的完全图数据测试最长单词链,程序从优化前跑不出结果到优化后能在几十秒跑出结果,性能具有很大提升。
编译器编译截图
Design by Contract, Code Contract
契约式设计强调三个概念:前置条件,后置条件和不变式。前置条件发生在每个操作(方法,或者函数)的最开始,后置条件发生在每个操作的最后,不变式实际上是前置条件和后置条件的交集。违反这些操作会导致程序抛出异常。
在实际实现时,一个函数内部的功能实现基本依赖传入的参数,而函数本身可以对参数“施加契约”,如果参数不符合条件则“撕毁契约”。assert
语句是典型的契约式编程的实现方式。
在我们的设计里,我们在dll接口里使用契约式设计,确保传入的参数指针非空。
如assert(words != nullptr);
。
单元测试
1、数据构造
- 构造数据采用自动化+手动补全两种方式相结合
- 对于自动化部分,采用随机数获取单个字符,但是采用一定规则来限制单词长度和首尾字母,以此来保证样例自身的正确性与多样性,对于部分测试样例只需要保证首尾字母正确性,单词长度也可以在一定范围内自动变化。c++不设置随机数种子时不会以时间为种子,因此可以保证每次自动生成的数据一致。
- 设置一定的规则,来确保生成的数据合理,如生成25个首尾相连的长单词、生成325个节点的复杂树结构、生成带大量环的随机数据等。
- 针对边界情况、特殊情况等手动构造数据,对于自动化测试无法覆盖的分支单独分析,单独构造针对性数据。
2、覆盖情况
- 使用
OpenCppCoverage
插件进行覆盖率检测,达到99%覆盖率。由于程序中存在部分冗余设计,比如留出一些接口、部分错误检测代码重复等,覆盖率不能达到100%。
异常处理
我们将异常封装为类,具有string类型的information属性,用于外层捕捉后输出错误信息。
具体异常分类如下:
指令格式异常
- 未知的参数
- -h -t -j后未指定字符
- -h -t -j后指定多个字符
- -h -t -j后指定非字符
- 参数组合冲突
- 未指定任何参数
文件异常
- 文件不是.txt格式
- 文件不存在
- 文件不可读
算法执行过程异常
-
数据有环但未指定-r
-
result长度超过20000
测试数据为长度很长的单词。 -
不存在满足条件的单词链
界面模块设计
1、界面布局
使用python调用PYQT编写,界面美化使用qss代码渲染,使用视频如下:
GUI展示
2、功能设计
-
左侧选择参数栏,每个按钮使用QToolBar()中的addAction构造,并通过自定义方法实现按钮图标的点击切换、按钮之间的相互关联,使用触发器监听点击事件。
-
右侧文本栏,继承QPlainTextEdit,重写resizeEvent方法,数字栏继承普通QWidget,重写updateContents等方法,两者结合达到文本编辑器效果。
-
保存导出,使用QFileSystemModel控件获得系统目录,并生成树结构
-
其余细节功能有些涉及自定义信号槽,来实现一些自定义的监听器;布局使用
QHBoxLayout
、QVBoxLayout
两种布局,使用弹簧等控件来控制位置。
界面模块与计算模块的对接
1、C++命令行程序与核心模块对接
- dll中需要暴露的接口使用
extern "C" __declspec(dllexport)
修饰,命令行程序中使用LoadLibraryA
函数动态加载dll,主程序中先定义函数指针typedef int(*AddFunc)(char* words[], int len, char* result[]);
,再通过GetProcAddress
找到函数入口地址,最后调用函数。
2、GUI模块调用dll
-
出于Loose Coupling考虑,我们认为已经在命令行中写过的文件处理、文本分析机制不应当再在python中重复第二遍,这样在修改一出往往会牵一发而动全身,因此使用python直接复用命令行模块,进而调用核心模块。
-
首先,必须满足题目只有一个运行程序的要求,因此我们将C++编写的命令行程序以二进制资源文件的格式打包进python生成的exe文件中,python每次自动使用该文件,以达到一个运行程序的目的。
-
其次,python使用popen管道,可以捕获所有输出,并监控命令行程序是否正常结束,最后将输出打印在文本区。
结对的过程
我们采用线下+线上方式进行共同开发。
由于结对队友在白天有实习需求,因此我们主要在晚上的共同时间一起线下开发。而白天不方便一起在线下编码时,则采用腾讯会议讨论的方式推进。
在结对开始的时候,我们先各自阅读指导书,然后一起分享各自的理解并讨论指导书中的细节。在充分分析题目需求后,我们开始讨论项目设计,明确core模块、GUI和CLI分别如何交互。之后我们再分析各模块的实现方法,包括编程语言、大致架构等。设计完成后我们开始结对编码实现各模块,分工主要为一个人写core模块,一个人写GUI,且两人轮换开发。完成编码后我们一起进行测试工作、对接工作,最终做完所有项目。
优缺点分析
结对编程方法的优缺点分析
- 优点
- 两人根据同一思路编码,能及时发现很多逻辑问题或粗心问题,减少 bug 出现的可能性。
- 结对的双方在开发过程中可以充分实时交流,提高两人合作效率。
- 结对者双方互相促进,提高二者的编程能力。
- 缺点
- 双方在结对开始时需要磨合。
- 结对过程中需要两个人同时在场,对于时间安排多样性的学生来说较难统一。
- 结对的过程中只有一个人在进行开发,因此编码环节的速度会相对慢。
成员优缺点分析
- 本人:
- 优点:对项目设计比较熟悉,熟悉本作业涉及的图的算法,能快速设计模块并用c++完成项目编码。
- 缺点:不了解UI和测试流程。
- 队友:
- 优点:使用PYQT写过UI,对前端有一定了解;懂一些性能测试方法,以及自动化测试方法;对跨代码调用有一定了解,愿意探索新方法。
- 缺点:coding速度慢,容易陷入一些小的问题中无法自拔。
交换模块
1、另一小组成员
- 19376309
- 20373862
2、互换运行
- 本组GUI运行对方组核心模块
- 对方组运行本组核心模块
3、遇到的问题
- 对方组也使用python,由于python调用dll比较困难,对方组并没有采用类似我们组将命令行程序打包成资源文件的做法,因此单独写了一个python调用的接口,而我们组没有在dll中设置该接口,导致对方GUI无法调用我方核心模块,只能使用命令行程序调用。
- 我们组使用GUI内置了转换成资源文件的命令行程序,因此可以直接调用对方组dll。
- 最受影响的是异常处理部分,我们均捕获exception基类来获取对方的异常,而没有编译对方的类。