项目 | 内容 |
---|---|
这个作业属于哪个课程 | 2023年北航敏捷软件工程 |
这个作业的要求在哪里 | 结对项目-最长英语单词链 |
我在这个课程的目标是 | 学习现代化的软件开发方法 |
这个作业在哪个具体方面帮助我实现目标 | 对结对编程敏捷开发进行实践,对测试工作有了新的认识 |
文章目录
一、项目地址
- 教学班级:周四班
- 项目地址:https://github.com/WAN-M/pair_programming2023.git
- 结对成员:20373861
二、PSP
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 50 |
· Estimate | · 估计这个任务需要多少时间 | 60 | 50 |
Development | 开发 | 2400 | 2920 |
· Analysis | · 需求分析 (包括学习新技术) | 120 | 150 |
· Design Spec | · 生成设计文档 | 100 | 90 |
· Design Review | · 设计复审 (和同事审核设计文档) | 50 | 60 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | 20 |
· Design | · 具体设计 | 300 | 280 |
· Coding | · 具体编码 | 900 | 1000 |
· Code Review | · 代码复审 | 200 | 120 |
· Test | · 测试(自我测试,修改代码,提交修改) | 700 | 1200 |
Reporting | 报告 | 280 | 380 |
· Test Report | · 测试报告 | 200 | 300 |
· Size Measurement | · 计算工作量 | 50 | 30 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 50 |
合计 | 2740 | 3350 |
三、接口设计
1、Information Hiding
- 信息封装与隐藏,在面向对象思维中,编写代码时需要对各模块进行封装,只暴露必要的接口,而不提供内部具体的实现逻辑,以达到数据的安全,以及降低程序的复杂度。
- 在算法模块的实现中,利用C++面向对象的特性,将图、边、节点等常用数据结构单独封装为类,对异常、工具类也进行单独封装,以供求解器调用,求解器无需知道数据结构实现自身功能的具体方式,只需要调用正确的接口即可。
- 在GUI模块中,对界面使用的各组件如文本块、按钮块等单独进行继承与封装,UI线程只需要加载这些模块,具体功能由模块各自内部封装实现,并且利用继承与重写,可以达到代码的复用,降低开发复杂度。
2、Interface Design
- 良好的接口设计有助于各模块之间的分离,使模块间的职责更加清晰明确,对于开发者而言可以达到模块的复用以避免重复的工作,并且使用相同规范的接口有助于不同开发者之间相互调用。
- 在最开始的设计中,没有将输入输出与算法相分离,因此设计的是统一的求解器接口solve(),通过该接口实现所有参数的分析处理。在后续的开发中,首先将文本处理模块与算法模块分离,再对基于solve()这个已实现的全功能接口进行封装,封装为课程设计所需的三个接口,由调用者决定具体使用哪个接口,达到接口的规范与统一,以便于交换核心模块时的处理。
3、Loose Coupling
- 接口封装应当尽量降低模块间的耦合度,例如核心模块与程序模块交互只通过指针与整型,不使用各自模块内部的类进行交互,这样其他人调用核心模块不需要实现核心模块内部封装好的类,核心模块也不需要实现其他人所实现的类,保证了耦合度的降低与模块的复用性。
- 在打包这两个模块中遇到的最需要注意的便是异常的交换,在接口没有明确说明如何传递异常的情况下,必须使核心模块抛出的异常可以被其他人的程序模块所捕获,因此异常均继承自父类exception并重写what()方法,使任何人可以通过catch直接捕获并读取内部信息。
四、接口实现
1、接口实现
- 核心模块按照作业要求提供如下三个接口:
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。
2、算法设计
-
对于本次单词链相关任务,单词之间以首尾字母构成关系,具有明显有向图的特征。因此我们将单词链任务转换成有向图上的算法,实现起来直观简洁。
-
我们小组尝试了两种建图方式,一种是每个单词作为结点,另一种是每个字母作为结点,边为单词。其实我们感觉两种建图方式对于不优化的搜索效率基本一样,但后者有一个明显的优势,也是我们算法实现中贪心策略体现的重要部分:在结点为字母的图中,点到点的搜索优先选择权值最大的。比如abcd de dddde这一样例,显然在搜索d到e的路径时,可以直接选择最长的ddde,而不用在考虑较短的de。这一性质在实际搜索时可以剪枝相当一部分,不用搜索各种路径,大幅提升性能。我们同样采用简洁直观的实现方式:对于每个结点,用容器(我们使用的list)分开存储其到下一结点的边,存完所有边后对容器按权值从大到小排序,且维护一个指针指向容器开始。在搜索时,每次只遍历指针指向的边,遍历一条边后指针后移,回溯时指针前移,这样可保证搜索正确性且每次都只遍历当前最长边。
-
搜索时要不断维护当前结果和已知最长结果,同样为了减少中间结果拷贝开销,我们以string &传递,每次修改同一string。
-
在具体实现以字母为点的算法时,考虑到可以以多个点为起点或多个点为终点,我们在图中引入虚点SOURCE和TARGET作为超级源、超级汇。这样就确保了起点终点唯一,算法代码实现起来更为简洁。
-
所有单词链算法是我们比较纠结的点。若是只需要输出所有单词链个数,动态规划可以以O(n)的复杂度求解。但要输出所有路径则让人很难权衡。如果同样根据动态规划思路求解,则需要保存每个点到终点的单词链,这样对于新的路径产生时,可以维持O(n)的复杂度输出。但这样的内存复杂度则提升到指数级。因此,我们认为采取遍历所有路径一条一条输出的算法,将时间复杂度提高而非“以空间换时间”。
-
采用以字母为结点的建图方式还有一大好处,即无论图有环无环,在保证每次搜索搜最长边的前提下,都可采用同一搜索策略完成算法。而以单词为结点的建图方式,我们根据性能需要还图分为有环图和无环图分别处理,有环图暴搜,无环图通过负权边用spfa求得最长路,实现起来更繁琐复杂。
3、结构设计
-
由于我们小组最后采用基于以字母为结点的图的算法,这里只介绍相关设计。
-
首先,输入的参数个数较多,且-n和-w-c所需的参数不同,因此各参数在算法中如何传递需要解决。此外,建好的图也需要在算法直接流动,图的对象由谁保管也需要慎重设计。考虑到每次调用dll时,参数只有一种组合,图只有一个,我们小组决定在项目中以单例模式建立Global类,其中包含Graph对象和Parameter对象。这种设计相当于将图和参数信息设计为全局变量,项目任何地方需要可以直接查看。
-
算法实现部分我们融合在Solver类里。Solver类具有public静态方法solve供外界调用,然后Solver根据全局参数信息调用对应函数求解。
-
结构如下:
algorithm
模块:Edge
、Node
、Graph
、Global
、Solver
var
模块:Parameter
、常量信息tools
模块:全局工具函数exception
模块:自定义异常类,继承自exception
五、编译情况
六、UML
七、性能优化
1、优化前算法
- 最初版中采用的以单词为结点的建图方式,采用
Visual Studio性能探查器
,数据为随机生成与固定数据相结合。
- 排名靠前的为自动数据生成器相关代码,因此大部分CPU占用由自动数据生成器与系统调用代码组成,算法已经有较好的性能,但是对节点非常多的数据会非常吃力。
2、优化后算法
- 我们小组的性能改进策略如下:
- 采用以字母为结点的方式建图,同时确保每次遍历只遍历点对点间的最长边的贪心策略。
- 容器中存储指针,以减少拷贝开销。
- 搜索时最先走完自环。
- 相比之下,CPU使用量也有所降低
- 经过对a-e的完全图数据测试最长单词链,程序从优化前跑不出结果到优化后能在几十秒跑出结果,性能具有很大提升。
八、Design by Contract & Code Contract
1、契约式编程
-
契约式编程组成部分:前置条件、后置条件、不变量条件
-
优点:
当双方都保证实现约束条件时,开发工作可以变得非常高效,因为程序保证了正确性
-
缺点
契约作用于双方,每一方都必须完成任务,有一方违反则会失败,并且需要一个验证机制,来保证确实是按约束条件coding的
-
我们对api接口做出了约定:传入的数据必须为全部小写,传入的单词必须去重,传入之前需要对指针数组开辟20000个char*空间。
2、Code Contract
- .Net提供了Code Contract,用于创建软件契约,可以用代码的形式检查前置条件、后置条件和不变量条件
- 非常值得一试,但由于没有使用C#开发,因此没有使用该插件
九、单元测试
1、数据构造
- 构造数据采用自动化+手动补全两种方式相结合
- 对于自动化部分,采用随机数获取单个字符,但是采用一定规则来限制单词长度和首尾字母,以此来保证样例自身的正确性与多样性,对于部分测试样例只需要保证首尾字母正确性,单词长度也可以在一定范围内自动变化。c++不设置随机数种子时不会以时间为种子,因此可以保证每次自动生成的数据一致。
- 设置一定的规则,来确保生成的数据合理,如生成25个首尾相连的长单词、生成325个节点的复杂树结构、生成带大量环的随机数据等。
- 针对边界情况、特殊情况等手动构造数据,对于自动化测试无法覆盖的分支单独分析,单独构造针对性数据。
2、覆盖情况
- 使用
OpenCppCoverage
插件进行覆盖率检测,达到99%覆盖率,
十、异常处理
1、文件类异常
- 文件不存在、文件不可读
- 数据文件类型不是.txt
2、参数类错误
- 参数组合冲突,如-n与-w一起用
- -h -t -j后指定多个字符
- -h -t -j后未指定字符
- -h -t -j后指定非字符
- 未知的参数
- 未指定任何参数
3、算法类异常
- 数据有环但未指定-r
- result长度超过20000
- 不存在满足条件的单词链,如首字母禁止与强制相同
十一、界面设计
1、界面布局
- 使用python调用PYQT编写,界面美化使用qss代码渲染,使用视频如下:
结对
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,且两人轮换开发。完成编码后我们一起进行测试工作、对接工作,最终做完所有项目。
十四、优缺点总结
1、结对编程优缺点
-
优点:
(1)结对编程时两人思路可以总是保持一致,即使有分歧也可以在问题扩大之前快速解决。 (2)效率高,代码质量高,出错率降低。 (3)双方可以相互学习,互相学习对方编程中优秀的经验。
-
缺点
(1)在正式进入项目开发前需要时间相互了解。 (2)正式进入开发后有可能因为性格问题导致无法解决的分歧进而解体。 (3)作为学生很难每条腾出固定的时间进行结对,无法模拟上班的真实情况。
2、个人优缺点
队友 | 自己 |
---|---|
优点:算法能力非常优秀 | 优点:对于Python使用PYQT写客户端界面非常熟悉 |
优点:对于C++以及CMake等工具使用熟练 | 优点:对visual studio的开发环境配置有一定的了解 |
优点:工作效率高,开发速度快 | 优点:学过一些测试相关理论,对自动化测试比较热衷 |
缺点:对于测试和仓库管理等有所欠缺 | 缺点:算法能力不是很强 |
十五、实际PSP
- 见 二、PSP 部分
十六、额外任务
1、另一小组
- 19376309
- 20373862
2、互换运行
- 本组GUI运行对方组核心模块
- 对方组运行本组核心模块
3、遇到的问题
- 对方组也使用python,由于python调用dll比较困难,对方组并没有采用类似我们组将命令行程序打包成资源文件的做法,因此单独写了一个python调用的接口,而我们组没有在dll中设置该接口,导致对方GUI无法调用我方核心模块,只能使用命令行程序调用。
- 我们组使用GUI内置了转换成资源文件的命令行程序,因此可以直接调用对方组dll。
- 最受影响的是异常处理部分,解决方案是我们均捕获exception基类来获取对方的异常,而没有编译对方的类