敏捷软工结对编程博客
项目 | 内容 |
---|---|
这个作业属于哪个课程 | 2022春季软件工程(罗杰 任健) |
这个作业的要求在哪里 | 结对编程项目-最长英语单词链 |
我在这个课程的目标是 | 学习软工的项目合作管理知识,提升软件开发技术 |
这个作业在哪个具体方面帮助我实现目标 | 学习敏捷开发中的PSP与结对编程的思想并付诸实践 |
Part0 准备
1.必要信息
教学班级:周五班
项目地址:https://github.com/BUAADreamer/Longest-English-word-chain
成员:18373466 战晨曦,19373573 冯张驰
2.PSP开发时间估计
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) |
---|---|---|
Planning | 计划 | 90 |
· Estimate | · 估计这个任务需要多少时间 | 90 |
Development | 开发 | 790 |
· Analysis | · 需求分析 (包括学习新技术) | 60 |
· Design Spec | · 生成设计文档 | 30 |
· Design Review | · 设计复审 (和同事审核设计文档) | 30 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 |
· Design | · 具体设计 | 60 |
· Coding | · 具体编码 | 360 |
· Code Review | · 代码复审 | 60 |
· Test | · 测试(自我测试,修改代码,提交修改) | 180 |
Reporting | 报告 | 100 |
· Test Report | · 测试报告 | 60 |
· Size Measurement | · 计算工作量 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 |
合计 | 980 |
Part1 设计
3.接口设计理念
接口,对于团队开发来说至关重要。只有确定了接口,才能够在开发时让接口的实现者与接口的调用者之间的交流尽可能少,有利于提高效率;反之,如果接口模模糊糊,那么接口的实现者以及接口的调用者各自干的工作就很有可能冲突,少不了重构。在这个事情上我算是体验深刻:在结对进入互换模块阶段时,我们发现我们提供的计算模块接口和互换组的接口不一样(因为官方给的接口是废的,两个组在互相没通知的情况下各自重新设计了接口),这导致我们没法互换模块测试。为了能实现互换模块,当时我们花了 1 个小时改我们这边的计算模块的接口,这个过程很痛苦且没有意义。倘若最开始有一个稳定的接口且双方都严格遵守接口,那估计就没有这种返工了。而在两边接口重新确定后,不管我怎么修改接口的具体实现(比如修 bug,性能调优等),其他部分的代码都不需要改动(如果其他地方是对的话),并且互换模块的时候也不会带来额外的工作量,这样其实就是实现了松耦合,非常舒服。
另外在设计接口的时候,要尽可能做到信息隐藏。信息隐藏指的是在设计和确定模块时,使得一个模块内包含的特定信息(过程或数据),对于不需要这些信息的其他模块来说,是不可访问的。在数据方面,CalcuCore
是我们整个项目的计算核心,但是其构造方法传入的属性对外界并没有什么用处,所以我们把 CalcuCore
中的属性都设计成了 private
。在过程方面,对 PairTestInterface
中的 4 个接口 API 来说,CalcuCore
中只有 4 个对应的方法需要被其知道,所以我们只把 CalcuCore
中的 4 个核心方法设计成 public
;而这几个核心方法所调用的其他工具方法却对外界没有什么用处,我们也不希望外界知道 CalcuCore
内部有这么几个方法,所以工具方法我们都设计成了 private
。
4.接口/算法的设计与实现
接口设计
由于我们事先阅读了后面阶段的要求,并且预先找到了互换模块的小组,所以我们设计了适用于 C#
的类似于官方接口的接口,接口中每个方法的功能,和官方接口同名方法的功能相同。
接口设计如下:
public static int gen_chain_word(List<string> words, List<string> result, char head, char tail, bool enable_loop);
public static int gen_chains_all(List<string> words, List<string> result);
public static int gen_chain_word_unique(List<string> words, List<string> result);
public static int gen_chain_char(List<string> words, List<string> result, char head, char tail, bool enable_loop);
我们将这些接口方法整合到了 PairTestInterface
中。在本地测试时,我们通过 CmdTestInterface
接口去驱动测试,调用 PairTestInterface
提供的接口,而 PairTestInterface
提供的接口调用 CalcuCore
中的具体计算方法。
接口实现
接口方法的实现,实际上是首先通过参数去实例化 CalcuCore
类(这是我们真正的核心计算类),然后调用 CalcuCore
类中对应的真正的计算方法 ,之后对返回的结果做异常处理,体现了层次化的思想。例如:
- 对于
gen_chain_word
,我们用给定的参数words
、head
、tail
、enable_loop
以及一些default
参数(辅助建图)去实例化一个CalcuCore
,然后调用CalcuCore
中的getMaxWordCountChain
方法进行具体的计算。 - 对于
CalcuCore
:- 其构造方法会使用
gen_chain_word
给定的参数,进行参数化建图。 getMaxWordCountChain
首先会调用数据检查方法dataCheck
,去检查数据中是否有隐含环以及是否允许在有环的情况下求解;如果数据中没有环,则调用重构图方法refactorGraph
对图进行预处理,接着调用快速算法fastGetMaxWordCountChain
在DAG
上跑dp
求解;如果数据中有环,且要求在有环的情况下求解,则调用暴力算法trivialGetMaxWordCountChain
求解最长链。
- 其构造方法会使用
- 对于异常:
- 主要是处理结果过长以及数据有环且不能求解这两种情况。
算法设计
首先,如果单词 A
的尾字母和单词 B
的首字母相同,则以 A
和 B
为结点,连接一条从 A
到 B
的有向边,在 O ( n 2 ) O(n^2) O(n2) 的时间内建立一张有向图。其中 n n n 是不同单词的种类数。有向图的边权我们并不关心,但是点权的设置要根据情况来:如果我们求解的最长链是以个数为指标,则点权为 1;如果是以字母数为指标,那么点权为单词的长度。
然后就是每个具体接口的求解算法:
gen_chain_word
的求解算法主要是 getMaxWordCountChain
,而 getMaxWordCountChain
的算法分为两部分。如果图是一个有向无环图,那么可以使用动态规划求解:
dp[word]
表示word
以word
结尾的单词链的最长长度,lastWord[word]
表示以word
结尾的最长链的前驱结点。- 初始化
dp[word]
为word
的点权,lastWord[word]
为空(即没有前驱)。 - 转移时,采用在拓扑排序的基础上进行状态转移的方式,假设
A
是B
的前驱,那么dp[B] = max(dp[B], dp[A] + weight[B])
,其中weight[B]
是B
的点权。假如有dp[A] + weight[B] > dp[B]
,那么还需要更新前驱,即lastWord[B] = A
。 - 最终统计答案时,需要看所有点的
dp
值,把最大的那个