项目 | 内容 |
---|---|
这个作业属于哪个课程 | 课程社区的链接 |
这个作业的要求在哪里 | 作业要求的链接 |
我在这个课程的目标是 | 掌握软件开发中常用的结对编程模式 |
这个作业在哪个具体方面帮助我实现目标 | 了解软件团队中如何利用结对编程来提升开发效率 |
文章目录
- [BUAA软工第三次]结对编程
- 结对编程作业
- 班级及项目地址
- PSP表格
- 编程原则
- Information Hiding
- Interface Design
- Loose Coupling
- 计算模块接口的设计与实现过程
- 方法设计
- 代码组织
- UML
- 计算模块接口部分的性能改进
- Design by Contract, Code Contract
- 8 计算模块部分单元测试展示
- 9 计算模块部分异常处理说明
- 9.1 参数冲突异常
- 9.2 指定首尾字母异常
- 9.3 缺少关键参数异常
- 9.4 输入参数种类异常
- 9.5 文件不存在异常
- 9.6 非法循环异常
- 9.7 答案过长异常
- 界面模块
- 设计过程
- CLI
- GUI
- 与计算模块的对接
- 结对编程
- 结对过程
- 优点
- 缺点
- 成员互评
[BUAA软工第三次]结对编程
结对编程作业
发表在你的个人博客上,也可以同时转发到你的团队博客上来增加你们团队博客的人气。博客共 50 分,具体要求如下:
班级及项目地址
- 教学班级:周五班
- 项目地址:GitHub库
PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 60 |
· Estimate | · 估计这个任务需要多少时间 | 60 | 60 |
Development | 开发 | 940 | 850 |
· Analysis | · 需求分析 (包括学习新技术) | 60 | 90 |
· Design Spec | · 生成设计文档 | 60 | 60 |
· Design Review | · 设计复审 (和同事审核设计文档) | 30 | 30 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 10 |
· Design | · 具体设计 | 120 | 150 |
· Coding | · 具体编码 | 180 | 150 |
· Code Review | · 代码复审 | 180 | 120 |
· Test | · 测试(自我测试,修改代码,提交修改) | 300 | 240 |
Reporting | 报告 | 170 | 200 |
· Test Report | · 测试报告 | 120 | 150 |
· Size Measurement | · 计算工作量 | 20 | 20 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 30 |
合计 | 1170 | 1110 |
编程原则
Information Hiding
在计算机科学中,信息隐藏(Information Hiding)是将计算机程序中最有可能发生变化的设计决策隔离开来的原则,从而保护程序的其他部分在设计决策发生变化时不被大量修改。这种保护包括提供一个稳定的接口,保护程序的其余部分不受实现(其细节可能会改变)的影响。
在我们的实现中,我们依照这个原则将两大关键板块分割开来,IO系统和计算模块两者互不干扰。在这两个关键板块之内我们也细分了更精密的信息隐藏方法,比如在IO系统中,读取文件和手动输入两个方法互不耦合,只对最终的借口提供一个字符串;在计算模块中,字符串处理和核心方法互不干涉,核心方法只需要接受一个处理妥当的字符串,而不需要关心其具体处理方法;同时在最终的结果输出,针对GUI和CLI,我们只需要在输出系统接受一个结果并且根据具体环境来实现输出方法,完全没有和计算模块耦合。这三个例子只是我们实现方法中很小的一部分体现信息隐藏原则的地方。
Interface Design
用户界面设计(Interface Design)的重点是最大限度地提高可用性和用户体验。在计算机或软件设计中,用户界面设计是建立具有美感的界面的过程;UI设计师建立易于使用和使用感良好的界面,在完成用户目标的前提下,使用户的交互尽可能的简单和有效(以用户为中心的设计)。一个良好的图形用户界面(GUI)往往可以增强软件的可用性,让用户摆脱命令行界面(CLI)的繁琐。
关于我们的UI设计请移步后文GUI。
Loose Coupling
在计算和系统设计中,松耦合指:
- 其中组件之间的联系很弱(有可中断的关系),因此,一个组件的变化对另一个组件的存在或性能影响很小。
- 在这个系统中,每个组件对其他独立组件的定义知悉甚少或一无所知。
我们在实现功能时候尽可能地遵守松耦合的原则,除了在上面信息隐藏的过程中实现的松耦合,我们在核心计算模块更加重视松耦合。我们的核心计算模块Processor.cs
主要有以下两个关键部分:ConcatTree.cs
、ResGen.cs
,他们完全独立。其中:
ConcatTree.cs
产生我们的方法依赖的单词树ResGen.cs
处理单词树以生成结果
另外,我们在Core.cs
中封装所有的方法,其内容和具体的实现方法也是相互独立的,只需要传入一个Processor
并且接受ResGen
的返回值。
计算模块接口的设计与实现过程
方法设计
我们的方法主要基于建立和维护一个由多叉树组成的森林(Wikipedia: Forest is a collection of disjoint trees. In other words, we can also say that forest is a collection of an acyclic graph which is not connected. Here is a pictorial representation of a forest)。一个叶节点的构造为:一个子叶节点群和一个自身的值。用 C#
描述为:
class ConcatTree{private List _kids;private string _val;}
在建立森林之前,我们先处理输入。对于输入,我们建立一个单词字典,将词语和首字母联系起来,其构造用 C#
描述为 Dictionary<char, List>
。比如,若输入为:
Heaven!#@(*$element TABLE:teach!0tAlK
我们则将其处理为:
{['e': "element"], ['h': "heaven"], ['t': "table", "teach", "talk"]}
在建立森林的过程中,我们以给定的单词组中的每一个单词为根,分别建树。即有 nnn 棵树就有 nnn 棵树。在建立时,我们采取这样的方法:对于一个单词树节点,将其值的尾字母作为子节点群的首字母;在字典中检索这个字母的单词,将它们依次加入子节点;对所有子节点重复该操作。
至此,一个完整的单词森林已经建立完毕,之后在尝试获得所有单词链结果时,遍历每一课树并每次记录结果即可。
代码组织
我们将核心计算模块、文件读入和输出模块封装成相互独立的部分。现在整个项目的文件结构为:
├── Core│ ├── ConcatTree.cs│ ├── Core.cs│ ├── Core.csproj│ ├── LoopException.cs│ ├── OverflowException.cs│ ├── Processor.cs│ ├── WordChain.cs│ └── WordsGen.cs├── FileOutput│ ├── FileOutput.cs│ └── FileOutput.csproj├── FileReader│ ├── FileNotFoundException.cs│ ├── FileReader.cs│ └── FileReader.csproj└── WordList├── WordList│ ├── ArgsConflictException.cs│ ├── ArgsMissCharacterException.cs│ ├── ArgsMissNecessaryException.cs│ ├── ArgsParser.cs│ ├── ArgsShouldBeCharException.cs│ ├── ArgsTypeException.cs│ ├── Program.cs│ ├── WordList.csproj└── WordList.sln
三个独立模块和主项目文件平行。
在与交换测试小组商讨完毕后,我们决定将我们的接口设计为:
// -npublic static int gen_chains_all(HashSet words, int len, ArrayList result) { ... }// -wpublic static int gen_chain_word(HashSet words, int len, ArrayList result, char head, char tail, bool enable_loop) { ... }// -mpublic static int gen_chain_word_unique(HashSet words, int len, ArrayList result) { ... }// -cpublic static int gen_chain_char(HashSet words, int len, ArrayList result, char head, char tail, bool enable_loop) { ... }
UML
计算模块接口部分的性能改进
根据输入:
ab ba ac ca ad da ae eabd db bf fb bq qbca ac cb bcpo op
我们运行Visual Studio的性能分析工具,得到:
- 改进前CPU及耗时:
- 改进前各函数执行时间占比:
我们发现性能损耗最大的地方在建立单词树的相关方法上。我们针对这部分核心方法进行的优化主要有:- 剪枝递归;
- 优化数据初始化方式;
- 避免滥用反射;
- 尽量使用
foreach
代替for
。
优化后,我们再传入相同的数据运行性能分析工具,得到:
- 改进后CPU及耗时
- 改进后执行时间占比
可以发现,改进后CPU占用和运行时间都有较大改善。CPU占用从平均 40 %40\%40% 降低到 30 %30\%30%,降低了 25 %25\%25%,并且运行时间也降至了之前的 68 %68\%68%,加速比大约为 1.51.51.5。
Design by Contract, Code Contract
契约设计(DbC),也被称为契约编程、契约编程和按契约设计编程,是一种设计软件的方法。它规定软件设计者应该为软件组件定义正式的、精确的和可验证的接口规范,这些规范扩展了带有前提条件、后置条件和不变量的抽象数据类型的普通定义。这些规范被称为 “合同”,与商业合同的条件和义务在概念上有一个比喻。
契约的思想也很简单,就是一组结果为真对表达式,如果违反,则契约失效。
在DbC中,使用者和被调用者地位平等,双方必须彼此履行义务,才可以行驶权利。调用者必须提供正确的参数,被调用者必须保证正确的结果和调用者要求的不变性。双方都有必须履行的义务,也有使用的权利,这样就保证了双方代码的质量,提高了软件工程的效率和质量。
然而,缺点是对于程序语言有一定的要求,契约式编程需要一种机制来验证契约的成立与否。而断言显然是最好的选择,但是并不是所有的程序语言都有断言机制。那么强行使用语言进行模仿就势必造成代码的冗余和不可读性的提高。此外,契约式编程并未被标准化,因此项目之间的定义和修改各不一样,给代码造成很大混乱,这正是很少在实际中看到契约式编程应用的原因。
在我们用 C#
的实现中:
Contract.Assert()
:断言是最基本的契约,断言失败时CLR仅调用DEBUG.Assert,成功时什么都不做;Contract.Assume()
:假设契约,运行时验证检查和Assert
一样;Contract.Requires()
:前置条件契约,通常用作方法参数的验证;Contract.Ensures()
:后置条件契约,通常用作方法返回值验证。
8 计算模块部分单元测试展示
利用VS的单元测试模块进行测试,对于每个接口,针对输入参数的所有可能组合(head,tail,enable_loop)进行测试。将得到的result与正确的realResult挨个元素进行比较,确保单词链生成正确,利用equalList函数进行比较,函数及测试样例如下:
public void eqaulList(ArrayList result, ArrayList realResult){for (var i = 0; i < result.Count; ++i){Assert.AreEqual(result[i], realResult[i]);}}
[TestMethod()]public void gen_chain_word_blendTest(){HashSet words = new HashSet { "abb", "bbc", "ccd", "dde", "wq", "qwe", "ert", "cew", "bw" };int len = words.Count;ArrayList result = new ArrayList();//test with head and tailchar head = 'b';char tail = 'd';bool enable_loop = false;int cnt = Core.gen_chain_word(words, len, result, head, tail, enable_loop);ArrayList realResultWithHeadAndTail = new ArrayList { "bbc", "ccd" };Assert.AreEqual(cnt, realResultWithHeadAndTail.Count);eqaulList(result, realResultWithHeadAndTail);//test with head and loophead = 'w';tail = '\0';enable_loop = true;words = new HashSet { "bc", "cw", "wb", "ber", "rw", "wc" };len = words.Count;result = new ArrayList();ArrayList realResultWithHeadAndLoop = new ArrayList { "wc", "cw", "wb", "ber", "rw" };cnt = Core.gen_chain_word(words, len, realResultWithHeadAndLoop, head, tail, enable_loop);Assert.AreEqual(cnt, realResultWithHeadAndLoop.Count);eqaulList(result, realResultWithHeadAndLoop);//test with tail and loophead = '\0';tail = 'd';enable_loop = true;words = new HashSet { "abc", "cwa", "acd", "dwq", "cde", "efg" };len = words.Count;result = new ArrayList();ArrayList realResultWithTailAndLoop = new ArrayList { "abc", "cwa", "acd" };cnt = Core.gen_chain_word(words, len, realResultWithTailAndLoop, head, tail, enable_loop);Assert.AreEqual(cnt, realResultWithTailAndLoop.Count);eqaulList(result, realResultWithTailAndLoop);//test with head, tail and loophead = 'q';tail = 'w';enable_loop = true;words = new HashSet { "asd", "dfg", "qwe", "ewq", "qta", "qw" };ArrayList realResultWithHeadTailAndLoop = new ArrayList { "qwe", "ewq", "qw" };len = words.Count;cnt = Core.gen_chain_word(words, len, realResultWithHeadTailAndLoop, head, tail, enable_loop);Assert.AreEqual(cnt, realResultWithHeadTailAndLoop.Count);eqaulList(result, realResultWithHeadTailAndLoop);}
对所有可能出现的参数组合构造了相应样例,最终代码覆盖率如图:
9 计算模块部分异常处理说明
9.1 参数冲突异常
根据题意我们可以知道,-n/-m指令不能与-h/-t/-r复合使用,同时-n/-w/-m/-c四条指令只能出现一条,在ArgsParser中分析输入时,若存在上述指令的错误复合,则会抛出ArgsConflictException异常 。
为此设计单元测试样例,穷举了所有可能的错误组合,样例如下:
[TestMethod()]public void ArgsConflictExceptionTest(){string[] args = new string[] { "test.txt", "-n", "-w" };ArgsParser argsParser;Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "test.txt", "-n", "-w" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "test.txt", "-n", "-c" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "test.txt", "-n", "-m" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "test.txt", "-n", "-w", "-c" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "test.txt", "-n", "-w", "-m" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "test.txt", "-n", "-c", "-m" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "test.txt", "-n", "-w", "-c", "-m" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "test.txt", "-w", "-c" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "test.txt", "-w", "-m" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "test.txt", "-w", "-c", "-m" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "test.txt", "-c", "-m" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "test.txt", "-n", "-h", "a" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "test.txt", "-n", "-t", "a" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "test.txt", "-n", "-r" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "test.txt", "-n", "-r", "-h", "a" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "test.txt", "-n", "-r", "-t", "a" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "test.txt", "-n", "-r", "-h", "a", "-t", "a" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "test.txt", "-n", "-h", "a", "-t", "a" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "test.txt", "-m", "-h", "a" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "test.txt", "-m", "-t", "a" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "test.txt", "-m", "-r" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "test.txt", "-m", "-r", "-h", "a" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "test.txt", "-m", "-r", "-t", "a" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "test.txt", "-m", "-r", "-h", "a", "-t", "a" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "test.txt", "-m", "-h", "a", "-t", "a" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));}
9.2 指定首尾字母异常
当使用-h/-t指令时,后面需要紧跟其所需的字母,在ArgsParser中分析输入时,若不存在紧跟着的字母,则抛出ArgsMissCharacterException异常,表示缺少紧跟字母;若紧跟的参数长度大于1,则抛出ArgsShouldBeCharException异常,表示紧跟参数不是单个字母。
对于前者,直接构造-h/-t后不紧跟字母的样例,如下:
[TestMethod()]public void ArgsMissCharacterExceptionTest(){string[] args = new string[] { "test.txt", "-w", "-h" };ArgsParser argsParser;Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "test.txt", "-w", "-t" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));}
对于后者,构造-h/-t紧跟长度大于1的参数的样例,如下:
[TestMethod()]public void ArgsShouldBeCharExceptionTest(){string[] args = new string[] { "-w", "-h", "ab", "test.txt" };ArgsParser argsParser;Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "-w", "-t", "-r", "test.txt" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "-w", "-t", "test.txt" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "-w", "-h", "test.txt" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));}
9.3 缺少关键参数异常
依据题意,输入指令参数有且仅有-n/-w/-m/-c中的一个,在ArgsParser中分析输入时,若输入参数中四者都不存在,则抛出ArgsMissNecessaryException异常。
为此,构造出不存在上述四个指令的样例,如下:
[TestMethod()]public void ArgsMissNecessaryExceptionTest(){string[] args = new string[] { "test.txt" };ArgsParser argsParser;Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "test.txt", "-r" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "test.txt", "-h", "h" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "test.txt", "-t", "h" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));}
9.4 输入参数种类异常
所支持参数仅包括-n,-w,-m,-c,-h,-t,-r及.txt
文件路径,在ArgsParser中分析输入时,若输入参数不属于其中之一,则抛出ArgsTypeException异常。
为此,构造几个不属于不属于上述参数的样例,如下:
[TestMethod()]public void ArgsTypeExceptionTest(){string[] args = new string[] { "-q", "test.txt" };ArgsParser argsParser;Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "-n", "notxt" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));args = new string[] { "-x", "notxt" };Assert.ThrowsException(() => argsParser = new ArgsParser(args));}
9.5 文件不存在异常
在FileReader中,对于输入参数中的.txt
文件路径,若该路径不存在,则抛出FileNotFoundException异常。
对此,构造几个不存在的路径传入FileRead进行测试,如下:
[TestMethod()]public void ReadFileAsStringExceptionTest(){string path = "C;\\no_exit.txt";FileReader fileReader = new FileReader(path);Assert.ThrowsException(() => fileReader.ReadFileAsString());path = null;fileReader = new FileReader(path);Assert.ThrowsException(() => fileReader.ReadFileAsString());}
9.6 非法循环异常
对于输入的文本,若能构成单词环且没有-r或-m参数,则抛出LoopException异常。
对此,构造在不允许构成单词环的情况下输入能够形成单词环的样例,如下:
[TestMethod()]public void gen_chain_word_loopTest(){HashSet words = new HashSet { "abc", "cwa", "acd", "cew", "wq", "dwq" };int len = words.Count;ArrayList result = new ArrayList();//test with loopchar head = '\0';char tail = '\0';bool enable_loop = true;int cnt = Core.gen_chain_word(words, len, result, head, tail, enable_loop);List realResultWithoutTail = new List { "abc", "cwa", "acd", "dwq" };Assert.AreEqual(cnt, realResultWithoutTail.Count);Assert.AreEqual(true, validChain(result, enable_loop));//test without loopenable_loop = false;Assert.ThrowsException(() => cnt = Core.gen_chain_word(words, len, result, head, tail, enable_loop));}
9.7 答案过长异常
根据题意,Core里接口中的result答案长度若大于20000则抛出OverflowException异常。
对此,构造一个较长的样例使其答案长度能超过20000,如下:
[TestMethod()]public void overFlowTest(){HashSet words = new HashSet { "aa", "ab", "bb", "bc", "cc", "cd", "dd", "de", "ee","ef", "ff", "fg", "gg", "gh", "hh", "hi", "ii", "ij","jj", "jk", "kk", "kl", "ll", "lm", "mm"};int len = words.Count;ArrayList result = new ArrayList();int cnt;Assert.ThrowsException(() => cnt = Core.gen_chains_all(words, len, result));}
界面模块
设计过程
CLI
在针对 CLI
的界面交互设计中,我们严格按照课程组要求的参数输入方式支持基于命令行的交互。为了让 CLI
尽可能清晰明了,我们在原有的几个参数基础上新增了帮助指令,用户可以通过 help
或者 --help
在终端中呈现使用提示和规范:
Word List=========Usage: Wordlist [options]Functions: -nGet the numeration of word chains. -wGet the word chain with most words. -mGet the word chain with most words of different starting letter. -cGet the word chain with most letters.Options: -h Specify the starting letter of the chain. -t Specify the ending letter of the chain. -rAllow potential word circles like "ab"-"ba".
GUI
在选择制作 GUI
的工具时,考虑到UnityEngine和C#语言的天然融合性,我们最终采用Unity2D来完成我们的图形界面。秉持简洁、强引导的用户界面设计理念,我们在界面的设计上尽可能清晰地将所有功能合理排布在图形界面上:
并且在使用过程中,我们使用状态机来管理用户使用状态,避免程序不支持的指令产生,以提高软件的可靠性并且降低用户在使用过程中的疑惑。比如,我们规定在选择指定单词链首字母、指定单词链尾字母、允许隐含单词患之前必须选取一个基本功能,否则这三个选项将不可选;另外,在选择了一个基本功能后,其他三个基本功能将不可选。这样的状态管理在用户使用环节即把有关指令的错误全部排除了。
与计算模块的对接
无论是CLI还是GUI,我们的计算模块对接都很简单,即载入核心计算模块 Core.dll
,然后根据特定环境开发IO模块。例如在CLI中,我们根据命令行交互的单源特殊性编写的 FileReader
读入模块;然而,在GUI中,支持手动输入和文件上传,我们则另外根据UnityEngine的组件生命周期开发针对GUI的读入模块。最重要的是,计算模块都是一致普适的;在CLI和GUI中,都只需要 using Core;
即可完成对接。
结对编程
结对过程
在编码交流中,我们主要采用线上会议的方式进行屏幕共享或远程控制。
优点
更少的错误和bug
软件开发人员通常是独自工作,这可能会导致像固执和隧道视野这样的负面特征。当你试图修复一个错误时,很容易因为一个不正确的假设、一个隐藏的错别字或你知识上的差距而被卡住。
然而,当你结对编程时,你被迫作为一个团队工作。这自动给了代码更多的 “质量控制”。伙伴双方利用他们共同的经验和知识,在问题出现时更快地解决问题。
更高的代码质量
结对编程伙伴之间共同的代码实现会带来更好的整体代码质量。伙伴双方必须对彼此负责,因为任何bug不管出自哪一方,最终都会或多或少落到自己头上,这使两个成员都不愿意走任何捷径或者做出任何欠妥当的决定。结对编程鼓励团队建立强大的解决方案,不会在以后产生意想不到的错误。提高团队士气
结对编程让我们在项目中与其他人交谈。团队能与个体产生共鸣,并帮助解决问题,这有助于使整个团队更有效率,更有充实感和成就感。
更快的培训进展
结对编程的伙伴通常是两个专家或一个专家和一个新手。在后一种情况下,结对编程允许新成员从他们更有经验的同事那里获得信息,这可以极大地加快新成员的入职过程。
更大的团队弹性
如果一个项目完全依赖于一个人,那么公司在人员调动上会非常被动。结对编程在很大程度上可以解决这个问题。在一个项目上,至少有两个人应该熟悉代码库的每一部分,而不是只能依靠一个人。这有助于防止由于人员流动造成的意外的项目减速和延误。
缺点
对工作内容和团队会非常挑剔
普遍来讲,结对编程会非常重视伙伴间的合作,即是说交流和思想一致在结对编程中会非常重要。这也就要求两个结对编程伙伴必须尽量保证编程思想大体一致,并且在交流上不成问题。否则,如果结对伙伴一直不和,结对编程反而会拖慢整体进度。
成员互评
肖伟强 | 刘兆薰 |
---|---|
对新工具新框架精通很快,面向对象知识非常扎实 | 代码效率高,很快就完成了基础框架的书写 |
阅读代码能力很强,可以很快找到程序出错点并且提出高效的解决办法 | 沟通效率高,对于一些问题或者bug很快能讲清楚并解决 |
沟通效率高,很快可以确定好接下来的任务 | 性格好,相处十分愉快 |
对C#有些语法不是很熟悉 | 有些时候会先行而后思导致一些低级错误 |