[2023软工作业]结对作业-项目总结

项目

内容

这个作业属于哪个课程

2023年北航敏捷软件工程

这个作业的要求在哪里

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

我在这个课程的目标是

通过结对编程完成项目

这个作业在哪个具体方面帮助我实现目标

在实践中体会、理解结对编程的优缺点

一、课程信息


  • 教学班级:周四班

[独立]二、PSP表格记录估计开发时间


PSP2.1

Personal Software Process Stages

预估耗时(分钟)

Planning

计划

60

· Estimate

· 估计这个任务需要多少时间

60

Development

开发

1390

· Analysis

· 需求分析 (包括学习新技术)

40

· Design Spec

· 生成设计文档

30

· Design Review

· 设计复审 (和同事审核设计文档)

30

· Coding Standard

· 代码规范 (为目前的开发制定合适的规范)

10

· Design

· 具体设计

90

· Coding

· 具体编码

840

· Code Review

· 代码复审

50

· Test

· 测试(自我测试,修改代码,提交修改)

300

Reporting

报告

180

· Test Report

· 测试报告

150

· Size Measurement

· 计算工作量

15

· Postmortem & Process Improvement Plan

· 事后总结, 并提出过程改进计划

15

合计

1630

[独立]三、接口设计方法


Information Hiding

对于输入输出与计算功能的实现,我将其实现的子功能封装成各个函数(如下表),相互之间仅通过函数接口进行交互,不设置起交互作用的全局变量,以此实现了多个函数间的信息隐藏;对于封装的异常类,对其存储的异常信息设为private,仅通过public string print_exception()函数交互。

 // declare func
 boolis_end_word(charc);
 stringget_next_word(FILE*file);
 ​
 boolcheck_circle();
 vector<Record>get_all_records(charhead_letter, chartail_letter, charhead_not_letter, boolis_circle);
 vector<Record>get_main_records(charhead_letter, charhead_not_letter, boolis_circle);
 vector<Record>get_first_letter_main_records(inthead, boolis_circle);
 vector<Record>get_all_child_records(vector<Record>main_records, chartail_letter);
 vector<Record>get_child_records(Recordrecord, chartail_letter);
 vector<string>get_chain_by_record(Recordrecord);
 stringget_one_max_length_letter_chain_by_record(Recordrecord, boolis_circle);
 ​
 voidprint_records(vector<Record>records);
 stringprint_chains(vector<string>chains);
 stringprint_max_chain(stringchain);

Interface Design

在接口设计中,做到单一职责原则和接口隔离原则。每个接口仅完成了希望完成的任务,而没有进行额外的性能消耗,功能单一,不可再分。

Loose Coupling

在step2的接口设计中,将功能分为输入、计算、输出三个模块。三个模块间不存在数据直接交互(如需交互需要上层函数在三个模块间分别传递words和result变量),可以分别独自调用;并且各模块内部实现了众多功能(甚至输入模块完全可以用于其他项目的输入),实现了高内聚低耦合的目标。

四、接口设计与实现


模块间接口设计

在step2中,考虑到进一步功能切分,将功能分为输入、计算与输出三个模块。

输入模块模块(在lib.dll中)考虑到了输入的多样性,如CLI读入、文件读入、字符串读入、数组读入等。需要注意的是,为松耦合,模块之间不存在调用关系,即调用输入模块接口的主函数需自行存储处理所需参数(如head),但为便于CLI读入,输入接口提供处理CLI的接口

 intget_words_from_file_path(char*words[], char*file_path);
 intget_words_from_file_path(char*words[], stringfile_path);
 // 需要自行分配内存
 intget_words_from_cmd(char*words[], intargc, char*argv[], int*type_p, char*head_p, char*tail_p, char*reject_p, bool*enable_loop);
 intget_words_from_char(char*words[], char*words_char);
 intget_words_from_string(char*words[], stringwords_string);
 intget_words_from_string_array(char*words[], stringword_string[], intword_string_num);

计算模块(在core.dll中)主要用于完成计算数据,提供以下五个接口。分别用于求解所有链、求解所有满足条件的最长单词链、求解所有满足条件的最长字母链、求解一条满足条件的最长单词链、求解一条满足条件的最长字母链。

 intgen_chains_all(char*words[], intlen, char*result[]);
 intgen_chains_word(char*words[], intlen, char*result[], charhead, chartail, charreject, boolenable_loop);
 intgen_chains_char(char*words[], intlen, char*result[], charhead, chartail, charreject, boolenable_loop);
 intgen_chain_word(char*words[], intlen, char*result[], charhead, chartail, charreject, boolenable_loop);
 intgen_chain_char(char*words[], intlen, char*result[], charhead, chartail, charreject, boolenable_loop);
输出模块(在lib.dll中)提供了标准输出(cout)以及输出到文件两种方式。并且默认输出文件夹为\solution.txt。
 voidstd_out_result(char*result[], intlen);
 voidfile_write_result(char*result[], intlen, stringfile_path="solution.txt");

另外,三个模块中均可能出现各种异常,模块与异常间的接口便是对异常类的调用。以FileContentException异常为例,异常构造函数读入造成异常的信息c,并为保证信息隐藏将其作为私有变量存储。模块通过公共函数string print_exception();获取异常提示信息。异常类仅存储信息,并不直接进行异常信息输出。为便于提示,在各模块中以cout << e.print_exception() << endl;的方式输出异常信息,但完全可以支持将异常信息输出到文件中等行为。

 classFileContentException : publiclogic_error {
 public:
     explicitFileContentException(stringc): logic_error("file contains unexpected character: ") {
         this->c=std::move(c);
     }
 ​
     stringprint_exception() {
         strings=this->what();
         s+=c;
         returns;
     }
 ​
 private:
     stringc;
 };

计算模块设计

核心思想

可以发现,对于一条链,我们只关注其每个单词首位两个字母,而中间的字母仅起到增加单词长度的作用。我们进行如下定义记录Record:一个元素为字母,长度至少为3的链表,如a->b->c。很明显,一个Record代表了很多条链,这取决于words的分布,如a->b->d可以代表链aab->bbd,也可以代表链acdb->bdafd。但是我们完全可以根据一个Record,得到最合适的链。

另外,我们称所有以head_letter为首元素的所有最长Record(允许loop)的集合为head_letter的主Record集。主Record集中的每一个Record称为以head_letter为首元素的主Record。很明显,主Record代表能从head_letter沿着words能走到的(各种)最远路径。对于每条主Record,可以递归地求到以head_letter为首元素的子Record集(如a->b->c->d的子Record集为a->b->c, a->b->c->d)。主Record集中所有Record的子Record集的集合,就是以head_letter为首元素的所有Record。

Record是算法的核心。它可以很好地简化算法,并降低复杂度。

因此,对于所有功能,一般的,我们首先将words转化成图中的有向边。对于找到合适的链,首先转化为找到合适的Record,再根据Record找到合适的链。

这样的优点是,首先对问题起到了很好地抽象作用,便于思考,也有利于函数的单一功能实现。另外,对于求所有链的问题,得到主Record集就可以递归得到所有链;对于求最长链问题,由于最长Record一定大于子Record,完全可以先求最长主Record,再只判断主Record对应链长度得到最长链(仅对于不限制尾元素)。

数据结构
Vertex

用于图的拓扑排序,判断是否成环。

 typedefstructvertex {
     set<int>in_degrees;
     set<int>out_degrees;
 } Vertex;
 ​
 Vertexvertexes[26];
Edge
 structcmp_words {
     booloperator() (stringword1, stringword2) {
         returnword1.length() <word2.length();
     }
 };
 ​
 structedge {
     vector<string>words;
     priority_queue<string, vector<string>, cmp_words>words_priority_queue;
     intnum;
     boolis_free;
 };
 ​
 typedefstructedgeEdge;
 ​
 Edgegraph[26][26];

需要注意的是,Edge有一个成员变量为优先级队列,便于在从Record到chain时获取最长word。

Record

虽然Record仅有一个成员变量,但作为算法实现核心,封装起来便于使用。

 structrecord {
     vector<int>nodes;
 };
 ​
 typedefstructrecordRecord;
函数功能
 intgen_chains_all(char*words[], intlen, char*result[]);
 intgen_chains_word(char*words[], intlen, char*result[], charhead, chartail, charreject, boolenable_loop);
 intgen_chains_char(char*words[], intlen, char*result[], charhead, chartail, charreject, boolenable_loop);
 intgen_chain_word(char*words[], intlen, char*result[], charhead, chartail, charreject, boolenable_loop);
 intgen_chain_char(char*words[], intlen, char*result[], charhead, chartail, charreject, boolenable_loop);

上面五个接口是计算模块与外界交互的接口,但在模块内部,其主要起到异常处理作用,在模块内部均调度int solution(char* words[], int len, char* result[], int type, char head_letter, char tail_letter, char head_not_letter, bool is_circle=true, bool is_all_chains=false)函数。该函数进行图初始化、loop检测、异常判断、调用子函数等功能。可以说该函数的作用是初始化与任务分发,而非具体实现算法。

初始化与Circle判断

void init_graph(char* words[], int len):读入words,将word转化为图,并存入数据结构中。注意的是,单词自环并不能作为loop,如eye

bool check_circle():根据图的拓扑排序算法判断是否成环。以及需要特判单独字母自环,如aba, abba成环

Record
  • vector<Record> get_first_letter_main_records(int head, bool is_circle):根据首元素得到其所有主Record集

  • vector<Record> get_main_records(char head_letter, char head_not_letter, bool is_circle):调用get_first_letter_main_records函数,并同时根据首元素可以、禁止字母进行筛选,获取所有主Record集

  • vector<Record> get_child_records(Record record, char tail_letter):根据Record得到子Record集

  • vector<Record> get_all_child_records(vector<Record> main_records, char tail_letter):调用get_child_records,获得主Record集的子Record集集合

  • vector<Record> get_all_records(char head_letter, char tail_letter, char head_not_letter, bool is_circle):调用get_all_child_records和get_main_records,获得所有Record,并对尾元素进行筛选

  • 因此如果要判断尾元素,不论是否是最长链,必须要进行get_all_records

  • 内在逻辑是,有可能主Record(不满足条件)的子Record满足尾元素条件,该子Record是最长链

  • 如极端情况a->b->c->d,尾元素要求为c,那么最长Record为a->b->c

  • 而对于不作尾元素要求的最长链,就可以进行优化,仅获取主Record

链获取与存储
  • vector<string> get_chain_by_record(Record record):获得所有Record满足的链

  • int print_chains(char* result[], vector<string> chains):将链集合存储到result中

  • int print_chain(char* result[], string chain):将满足的一条链存储到result中

所有链

int get_all_chain(char* result[], char head_letter, char tail_letter, char head_not_letter, bool is_circle):实现获取所有链的算法。先调用all_records获得所有Record,再调用get_chain_by_record和print_chains将记录转变为链并存储输出

最长所有链

int get_max_length_chains(char* result[], bool is_word, char head_letter, char tail_letter, char head_not_letter, bool is_circle):对满足条件的Record,遍历判断。区别在于,最长单词链可以先判断出最长Record再获取链,而最长字母链则需要先获取链再判断。

子函数:

  • vector<string> get_max_length_word_chains_by_record(Record record):根据Record获取所有链

  • vector<string> get_max_length_letter_chains_by_record(Record record):根据Record获取最长字母,本质上是对get_max_length_word_chains_by_record所得所有链进行进一步筛选

最长任意链

int get_max_length_chain(char* result[], bool is_word, char head_letter, char tail_letter, char head_not_letter, bool is_circle):与最长所有链思路一致,但仅需要存储一条最长链

子函数:

  • string get_one_max_length_letter_chain_by_record(Record record, bool is_circle):根据Record获取一条最长链。由于相同Record下,最长字母链是最长单词链的特例,所以我们仅得到最长单词链即可

五、编译无警告图

[独立]六、计算模块UML图


七、性能改进


时间:100min

对于本项目算法,各函数的功能都和Record的数量相关。如一部分函数的功能是求解Record,另一部分是根据Record得到一定条件的chain。根据上图中,get_one_max_length_letter_chain_by_record函数(即根据Record找到最长字母链)占用资源最多。

根据调查,如果输出对tail不做要求,那么就不需要传入所有Record,而是只需要传入main Records。因此通过对tail==0条件进行特判,将该情况传入Record更改为传入main Record。结果如下图。可以说性能得到了极大的增强。这是因为一条main Record可以递归地生成许多sub Record,并且每条sub Record又对应许多chains。

[独立]八、Contract编程


Design by contract:规定软件设计者应该为软件组件定义形式化的、精确的和可验证的接口规范,它为抽象数据类型扩展了前置条件、后置条件和不变量条件。

优点:

  • 通过对接口进行前置条件、后置条件、不变量条件的预先设计,更加精细化接口,避免了不同人员可能对接口产生的误解

  • 单元测试时,前置条件、后置条件、不变量条件正是测试的边界。前置条件不满足可以测试其鲁棒性或错误处理,后置条件与不变量条件则是必须要满足的

  • 在考虑接口的前置条件时,可以同时做好异常的设计

  • 出现bug时,分工明确便于差错

缺点:

  • 接口设计过于精细的话,容易考虑过深,造成时间浪费,降低效率

  • 过于细致的规范可能会限制算法的优化

融入项目:

对于项目整体,并没有根据DbC方法进行项目的开展。但是在计算模块内部,对于各个函数的功能约束近似应用了该方法,如对于每一个函数,我们都会作出如下规范

  • 确保输入满足的条件(例:init_graph函数的输入head_letter仅可能为0或字母)

  • 函数内部应处理的异常(例:init_graph函数处理WordsOverflowException,CircleTypeException)

  • 输出应确保满足的条件(例:init_graph函数完成graph的构建)

  • 保持的不变量(例:get_max_length_chains函数应保证运行前后graph不改变,但允许中途改变)

九、计算模块部分单元测试展示


计算模块代码展示

 #include <iostream>
 #include "gtest/gtest.h"
 ​
 intgen_chains_all(char*words[], intlen, char*result[]);
 ​
 intread_file(char*words[], std::stringfile_path);
 ​
 intgen_chain_word(char*words[], intlen, char*result[], charhead, chartail, charreject, boolenable_loop);
 ​
 intgen_chain_char(char*words[], intlen, char*result[], charhead, chartail, charreject, boolenable_loop);
 ​
 intgen_chains_word(char*words[], intlen, char*result[], charhead, chartail, charreject, boolenable_loop);
 ​
 intgen_chains_char(char*words[], intlen, char*result[], charhead, chartail, charreject, boolenable_loop);
 ​
 char*words1[100];
 char*words2[200];
 char*words3[100];
 char*words4[100];
 char*words5[100];
 char*words6[100];
 char*words7[100];
 char*words8[100];
 char*words9[100];
 char*words10[100];
 ​
 TEST(read_file, r_f1) {
     EXPECT_EQ(20, read_file(words1, "word1.txt"));
 }
 ​
 TEST(read_file, r_f2) {
     EXPECT_EQ(100, read_file(words2, "word2.txt"));
 }
 ​
 TEST(gen_chains_all, gca1) {
     char*result[100];
     read_file(words3, "word3.txt");
     EXPECT_EQ(48, gen_chains_all(words3, 10, result));
 }
 ​
 TEST(gen_chains_all, gca2) {
     char*result[2000];
     read_file(words4, "word4.txt");
     EXPECT_EQ(1837, gen_chains_all(words4, 10, result));
 }
 ​
 TEST(gen_chain_word, gcw1) {
     char*result[100];
     read_file(words5, "word5.txt");
     EXPECT_EQ(10, gen_chain_word(words5, 10, result, 'n', 'h', 'o', false));
 }
 ​
 TEST(gen_chains_word, gcw2) {
     char*result[100];
     read_file(words6, "word6.txt");
     EXPECT_EQ(8, gen_chain_word(words6, 10, result, 's', 'x', 't', true));
 }
 ​
 TEST(gen_chains_word, gcw3) {
     char*result[100];
     read_file(words7, "word7.txt");
     EXPECT_EQ(10, gen_chain_word(words7, 10, result, 'z', 'd', 0, false));
 }
 ​
 TEST(gen_chain_char, gcc1) {
     char*result[100];
     read_file(words8, "word8.txt");
     EXPECT_EQ(20, gen_chain_char(words8, 20, result, 'z', 'x', 'h', true));
 }
 ​
 ​
 TEST(gen_chains_char, gcc2) {
     char*result[100];
     read_file(words9, "word9.txt");
     EXPECT_EQ(20, gen_chain_char(words9, 20, result, 'n', 'z', 0, false));
 }
 ​
 ​
 ​
 TEST(gen_chains_char, gcc3) {
     char*result[100];
     read_file(words10, "word10.txt");
     EXPECT_EQ(51, gen_chain_char(words10, 20, result, 'z', 'u', 'h', true));
 }
 ​
 intmain() {
     ::testing::InitGoogleTest();
     returnRUN_ALL_TESTS();
 }

测试的函数以及测试数据构造

单元测试主要测试了以下四个函数

 intread_file(char*words[], std::stringfile_path);
 ​
 intgen_chains_all(char*words[], intlen, char*result[]);
 ​
 intgen_chain_word(char*words[], intlen, char*result[], charhead, chartail, charreject, boolenable_loop);
 ​
 intgen_chain_char(char*words[], intlen, char*result[], charhead, chartail, charreject, boolenable_loop);
 ​
 intgen_chains_word(char*words[], intlen, char*result[], charhead, chartail, charreject, boolenable_loop);
 ​
 intgen_chains_char(char*words[], intlen, char*result[], charhead, chartail, charreject, boolenable_loop);

其中后三个函数为计算模块的接口函数,第一个函数只是为了方便从文件中直接读取单词到words中。

gen_chains_all

这一函数找出所有符合要求的单词链(在此可以看作-n)。由于这一环节无需考虑环的问题,因此随机生成一定数量的单词,并保证不构成单词环即可,如以下示例所示:

 # 生成单词的代码
 fromrandomimportrandint
 ​
 forcntinrange(10):
     filename = "word"+str(cnt+1) +".txt"
     file = open(filename, "w")
     c = chr(randint(ord('a'), ord('z')))
     foriinrange(20):
         word = c
         m = randint(3, 12)
         forjinrange(m):
             word += chr(randint(ord('a'), ord('z')))
         c = chr(randint(ord('a'), ord('z')))
         whilec == word[m]:
             c = chr(randint(ord('a'), ord('z')))
         word += c
         file.write(word+"\n")
     file.close()
 // 生成的单词
 aqhomctfqx
 xgsmwri
 iqpzjdu
 ukroumfyre
 ewjwzrx
 xvrfmatyf
 fnbntqxhf
 fhldsiryuufj
 jegvwlry
 yknvmmgpxbmf
 fwclwp
 pzbdps
 skdbafnbadi
 iiihoawdgshs
 suxwxvqesqeoz
 zibobraopn
 nuafwzvgotu
 ubhrkawvbvp
 ptbyidkdutztlv
 vmxauzm

gen_chain_word以及gen_chains_word

gen_chain_word以及gen_chains_word返回全部(或者单个最长单词链)。由于这里增加了首字符,尾字符,不能出现的首字符以及是否允许成环的限制,因此在测试时,需要同时覆盖这四种情形,以及他们的组合。

 // 仅为举例
 gen_chain_word(words, 10, result, 0, 0, 0, true);
 gen_chain_word(words, 10, result, 0, 0, 0, false);
 gen_chain_word(words, 10, result, 'a', 0, 0, true);
 gen_chain_word(words, 10, result, 0, 'a', 0, true);
 gen_chain_word(words, 10, result, 0, 0, 'a', true);

特别的,当enable_loop == false时,与上述构造数据方法类似,仍然不能出现环(否则需要产生异常)。而enable_loop == true时,可以产生环,因此在生成单词,可以删除对单词环的限制。

gen_chain_char以及gen_chains_char

与上述类似

测试覆盖率截图

其中lib为外部导入的文件夹(类似库文件,可以不考虑)。其余文件的测试覆盖率均达到了90%以上。

十、异常处理说明


输入模块异常

异常类

异常

输入参数

print_exception

说明

IllegalParameterException

出现非法参数

----

illegal parameter occurred!

RequiredParameterMissingException

缺少必要参数

----/-h

missing required parameter!/missing required parameter after -h!

缺少-n, -w, -c参数,或-h, -t, -j后缺少字母

ConflictParameterException

参数冲突

----

parameters not compatible!

多次出现-n, -w, -c

DuplicatedParameterException

同一参数出现多次

----

multiple occurrences of the same parameter!

ParameterTypeException

参数类型错误

-h

the parameter after -h should be a letter but not a string!

-h, -t, -j后的参数并非字母

FileNonExistException

文件不存在

file_name

file_name is non-existent!

FileNonReadException

文件不可读

file_name

file_name is not readable!

在文件存在的基础上判断

FileEmptyException

文件为空

----

words file is empty!

FileContentException

文件内容错误

c

contains unexpected character: c

其中c为非法字符(字母以外字符)

OneLetterWordException

单词仅有一个字母

word

have one-letter word: word

DuplicatedWordException

出现重复单词

word

have duplicated word: word

计算模块异常

异常类

异常

输入参数

print_exception

说明

WordsOverflowException

输入单词数过大

is_circle

words more than is_circle ? 100 : 10000 !

环最大为100,否则为10000

CircleTypeException

输入enable_loop与实际不匹配

is_circle

enable_loop is !is_circle but should be is_circle!

ResultOverflowException

输出链(单词)数过大

----

result chains more than 20000!

会返回输出数,但是否返回正确result并不保证

输出模块异常

异常类

异常

输入参数

print_exception

说明

NonMatchedChainException

输出链为空

----

not have matched chain!

我们并未在计算模块关注是否存在符合条件的链,而是在输出模块判断

FileNonWriteException

文件不可写

file_name

file_name is not writable!

如果不存在文件,则会创建文件;只有存在文件且不可写才会触发异常

计算模块单元测试样例


WordsOverflowException
 gcgvvmjcrffke
 ewqhnoahw
 wkwmok
 kjhsz
 zpesxg
 gcriiytkswted
 daxmgeydgsrrv
 vudmndopefpkiy
 yjwgslidvts
 sicjwnvqevero
 otajk
 ...
 ...
 bbjbstoojtju
 ufkxtq
 // 共20000个
输出:words more than 100!
CircleTypeException
gen_chain_word(words, 2, result, 0, 0, 0, false)
输出:enable_loop is !is_circle but should be is_circle!
ResultOverflowException
输入:类似WordsOverflowException 
输出:result chains more than 20000!

十三、结对过程


2023年3月7日,在宿舍进行项目的计划、step1的开发,总计6h。

2023年3月12日,三号楼进行step1的基本功能测试、step2的核心模块封装,总计4h。

2023年3月15日,图书馆咖啡厅进行step2的单元、回归测试,修改bug,step2的核心模块封装,step3异常实现,总计7h。

2023年3月17日,主楼进行step2的测试,修改bug,step2的输入输出模块封装,异常测试,总计7h。

2023年3月18日,主楼修改bug,博客写作,总计5h。

[独立]十四、结对编程优缺点


结对编程
优点
  • 在代码架构、接口设计阶段,双方可以很明确不同模块的功能,减少双方理解的模糊地带;同时对于算法的设计时,可以集两家之长

  • 在代码开发时,结对编程可以在开发过程中进行bug排查,减少代码中的错误和缺陷

  • 开发中,双方可以相互交流和学习,两个人掌握技术的覆盖面更大,减少了学习新技术的成本

  • 结对编程让双方共同保证项目的开发,互相监督,确保按时完成任务

缺点
  • 两个人一起工作,如果不能很好地沟通于合作,势必造成人力、时间的浪费,甚至效率不如分别进行

  • 双方一同工作,可能会导致编程时间拉长,造成精神上的压力

本人
优点
  • 能够对项目进行很好的架构,设计好接口与模块功能

  • 熟练掌握数据结构和算法,能够将函数功能很好实现

  • 较为擅长于他人交流

缺点
  • 没有使用过C++

  • 不善于测试,没有使用过VS相关插件

队友
优点
  • 代码风格规范,编程功底很好,能够很好地实现函数功能

  • 熟悉C++语法

  • 善于合作,可以很好地交流,愿意主动承担工作

缺点
  • 对附加模块(如GUI)不太有热情,缺乏对完美的追求

[独立]十五、PSP表格记录实际时间


PSP2.1

Personal Software Process Stages

预估耗时(分钟)

实际耗时(分钟)

Planning

计划

60

40

· Estimate

· 估计这个任务需要多少时间

60

40

Development

开发

1390

1720

· Analysis

· 需求分析 (包括学习新技术)

40

100

· Design Spec

· 生成设计文档

30

30

· Design Review

· 设计复审 (和同事审核设计文档)

30

20

· Coding Standard

· 代码规范 (为目前的开发制定合适的规范)

10

10

· Design

· 具体设计

90

120

· Coding

· 具体编码

840

900

· Code Review

· 代码复审

50

40

· Test

· 测试(自我测试,修改代码,提交修改)

300

500

Reporting

报告

180

160

· Test Report

· 测试报告

150

120

· Size Measurement

· 计算工作量

15

20

· Postmortem & Process Improvement Plan

· 事后总结, 并提出过程改进计划

15

20

合计

1630

1920

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值