结对项目——最长英语单词链

单词链项目

项目内容
这个作业属于哪个课程2023年北航敏捷软件工程
这个作业的要求在哪里结对项目-最长英语单词链
我在这个课程的目标是学习现代化的软件开发方法
这个作业在哪个具体方面帮助我实现目标对结对编程进行实践,对软件开发方法有了新的认识

项目地址

  • 教学班级:周四班

  • 项目地址:https://github.com/WAN-M/pair_programming2023.git

  • 结对成员:19375035

PSP

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

接口设计相关方法

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图如下:

Solver
+int solve(char **result)
-int allWordlist(char **result)
-int longestWords(char **result)
-int longestAlphas(char **result)
Global
-Graph graph;
-Parameter parameter;
+static Global &get_instance()
Graph
-vector nodes;
+Node &getNode(int pos)
Node
-vector<list Edge * > edges;
-list<Edge *>::iterator edgePos[30];
+void addEdge(Edge *e)
+bool hasEdge(int v)
+Edge *nextEdge(int v)
+void increaseItr(int v)
+void decreaseItr(int v)
Edge
-char *word;
-int len;
-int u;
-int v;
Parameter

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

由于我们在最初版中采用的以单词为结点的建图方式,后来在修改算法时需要大幅改动项目结构,耗时较多。加上后续不断确保正确性,我们在性能改进部分总耗时大约为15小时。

我们小组的性能改进策略如下:

  • 采用以字母为结点的方式建图,同时确保每次遍历只遍历点对点间的最长边的贪心策略。
  • 容器中存储指针,以减少拷贝开销。
  • 搜索时最先走完自环。
  1. 优化前算法

    最初版中采用的以单词为结点的建图方式,采用Visual Studio性能探查器,数据为随机生成与固定数据相结合。

在这里插入图片描述

在这里插入图片描述

排名靠前的为自动数据生成器相关代码,因此大部分CPU占用由自动数据生成器与系统调用代码组成,算法已经有较好的性能,但是对节点非常多的数据会非常吃力。

  1. 优化后算法

在这里插入图片描述

相比之下,CPU使用量也有所降低。

经过对a-e的完全图数据测试最长单词链,程序从优化前跑不出结果到优化后能在几十秒跑出结果,性能具有很大提升。

编译器编译截图

请添加图片描述

Design by Contract, Code Contract

契约式设计强调三个概念:前置条件,后置条件和不变式。前置条件发生在每个操作(方法,或者函数)的最开始,后置条件发生在每个操作的最后,不变式实际上是前置条件和后置条件的交集。违反这些操作会导致程序抛出异常。

在实际实现时,一个函数内部的功能实现基本依赖传入的参数,而函数本身可以对参数“施加契约”,如果参数不符合条件则“撕毁契约”。assert语句是典型的契约式编程的实现方式。

在我们的设计里,我们在dll接口里使用契约式设计,确保传入的参数指针非空。

assert(words != nullptr);

单元测试

1、数据构造

  • 构造数据采用自动化+手动补全两种方式相结合
  • 对于自动化部分,采用随机数获取单个字符,但是采用一定规则来限制单词长度和首尾字母,以此来保证样例自身的正确性与多样性,对于部分测试样例只需要保证首尾字母正确性,单词长度也可以在一定范围内自动变化。c++不设置随机数种子时不会以时间为种子,因此可以保证每次自动生成的数据一致。

在这里插入图片描述

  • 设置一定的规则,来确保生成的数据合理,如生成25个首尾相连的长单词、生成325个节点的复杂树结构、生成带大量环的随机数据等。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  • 针对边界情况、特殊情况等手动构造数据,对于自动化测试无法覆盖的分支单独分析,单独构造针对性数据。

在这里插入图片描述

2、覆盖情况

  • 使用OpenCppCoverage插件进行覆盖率检测,达到99%覆盖率。由于程序中存在部分冗余设计,比如留出一些接口、部分错误检测代码重复等,覆盖率不能达到100%。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

异常处理

我们将异常封装为类,具有string类型的information属性,用于外层捕捉后输出错误信息。

具体异常分类如下:

指令格式异常

  1. 未知的参数

在这里插入图片描述

在这里插入图片描述

  1. -h -t -j后未指定字符

在这里插入图片描述

在这里插入图片描述

  1. -h -t -j后指定多个字符

在这里插入图片描述

在这里插入图片描述

  1. -h -t -j后指定非字符

在这里插入图片描述

在这里插入图片描述

  1. 参数组合冲突
    在这里插入图片描述

在这里插入图片描述

  1. 未指定任何参数

在这里插入图片描述

在这里插入图片描述

文件异常

  1. 文件不是.txt格式

在这里插入图片描述

在这里插入图片描述

  1. 文件不存在

在这里插入图片描述

在这里插入图片描述

  1. 文件不可读

算法执行过程异常

  1. 数据有环但未指定-r
    在这里插入图片描述

    在这里插入图片描述

  2. result长度超过20000
    测试数据为长度很长的单词。在这里插入图片描述 在这里插入图片描述

  3. 不存在满足条件的单词链
    在这里插入图片描述

    在这里插入图片描述

界面模块设计

1、界面布局

使用python调用PYQT编写,界面美化使用qss代码渲染,使用视频如下:

GUI展示

2、功能设计

  • 左侧选择参数栏,每个按钮使用QToolBar()中的addAction构造,并通过自定义方法实现按钮图标的点击切换、按钮之间的相互关联,使用触发器监听点击事件。

  • 右侧文本栏,继承QPlainTextEdit,重写resizeEvent方法,数字栏继承普通QWidget,重写updateContents等方法,两者结合达到文本编辑器效果。

  • 保存导出,使用QFileSystemModel控件获得系统目录,并生成树结构

  • 其余细节功能有些涉及自定义信号槽,来实现一些自定义的监听器;布局使用QHBoxLayoutQVBoxLayout两种布局,使用弹簧等控件来控制位置。

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

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. 两人根据同一思路编码,能及时发现很多逻辑问题或粗心问题,减少 bug 出现的可能性。
    2. 结对的双方在开发过程中可以充分实时交流,提高两人合作效率。
    3. 结对者双方互相促进,提高二者的编程能力。
  • 缺点
    1. 双方在结对开始时需要磨合。
    2. 结对过程中需要两个人同时在场,对于时间安排多样性的学生来说较难统一。
    3. 结对的过程中只有一个人在进行开发,因此编码环节的速度会相对慢。

成员优缺点分析

  • 本人:
    • 优点:对项目设计比较熟悉,熟悉本作业涉及的图的算法,能快速设计模块并用c++完成项目编码。
    • 缺点:不了解UI和测试流程。
  • 队友:
    • 优点:使用PYQT写过UI,对前端有一定了解;懂一些性能测试方法,以及自动化测试方法;对跨代码调用有一定了解,愿意探索新方法。
    • 缺点:coding速度慢,容易陷入一些小的问题中无法自拔。

交换模块

1、另一小组成员

  • 19376309
  • 20373862

2、互换运行

  • 本组GUI运行对方组核心模块

在这里插入图片描述

在这里插入图片描述

  • 对方组运行本组核心模块

在这里插入图片描述

在这里插入图片描述

3、遇到的问题

  • 对方组也使用python,由于python调用dll比较困难,对方组并没有采用类似我们组将命令行程序打包成资源文件的做法,因此单独写了一个python调用的接口,而我们组没有在dll中设置该接口,导致对方GUI无法调用我方核心模块,只能使用命令行程序调用。
  • 我们组使用GUI内置了转换成资源文件的命令行程序,因此可以直接调用对方组dll。
  • 最受影响的是异常处理部分,我们均捕获exception基类来获取对方的异常,而没有编译对方的类。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值