1. GitHub地址
https://github.com/swearitagain/wordlist
2. 项目预估开发时间&实际开发时间
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
Planning | 计划 | 10 | 20 |
· Estimate | · 估计这个任务需要多少时间 | 10 | 20 |
Development | 开发 | 870 | 1470 |
· Analysis | · 需求分析 (包括学习新技术) | 60 | 120 |
· Design Spec | · 生成设计文档 | 40 | 40 |
· Design Review | · 设计复审 (和同事审核设计文档) | 30 | 30 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 20 | 20 |
· Design | · 具体设计 | 60 | 100 |
· Coding | · 具体编码 | 400 | 1000 |
· Code Review | · 代码复审 | 60 | 40 |
· Test | · 测试(自我测试,修改代码,提交修改) | 120 | 120 |
Reporting | 报告 | 70 | 120 |
· Test Report | · 测试报告 | 20 | 30 |
· Size Measurement | · 计算工作量 | 20 | 30 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 60 |
合计 | 950 | 1610 |
3. 接口设计
Information Hiding,Interface Design, Loose Coupling
Information Hiding
首先参考wikipedia定义:
information hiding is the principle of segregation of the design decisions in a computer program that are most likely to change, thus protecting other parts of the program from extensive modification if the design decision is changed
就是把数据封装起来,防止变化的部分对于原有数据的破坏,在结对编程中就是面向对象的实现,比如把输入输出封装为input_output类,将读入的数据暴露一个input函数,对外返回的是vector<string>。
Interface Desgin
接口按照https://edu.cnblogs.com/campus/buaa/BUAA_SE_2019_LJ/homework/2638 设计
static int gen_chain(char* words[], int len, char* result[]);
static int gen_chain_word(char* words[], int len, char* result[], char head, char tail, bool enable_loop);
static int gen_chain_char(char* words[], int len, char* result[], char head, char tail, bool enable_loop);
Loose Coupling
松耦合是指在编程的时候让一个部分尽可能少地依赖其他部分的组件,这样就算因为需求更改而重写以前的函数,也能避免对其他没有变化的部分造成影响。
松耦合在本次结对编程项目中主要体现在对于函数以及.cpp的封装上,保证每个模块的功能独立性,比如input_output.cpp只是对于输入输出的处理,calculate只是对于计算的处理。
4. 计算模块接口部分的性能改进
首先定义了一个接口类作为基类,暴露出调用者需要的方法,其中get_result()函数返回计算模块计算的结果:
class calculateInterface
{
public:
virtual ~calculateInterface();
virtual vector<string> *get_result()=0;
};
其次设计了calculate子类来完成最基本的计算功能:
这个类实现了接口类中定义的get_result方法,并新增了一个构造方法,以及一些私有的成员和方法。构造方法传入单词文本,以及-c参数。
私有方法中chain_find_next方法作用为找到当前单词结点的所有能够成链的下一个单词,而check_current_chain判断当前链是否是找到的最大链。
class calculate :
public calculateInterface
{
public:
calculate(vector<string> words, bool more_letter);
~calculate();
vector<string> *get_result() override;
protected:
vector<word_node> word_map[ALPHA_COUNT];
bool has_circle = false;
bool more_letter;
int longest_letter_count = 0;
int current_letter_count = 0;
vector<string> longest_word_chain;
vector<string> current_word_chain;
virtual bool chain_find_next(word_node prev_node);
virtual void check_current_chain();
};
再然后设计了specified_calculate类,该类继承calculate类,支持了指定链首尾字母的功能。
该类重写了calculate类的get_result方法和check_current_chain方法,保留使用了父类的chain_find_next方法。
class specified_calculate :
public calculate
{
public:
//构造函数四个参数:
//1. 字符串数组,由所有单词构成
//2. 布尔变量,是否按照字母最多计算单词链
//3. 整型,指定首字母,-1为不指定,0-26对应26个字母
//4. 整型,指定尾字母,-1为不指定,0-26对应26个字母
specified_calculate(vector<string> words, bool more_letter, int assigned_initail, int assigned_tail);
~specified_calculate();
vector<string> *get_result() override;
void check_current_chain() override;
protected:
int assigned_initial;
int assigned_tail;
};
最后设计了circle_calculate类,该类继承specified_calculate类,支持了允许单词文本中隐含单词环功能。
该类重写了specified_calculate类的check_current_chain方法,保留使用了父类的其他所有方法。
class circle_calculate :
public specified_calculate
{
public:
//构造函数五个参数:
//1. 字符串数组,由所有单词构成
//2. 布尔变量,是否按照字母最多计算单词链
//3. 整型,指定首字母,-1为不指定,0-26对应26个字母
//4. 整型,指定尾字母,-1为不指定,0-26对应26个字母
//5. 布尔类型,是否允许文本隐含单词环
circle_calculate(vector<string> words, bool more_letter,
int assigned_initail, int assigned_tail, int circle);
~circle_calculate();
bool chain_find_next(word_node prev_node) override;
protected:
bool circle;
};
类之间的关系可以参考下文的UML图。
5. UML图
6. 性能分析
我们对项目进行了性能分析,发现项目的性能瓶颈在于递归迭代的深度过大。因此我们优化了递归函数的结构,减少了其不必要的操作,性能稍有提升。
7. Design by Contract, Code Contract
最开始不是很了解Design By Contract的概念,在Wikipedia上得到的标准定义如下:
//Design by contract
Design by contract (DbC), also known as contract programming, programming by contract and design-by-contract programming, is an approach for designing software. It prescribes that software designers should define formal, precise and verifiable interface specifications for software components, which extend the ordinary definition of abstract data types with preconditions, postconditions and invariants.
//code contract
The contracts take the form of pre-conditions, post-conditions, and object invariants. Contracts act as checked documentation of your external and internal APIs. The contracts are used to improve testing via runtime checking, enable static contract verification, and documentation generation.
Design By Contract也就是契约式设计,Code Contract规定了接口的数据类型,接口执行之前的条件(precondition)和接口执行之后的条件(postcondition)。
我认为这种编程方式的优点:
- 保证在满足前提条件的情况下,代码才会按照特定的方式执行,同时反面就是如果不满足特定条件,代码不会执行,等同于过滤了错误输入。
- 不用在接口中验证输入是否满足条件,保证了功能的纯洁。
缺点:
- 增加编码的复杂度,对于迭代快的开发感觉不适合。
项目中如何使用:
- 通过提前规定接口文档的方式,一定程度上替代了契约式编程。
8. 计算模块部分单元测试展示
由于在单元测试中不能使用命令行输入,所以只需要设计函数的输入,鉴定所需要的输出即可,测试案例如下:
TEST_METHOD(test_gen_chain_w) {
char *result[4];
char *words[4] = { "END", "OF", "THE", "WORLD" };
Assert::AreEqual(2, gen_chain(words, 4, result));
}
测试覆盖率时使用OpenCppCoverage-0.9.6.1 VS插件进行,将单元测试模块迁移到main函数中测试之后,测试覆盖率图如下:
9. 计算模块部分异常处理说明
异常类型 | 设计目标 |
---|---|
对于传入的单词文本为空时的报错 | |
单词文本隐含单词环 | 对于没有-r参数时出现隐含单词环的报错 |
首尾字母约束不合法 | 对于单词首/尾字母指定不合法的报错 |
文本中某个单词为空 | 对于某个单词为空时的报错 |
文本中某单词首为非字母 | 对于首字母为非字母情况的报错 |
文本中某单词尾为非字母 | 对于尾字母为非字母情况的报错 |
关于命令行输入的异常如下:
抛出异常 | 说明 |
---|---|
-w param repeat | w参数重复 |
-c param repeat | c参数重复 |
-h param repeat | h参数重复 |
-t param repeat | t参数重复 |
-r param repeat | r参数重复 |
invalid param | 无效参数 |
非法输入:文件不存在 | 非法输入:文件不存在 |
10. 界面模块的详细设计过程
本次只实现了命令行模块。
在结对编程项目中构建了一个input_output类,专门处理从文本的输入和将结果输出到文本。
首先是从命令行的输入,核心模块是处理命令行的输入:
while (i < in.size()) {
if (in.at(i) == '-') {
i++; //get next char
char cur = in.at(i);
if (cur == 'w') {
if (is_w) {
throw exception("-w param repeat");
}
is_w = true;
}
else if (cur == 'c') {
if (is_c) {
throw exception("-c param repeat");
}
is_c = true;
}
else if (cur == 'h') {
if (is_h != 0) {
throw exception("-h param repeat");
}
i+=2; //get the blank char
is_h = in.at(i);
}
else if (cur == 't') {
if (is_t != 0) {
throw exception("-t param repeat");
}
i += 2; //get the blank char
is_t = in.at(i);
}
else if (cur == 'r') {
if (is_r) {
throw exception("-r param repeat");
}
is_r = true;
}
else {
throw exception("invalid param");
}
}
else if (in.at(i) != ' ') { //read the absolute path of input file
break;
}
i++;
}
其中对于不符合规定的部分使用异常抛出,在main函数中接受异常。
11. 界面模块与计算模块的对接
本次只实现了命令行模块。
根据解耦合的思想,设计了一个专门的input_output
class input_output
{
public:
input_output();
~input_output();
vector<string> input();
void output(vector<string> words);
vector<string> words;
bool is_w; //word-按单词数量统计
bool is_c; //count-按字母数量统计
char is_h; //head-指定首字母
char is_t; //tail-指定尾字母
bool is_r; //round-是否成环
string in_path; //输入文件路径
string out_path; //输出文件路径
string err_msg; //错误日志
};
12. 结对的过程
首先,在拿到题目后,我们迅速阅读了项目的整个要求。在对项目的整体轮廓有大致的了解后,我们开始讨论分析了项目的结构。仅仅浮于口上的讨论是不够的,也不利于后续实现。因此我们草拟了一个文档初稿来规定了具体分工、接口设计、代码规范等技术细节问题。
在具体分工方面,虽然是结对编程,但我们的工作仍有不同的侧重。根据分工,我主要负责计算核心模块的开发和异常处理,队友主要负责测试工作和界面模块开发。
在接口设计方面,我们遵照项目要求的接口设计,计算核心模块和界面模块都遵照项目要求中的三个接口进行设计。
在代码技术规范方面,我们采用《百度C++编程规范》中的要求和建议,作为我们的代码设计规范。
在前期的预备工作准备完毕后,我们开始上手工作。首先设计了约定的接口并作出简单的测试,之后我负责开发核心计算模块,队友则负责编写界面模块和测试用例。由于事先约定清晰,我们分别完成首个版本计算模块和交互模块后就立即展开了对接,没有多余的消耗。
此后我进行了几轮迭代,完善了计算模块的所有功能;队友跟进单元测试和回归测试,保证了计算模块的正确性。最后我们作了性能分析等后续工作,完成项目。
13. 结对编程解析
优点:能够使代码处于一种一直在被复审的状态,程序员不断审核对方的代码,可以提高编码质量以及及时发现问题[大概率上]。
缺点:对于迭代快的项目开发,人力资源可能会很紧张, 需要团队成员独自开发自己的模块,结对编程对时间的总体利用率很可能不高。
结对编程最终的效果如何无非就是取决于1. 两个人的编程水平 2. 两个人合作的效率。至于博客要求至少列出每个人三个优点和一个缺点,我觉得没啥可写的。如果两个人都不鸽对面,并且尽可能推进项目的进展,对于结对编程的目的来说,就够了。