协作新艺术——结对编程总结
教学班 | 罗杰、任建班周五3、4节 |
---|---|
gitlab项目地址 | Here it is. |
成员 | 周远航(3004) 李辰洋(3477) |
结对项目实践反思
实践中出现的问题
在此次结对项目——内存中文件系统的实现中,我组在两次强测中均通过了测试,然而,在这背后,是在大大小小的问题不断出现和被解决后产生的结果。出现的典型问题有:
下手仓促,没有充分思考
由于本次作业的需求相对复杂,且包含多种边界情况,因此理想的做法是做好系统的需求分析,对方法的每个分支都做好准备后再行实现。然而,有时为了追求效率,我们在了解指令的大概需求后就开始下手,导致后期需要不断补充对特殊情况的处理,降低了实现的效率,破坏了工作的原子性。
该问题出现的根源,其一是时间受限,其二是心急,其三是指导书的需求变动较多。
所幸由于最终修补工作做得比较全面,没有造成失分。
测试过长
在写单元测试时,我们以方法为单位构造了多种、多个复杂的测试用例。但是,我们存在一个排版失误,就是将针对一个方法的所有用例集中在了一个方法中,且缺少注释分割。这导致后期回看和debug时造成了很大的可读性难度。在第二次作业时,我们发现了此问题并进行了一定调整,注重了单个测试方法的长度和注释情况。
以上是两个典型的问题,当然我们在结对项目的需求分析、架构设计、功能实践中的进度、质量、沟通管理等多个方面遇到了或多或少的问题,将在下文各阶段展开详述。
需求分析实践体会
本次结对项目最难也最令人头痛的一个地方,正是在于对该项目每阶段的需求分析,也就是阅读和分析指导书对于阶段任务的相关明确要求。在每个阶段任务的开始,最重要的即使先对指导书进行阅读,了解指导书所规定的主要功能需求,以此来导向和约束我们接下来对项目框架的设计和实现。如果没有前期对指导书的精细阅读和深入分析,那么在结构设计和代码实现中就容易走弯路,甚至可能需要重构。
与此同时,在代码的编写过程中,我们也需要不断对目前所实现的功能同指导书所规定的需求进行比较和检查,以保证功能实现的正确性,而测试驱动的编程方法帮助我们减少了可能出现的对需求的理解偏差。
在我们结对项目的两个阶段的开发中,都在需求分析这一阶段下足了功夫,“万丈高楼平地起”,我们认为,宁愿多花点时间读懂指导书,也不要轻易开始编写代码,正是由于在最初的需求分析中的多一分付出,在后面的代码编写中,反而没有太多需要在功能实现上出现问题而大改的地方,但确实也存在着对需求分析不够充分而出现的一些bug,例如,在第一次作业中,编写到最后,才发现对于path/
这种结尾带有/
的路径,我们的功能实现完全对此疏忽了考虑。
另外,还有部分bug的出现来源于指导书语义不清、存在歧义等情况,我们在对指导书的分析和代码的编写中,基于自己的理解,也为指导书的勘误或更好的表述献出了自己的一份力,例如,第一阶段中关于mkdir -p
的issue14,第二阶段中关于ln -s
异常顺序的issue3,关于创建软链接特殊情况的疑惑的issue4,关于<dstpath>/<srcpath>
输出路径格式问题的issue12,关于cp
指令是否重定向硬连接的issue26。并且,助教们对于issues积极的回复和切中肯綮的解答,真正让我们感受到这门课程的与众不同。
架构设计实践体会
对于本次结对项目的架构设计,应该属于其中较为成功的一个地方了。在最初的设计中,我们以Linux中“一切皆文件”的理念为思路,并且以Google开源Jimfs文件系统的设计框架为灵感,构建了我们最初的体系架构,即从File抽象类开始,继承构建所有需求对象,我们利用多态的思想,统一管理以File为父类的目录和普通文件,在第二阶段中软硬链接文件的加入,更是充分体现了这种设计在可扩展性上的巨大优势。正是这种贴合Linux文件系统设计思路,外加设计精妙的框架结构,让我们在迭代中游刃有余,而这些设计,离不开精心的雕琢与前期对需求的细致分析。
在性能优化上,第二次博客-实现过程部分已经有所提及,我们利用JProfiler,以构造出来的极限数据为基础,进行我们的项目性能测试和分析,在对绝对路径的计算中,最终权衡利弊,放弃了缓存机制,改为使用Stringbuilder循环计算的方式生成绝对路径。除了此处改动,我们在后期,又对普通文件的content部分改用StringBuilder对象,并且针对@n
替换问题,设计了效率更好的replaceAll方法,使得在内存资源占用基本不变的基础上,针对fappend
指令在不断累加content的情况下,所需时间缩小为原来的1/10。
由于一开始的设计便抽象了文件,架构较好的可扩展性让我们有关架构设计不足导致的问题较少,但在设计中却是碰到相关的几个问题:
1、在对文件的查找和获取方法的编写中,最初在设计时将其置为一个专属于MyFileSystem的私有方法,但是随着指令的增加、条件的增多以及文件类型的多样化,此类方法越来越多,且需要利用文件查找的方法不仅仅只是MyFileSystem了,因此,我们调整了设计,为其构建一个工具类并封装起来,降低了结构的耦合性,同时提高了代码的可复用性。
2、同样针对这个查找文件的工具类,由于在设计时疏忽了对Exception类型的设计,使得在不同情况下对异常的抛出常常为同一类型,这导致了在特定情况需要捕获某些异常,反而捕获了其他无需处理的异常,导致了bug的出现。
进度、质量和沟通管理实践体会
进度管理
对于进度管理,主要通过切分任务的方式推动,对于一次作业,切分为完成基础功能、通过弱测、完成代码覆盖测试、完成边界条件和特殊情况测试几个大块,根据作业最终结束时间和前期任务量估计来进行时间段的划分。
对比两个阶段的个人开发流程PSP(Personal Software Process):
PSP2.1 | Personal Software Process Stages | stage_1预估耗时(分钟) | stage_1实际耗时(分钟 | stage_2预估耗时(分钟) | stage_2实际耗时(分钟 |
---|---|---|---|---|---|
Planning | 计划 | 90 | 90 | 60 | 40 |
Estimate | 估计这个任务需要多少时间 | 360 | 600 | 600 | 1200 |
Development | 开发 | 270 | 300 | 300 | 500 |
Analysis | 需求分析 (包括学习新技术) | 30 | 30 | 30 | 30 |
Design Spec | 生成设计文档 | 10 | 10 | 10 | 10 |
Design Review | 设计复审 (和同事审核设计文档) | 10 | 10 | 10 | 10 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 0 | 0 | 0 | 0 |
Design | 具体设计 | 30 | 30 | 30 | 30 |
Coding | 具体编码 | 120 | 120 | 300 | 500 |
Code Review | 代码复审 | 120 | 300 | 300 | 1000 |
Test | 测试(自我测试,修改代码,提交修改) | 120 | 300 | 500 | 1000 |
Reporting | 报告 | 60 | 60 | 60 | 60 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 20 | 20 | 20 | 20 |
可以看到,本次开发项目的进度流程有以下特点
- 计划阶段用时较少,因为架构方面队员已经做到心中有数,达成共识。
- 预估总用时远少于实际用时,在项目初期难以准确预估需要的时间。
- 尽管stage_2的代码量要小于stage_1,但无论是开发时间、代码复审环节还是测试环节,stage_2花费的时间都远多于stage_1。这是由于第二阶段提出的指令需求虽然数量较少,但是实现难度更大,而且指导书的研读过程更加困难。另外,由于stage_2的测试难度较大,对标准答案的质疑也较多,我们也与其他组进行了题目理解上的交流(鸣谢),虽然花费了一些时间,但为我们的后期实现上了保险。
- 虽然stage_2的指令复杂,但是整体实现思路较为简单,因此设计上第二阶段并没有花费比第一阶段长的时间。
综合来看,我组的架构设计能力和编码能力较强,能够做到短时间内完成完善的架构设计和主要的编码工作。但是,我组的测试能力和对细节的把控能力相对较弱,是后期需要改进的方向。
显然,从学生到职业程序员,并不是更加没完没了地写程序—花在写代码上的时间反而少了许多。
——《构建之法》2.3 个人开发流程
通过构建之法中相关章节对软件工程师和大学生编码特点的分析,我们发现我组在项目用时上类似软件工程师,即会在需求分析和测试上花费更多的时间。但我们所花时间发挥的真正价值和效能还有很大不足。
质量管理
对于质量管理,主要通过代码规范和代码复审来使得项目中的代码可读性高,且经过第二人审查,从而保证了高质量代码的编写,同时,测试驱动的代码编写和回归测试也大大提高了项目的质量。
沟通管理
对于沟通管理,我们会在前中期线下见面进行需求分析、框架设计和搭建、以及主要代码的编写,而对中后期的完善,主要通过git仓库同步两人项目进度,并且依靠标注注释来提出疑问、标示修改等,通过实践,这不失为一种高效且精确的沟通方式。
上图为我们在项目迭代开发中总共提交的commit数量,正是由于双方需要不断沟通交流、及时同步修改,因而产生了如此多的commit。
由于线下沟通依旧存在局限性,还是有着由于沟通缺失导致双方理解不一致的问题。在关于mv中需要循环修改目录所属文件和子目录modify_time的功能上,由于L同学在功能完成2/3时临时有事,说了几句模棱两可的话,希望剩下的功能能够由Z同学完成,然而Z同学理解为这块功能已经实现,于是双方都没有再去继续实现剩下的功能,导致这个bug被遗留,且在不经意间才被发现,令人虚惊一场。
结对项目实践建议
- 规范代码格式:在两人编程时,要注重代码格式规范,这才能让领航者更好的理解代码,同时,详尽的注释是必不可少的,除了帮助为他人解释关于此处的功能、参数、异常、返回值等信息,也能够作为一种协作中沟通交流的好方法。
- 分工明确、公平公正:结对编程中,最有可能发生的事情就是只有一个人在写代码,而另一个人无所事事,在他人写代码的同时,领航员更应充分集中精神,在及时理解同伴代码思路的同时,还需要及时思考方法实现的正确性、可行性,而领航员也没必要一直做领航员,将一个任务分割开来,对于这个小功能,领航员实行领航职责,而对于另一个小功能,可以让双方职责反转,这避免了由于双方任务量悬殊而导致的不公平,同时,在教学中也更好的让双方尽可能多的参与结对编程中的每个角色。
CI体验
使用
阶段设计
最初,我们在CI设置了如下三个阶段:
- demo:build + 小样运行
- test:单元测试
- submit:提交测评
后由于小样的测试对于本次结对项目提交意义不大,且为了精简CI流程,我们调整后仅保留两个阶段:
- test:build+单元测试
- submit:提交测评
触发条件调整
为了让我们的开发生态更贴近真实的软工现场,我们开辟了alpha分支,并在该分支上进行主要的开发工作,频繁版控,并在版本达到稳定时merge到master分支。但是,由于alpha分支并不代表完善的代码,在此分支触发submit阶段没有意义且浪费评测资源,因此我们对submit分支进行了触发条件限制,即添加仅master分支触发。
私有变量使用
在学习中,我们了解到了CI添加宏定义变量的两种方法:
- 添加到yml中,即公开变量
- 添加到项目的CI设置中,即私有变量
为了更加美观地在CI中使用官方包,我们学习了这个分享中使用的方法,将官方包解压密码等涉及隐私安全的变量作为私有变量进行设置,对官方包下载路径、命名等非敏感信息作为公开变量进行定义,通过CI实现了官方包的下载、解压、使用。
单元测试覆盖率渲染
我们使用maven的cobertura进行单元测试并使用coverage关键字渲染覆盖率,在README中展现结果。
感受
通过结对项目,我们体验了CI/CD工具带来的便捷和舒适。每次修改后触发的CI测试可以帮助我们实时反馈项目的build及unittest结果,一定程度上反应了该阶段工作的有效性。另外,通过对单元测试的持续补充,test阶段还能帮助我们进行回归测试,查看是否引入了新bug。
但是,由于本次项目的规模较小,且仅局限于课程作业,没有部署和交付环节,因此对CD阶段的感受较少。但或许触发测评的过程也可以类比为CD,指把程序部署or交付给评测机?
另外还有一点针对gitlab-ci的感受,就是运行速度相对较慢,等待是煎熬的。
结对编程感想
结对方法
我们每一周期的结对基本都是以下流程:
从发布作业到实现基本架构,我们一直采用面对面讨论和编程的方式,将此任务集中在一天时间内完成。
在之后的测试环节,由于战线长、任务零碎难以集中,我们很难在同一时间线下交流,因此多采用线上交流,确定修改后领活,修改完后汇报的方式循环迭代进行。
我认为我们的结对方法在现实条件下属于比较高效的。对于关键步骤,我们严格按照结对要求进行;对于细节。由于架构的高内聚低耦合特性,我们可以将各个功能板块分离,单独实现,提高了任务的并行程度。当然,我们也遇到了一些问题。对于为了追求效率而单独由队友完成的板块,我始终存在理解程度不够、测试不够全面等问题,没有那种江山尽在掌中的踏实感。
结合我的自身感受,我觉得结对的优点就在于一步一个脚印,走的很扎实,每一步都有双重保险;缺点就是效率较低,存在一定的码力浪费现象,而且领航员的精神集中力不好保证(我有时候会偷偷走神。在高压快节奏的作业周期内,要想从头结对到尾着实不易,这也让我更深刻感受到了结对的优越性和缺陷。
评价队友(Z同学著)
Bread
我的队友具有很强的个人能力和广阔的知识面,是他找到各种文件系统源码资料,并提出按照linux的一切皆文件的理念来进行设计,为我们后续的架构打下了基础,lcynb!
我的队友对Java语言的特点了解颇多,对OO思想的认识也很深入,为后续的性能优化和压测工作做出建设性贡献,lcynb!
我的队友非常细心可靠,对自己和团队有很高的要求和期望,他能在飞机上继续看指导书debug,也可以激发我原本已然消退的斗志,lcynb!
Meat
我的队友若是能在单元测试的强度上有所提高会更好。有时单元测试虽然覆盖率达到了,但是对于多种情况的覆盖率还略有欠缺,所以偶尔会有遗漏bug的情况出现。
我的队友若是能更积极地和课程组互动会更好,比如可以尝试自己发个issue来直接表达以下自己的看法之类的。
Bread
我和我的队友具有共同的目标,就是把本次作业做到极致,最终在我们的愉快配合下,取得了不错的成果,建立了深厚的革命友谊,感谢我的队友这俩周的辛苦付出,送上一瓶霸王以致敬意。
最后,当然要po一张队友的帅照,我的队友就是最帅的lcy:
评价队友(L同学著)
Bread
我的队友具有很强的代码编写能力,写起代码来手速超快,一节课的时间过去,就能pull到修改了150+行的代码,一下午的时间,基本上大半的功能实现就能够搞定,不得不让人大喊:zyhnb!
我的队友是一个有责任有担当的好伙伴,差一个功能没实现,她冲在了前面,测试还需要完善,她又冲到了前面,前方有一波bug来袭,她还是冲在前面,不由得让人怀疑,永动机竟是我队友!?
我的队友是一个优秀的bug排雷大师,在我看来已经臻于完美的代码,她依旧能够构造出奇妙的测试数据,找到那些隐藏的可恶臭虫,zyh yyds!
Meat
我的队友啊,有的时候看着你向前不断冲刺的步伐,真的让我充满干劲,我也变得利利索索,对任务一点也不敢拖沓,但这样有时候还是赶不上你的进度,如果你能够停下来多等等你的队友,一起齐头并进,那就更好啦。
Bread
但是,有一个如此充满干劲的队友,真的让人也热血起来了,大家一起让这个小项目变的完美、变得精妙起来,再苦再累也是开心快乐的,同样,要感谢我的队友在这几次作业中的疯狂输出,在ddl的那一天,为了度假的我,一人救火,力挽狂澜,现在,该去好好放风了ψ(`∇´)ψ
工具
作业相关
- gitlab:提交作业,版本控制
- gitlab-ci:CI/CD
- github:查找资料,学习架构
- issues:提出问题,和助教交流
- idea:代码编辑器
- ubuntu18.04:查看ubuntu处理行为
- JProfiler:对作业性能进行分析判断
- JUnit4:建立测试单元
协作相关
- 微信:各种交流,包括但不限于工作、吐槽等
- typora:笔记、博客共享
- 腾讯会议:远程结对编程
- 共享空间咖啡杯:友情出演各种类、对象,帮助讨论架构
感悟和体会(Z同学著)
个人能力相关,我觉得本次结对作业唤醒了我尘封一年的OO技能,并逼迫我把他们发挥到极致,甚至有所提高,新学了一些当时没有接触过的java特性。
结对协作相关,我熟练地掌握了git多人协作的正确姿势,跟队友的沟通也越来越流畅,更会表达自己的想法了 。
工程相关,我深刻的体会到了稳定的用户需求对于项目开发的顺利进展的重要影响。需求的不断变动会使工程的开发周期被拉长,过程更加煎熬,就像一个无底洞,开发者永远不知道是否会有新的需求出现,也摸不清当前的实现是否正确。同时,我也再次深刻体会到了,好的架构应当适应任何需求,需求的变动是牵动大局直接导致重构,还是细节微调即可解决,很大程度取决于项目的架构底子打的好不好(图为针对某一要求修改,我与其他组同学的交流)。
最想吐槽的和最需要改进的已经蕴含在前文中,其实问题还是挺明显的,出题出的太赶,没有时间好好验题,加之题目主题本就细节零碎,造成了大家一些不太理想的体验。但助教对于issue区的每一个提问都给予了回应和修改,能感受到课程组的辛苦和用心。
感悟和体会(L同学著)
结对编程,一种从未体验过的新颖的合作方式,对我们而言,这段时间的最大的困难之一,正是在于合作,两个人一个为驾驶员,一个为领航员,那么我们如何能够在保证任务能够及时完成的情况下,既要让不同职责下的两人维持任务量的公平,也要让双方的合作尽量趋于结对编程的形式呢,这对于未曾合作过的两人而言,有着巨大的考验,你需要适应他人的编程习惯,需要合理的调配任务的分配,需要认真履行自己的责任。
很幸运,结对结到了一个认真负责的好队友,从前期的有些生疏,在任务的不断进行中越来越能找到协作的感觉了,合作最重要的就是交流和沟通,好的交流方式,真的让合作编程变得不那么让人头疼和无奈,在我与Z同学关于实现中一些细节存在分歧时,我们更多的在采用说服(persuade)的方式影响对方,如果你更有道理,那么就按你说的来实现,不会那么不近人情,也不会因为过于考虑对方的感受而将时间都消耗在沟通之上。同时,在合作中,大家在大体上都是平等的,但必然有一个人会更像一个leader,我认为,这种形式是合理的,且是有助于更好的进行协作,更好的向前推进任务进度的。
除了结对编程,顺带利用了软工课中学习到的需求分析、架构设计、具体实现的项目流程来进行开发,深有体会,将更多的时间利用在分析项目需求和设计功能框架中,对后期代码的编写是有着巨大的好处的,其实在OO中已经渐渐有这种感觉,会为了一个完美的微分结构、一个精美的电梯调度模式,花很长很长时间去思考模块与模块间的联系、层次。面向对象的继承、多态、泛型,说白了就是寻找对象自身内部或其与外部世界的抽象关系,抽象,才是程序员最锋利的武器。