结对编程总结

项目 内容
本作业所属课程 2020春季软件工程(罗杰 任健)
本作业要求 采用结对编程的形式完成一个简单的文件管理系统

结对项目实践反思

(1)针对前面两个阶段中出现的问题,分析问题的特征、产生的根源和对质量的影响程度;

前两个阶段总共有4个强测测试点未通过,一共出现了3个bug:

  1. 未进行路径的总长度的判断,第一次指导书中要求路径最大长度不超过4096个字符;

    路径规范 路径由若干目录名、若干分隔符 /,以及可选的文件名一起构成,最长包括4096个字符

  2. fwrite、fappend指令未考虑创建文件时创建失败的情况;
  3. usermod <groupname> <username>指令中,<groupname><username>合法性的判断顺序错误;
  • 对于错误一,我认为这是一个对用户影响较小的bug。该约束是限制用户的行为,使得系统设计可以更为方便,但是开发系统时,并不需要这样的便利,所以忽略了这个限制。
  • 对于错误二,这是一个比较轻的错误,发生错误未报错(但是并没有造成系统被破坏),fwrite、fappend的<filepath>的文件不存在时需要创建,但是创建失败时未进行报错。发生这样的错误,其实蛮“机缘巧合”的,原因就是这两个函数是最后实现的,那个时候我们都比较累了,匆匆写了这些就结束了,写单元测试的时候也是,之前的单元测试都覆盖了各种情况,直到这两个函数的单元测试,因为是最后的两个,有点累又泄了紧绷的精神,所以就只覆盖了正确的情况,没有覆盖错误的情况,导致本不该发生的错误、本应该能够检查出来的错误最后没有被解决。
  • 对于错误三,也是比较轻的错误,发生多个错误时,报错的优先级不正确。这个错误发生的原因是这次作业在lnln -scpmv部分花费了大量精力,没有精力照顾User部分,所以遗漏了这种情况。

总的来说,这些错误都发生在不那么重点、困难的功能中,在不常见的输入下发生与需求文档不一致的行为。这些错误本应该可以通过单元测试来找到,但是单元测试却没有发现这些错误,我们单元测试没有少写,但是仍旧遗漏了情况,反思后提出以下解决方案:

  1. 编写单元测试应当按照需求文档/设计文档一个行为一个行为地写,而不要照着已经完成的代码一个函数一个函数/一个分支一个分支来写,虽然我们只能查看代码的覆盖率,但是实际上追求的应该是文档中的要求的覆盖率;
  2. 单元测试不要测试每一个行为,单元测试中的代码有三部分,第一部分是搭建模拟的环境,第二部是执行被测试目标,第三步是判断被测试目标的执行结果是否符合期望,其中第一部分搭建模拟环境时,可以默认其用到的模块行为正确,那些模块的测试应当在针对那些模块的单元测试中来进行,而不要再在这里的第三部分进行验证。倘若在使用模块时也进行验证,则与该模块的单元测试重复了,是无用功,无用功一方面可能会分散当前测试的注意力,无法将重点集中在待测模块上,一方面会消耗额外的精力,导致后续单元测试效果不佳;
  3. 应当拿出对待目标代码的重视程度来重视单元测试,目标代码从内部实现需求,单元测试从外部验证需求的实现,这是从两个不同的角度同样针对需求的流程,不应当厚此薄彼。

(2)总结结对项目中的需求分析实践体会,并分析哪些bug是因为需求分析不足而带来的;

需求分析印象最深的还是第二次作业的lnln -scpmv部分,这些指令本身的行为就具有很多的情况,并且还有很多前文提到的“不在赘述”的条件,这使得这些指令本身就有很高的复杂度。此外,这些指令带来了很高的灵活性,而且和已经实现的功能有很多交互,不仅要考虑这四条指令会有怎样的结果,还要考虑软硬连接与之前实现的指令有什么交互。

最开始针对这四条指令分析的时候,十分混乱,做了很多无用功,甚至git回档过几次。后来发现解决了如下这些问题后,问题就顺畅很多了:

  1. 软/硬链接文件在有些时候需要重定向,有些时候不需要重定向,但是所有的文档的描述都只有文件、目录两种类型的操作目标的行为,那么软/硬链接文件在不需要重定向时应当如何表现?——和文件一样;
  2. cpmv指令有时会发生覆盖,覆盖是用新的文件控制块替代旧的,还是将旧的文件控制块的内容替换为新的?——根据文档中“视为对<xxx>修改一次”中的<xxx>来决定,保留<xxx>
  3. 需求文档中将异常情况和正常情况揉在一起写的,看起来比较混乱,我们在需求分析时进行了再一次标注,帮助我们更好理解需求:

通过明确定义、理清条理,我们顺利攻克了需求分析中的难点。

我们发生的几个bug,并不是需求分析的难点出现问题,而是需求分析时的覆盖出现了问题。在实现的时候遗漏了某些需求,结对编程的时候,两个人的讨论确实能够帮助彼此聚焦重点、理解难点,但是我们却没能很好地互相补充遗漏。我认为造成这种情况的原因可能是我们二人都将全部过程留到了结对编程来进行,两个人在碰头的时候都发现对方没怎么研究过指导书,于是两个人一起看指导书,讨论的时候一般都是针对难点来讨论,于是就容易遗漏一些不起眼的需求。改进措施就是结对编程之前先独立自己看一遍需求文档。

(3)总结结对中的架构设计实践体会

描述通过改进设计来提高程序的性能改进的思路和方法,并分析哪些bug是因为架构设计不足(特别的,需求变化)而触发的bug;

文件系统中又两类操作,一类是查询操作,不进行任何更改,一类是更改操作,要对文件系统中的模型进行修改。我们在第一次作业将这两部分进行了隔离,每一个文件系统的模型,如文件File、目录Dir,都继承自一个抽象基类FileControlBlock,其有一个内部类ModifyHandler,通过对FileControlBlock调用modify()方法可以获得一个ModifyHandlerFileControlBlock本身只保留查询方法,而所有的修改方法都在ModifyHandler内实现。结构如下:

使用效果如下:

File cpy = file.copy(newName, dir);    // copy()方法不改变原对象,只是创建拷贝,所以直接调用
dir.modify(currentTime).paste(cpy);    // paste()方法为目录添加了新项,修改了原对象,需要调用modify()

这样带来两个好处:

  1. 每次调用可能修改模型的方法,都必须显示调用modify()方法,避免了错误地在不应当修改模型的时候修改了模型;
  2. 所有修改模型的方法拥有统一的前置方法,可以方便地在modify()方法内添加内容,来执行修改模型时的检查,如权限检查(虽然最后没用到);

还有一个好处,就是强制将修改方法集中到一起。在第二次作业中,加入了“每个操作都是原子操作”的新需求,所以我们加入了简单的事务管理,需要为每一个修改模型的方法添加事务相关的操作,而由于我们通过对查询/修改方法的隔离,使得修改模型的方法聚合在一起,使得添加事务相关操作减少了错误的可能性。

// 这是DirModifyHandler类的一个方法
public boolean paste(FileControlBlock fileControlBlock) {
   String name = fileControlBlock.getName();
   assert fileControlBlock.getParent() == Dir.this;
   if (subControlBlocks.containsKey(name)) {
         return false;
   }
   setModifyTime();
   // 这里需要注册一个回滚时的回调函数,当事务失败发生回滚时,将被FILO调用。
   addRollBack(() -> subControlBlocks.remove(name));
   subControlBlocks.put(name, fileControlBlock);
   return true;
}

在这种设计下,每次修改模型的方法的暴露,都是十分谨慎的,这导致了添加新需求时,经常需要添加新的接口,我认为我们的方法设计在封装方面有些过于保守,比如对于目录模型Dir,我们为其设计了makeDir()方法来添加新的子目录,设计了touchFile()方法为其添加新的文件,实施上可能我们只需要一个addFileControlBlock()方法来添加新的文件控制块即可,没必要一个一个设计方法。

过窄的方法暴露虽然保证了正确性,但是却徒增复杂性,当代码量提高时,代码就变得很长而难以阅读,虽然我们没有因此产生bug,但是修改时仍旧比较繁琐痛苦。

(4)总结结对过程中的进度、质量和沟通管理实践体会,并分析哪些哪些bug是因为两个人的理解不一致而导致;

第二次结对编程作业中,我们的进度遇到了很大的问题。有一些需求并没有分析明白,在讨论区提问无法获得及时回答,“单打独斗”的时候,我们可以先暂停,等问题回答后,找个时间继续即可;但是结对编程的时间并不好协调,需要两个人都有空闲时间才可以,经不起这样的暂停-继续,我们只能提问后尝试着按照我们认为更合理的方式来实现,等问题解答后再修改,但事实上我们做了很多无用功,最后花费了很多时间。

庆幸的是我们的沟通交流很顺利,虽然经常需要停下来写写画画才能完成交流,但是总的来说还算顺利,并没有因此导致产生bug。

(5)提出建议:根据三个阶段的结对项目的实践经验,对如何更好的实施和管理结对项目提出自己的建议。

  1. 开始结对编程前做一些功课,先独立地仔细看一遍需求文档,先独立思考一遍实现方式,先独立对可能遗忘的部分进行标注,在双方都有提前准备的时候,结对编程会更有效率,也会减少遗漏;
  2. 重视结对进行单元测试,虽然表面上是追求代码的覆盖率,但是实际应当追求需求文档/设计文档的覆盖率;
  3. 准备好纸笔/平板等方便写写画画的工具,很多时候写下来的东西比嘴里说的东西更容易让对方理解;
  4. 时间不宜过长,两个精力旺盛的人结对编程能够更进一步集中精力,两个精神涣散的人结对编程只会让错误翻倍;

CI体验感想

通过这次结对编程,你对CI的使用体验如何?你对这一工具有何认识?

CI工具的作用其实和shell脚本很类似,通过命令行接口来调用其他工具,进而可以实现很多复杂的功能。为了实现在CI中进行单元测试,我们上传了jar包,然后通过maven命令进行安装;后来看到其他同学分享到可以直接通过url下载官方包,然后通过apt安装zip工具对下载的官方包进行解压,这样可以避免在仓库中上传jar包这样的妥协操作。同学的分享提醒了我们,不要局限于简单的操作,CI中完全可以进行复杂的操作,CI中缺少的工具完全可以当场安装。


结对编程感想 (对队友/自身的评价 、过程中遇到的困难与收获、锻炼了哪些能力)

描述你们结对的方法、结对过程中遇到的困难与收获,结合自己的结对经历,说明结对编程的优点和缺点,分享可以推广的结对妙招。

1045

我们结对的过程还算顺利,因为我这学期课比较少,所以我队友有空的时间我基本都有空,所以时间比较容易协调开,所以我们绝大部分的代码都是结对一起写的,一些单元测试和一些小修小改是分开进行的,然后结对的时候交流更改了什么。

结对编程确实大大提高了攻坚的效率,而且代码的质量确实更高了,单元测试很少测出错误。但是在我们的实践中并没有提高对需求的覆盖,仍旧时不时遗忘一些不重要的需求。

我们结对遇到的最大困难就是环境,因为我们不在同一个宿舍楼,疫情期间不允许串宿舍楼,教室里有其他同学,图书馆也不适合需要频繁交流的结对编程,所以不得不在食堂进行。食堂没法跟电脑充电,电脑的电量大大限制了我们结对编程的时间。而且食堂的环境比较嘈杂,空气里飘荡着美食的香气,左边是一对情侣抱在一起卿卿我我,对面一大桌子人在打桌游,右边是一帮大叔在吹牛,虽然进入状态之后确实可以过滤掉这些干扰,但是终究不是一个很好的环境。如果能以教学班为单位申请一个空教室可能会好很多。

3298

队友在结对的时候相当给力,我生怕他把活都干了,这也是这么多年组队完成作业最舒服的一次了.我和他本来是不认识的,为了不使氛围尴尬,我们尽量的多找话题,很快的就熟悉了,写起代码也更顺手了.

在结对的过程中我可能更多的是帮助队友完成相对来说简单的部分,比较困难的部分则是由两人共同商讨完成(他写,我看).结对编程总的来说效率是高的,每次食堂结对都奔着一个小目标去完成.第二次作业第一天先写了用户系统部分,第二天又完成了mvcpln等剩余的指令.第三天没过弱测开始写单元测试debug.第四天终于完成.总的来说效率还是很高的.结对编程能够使得两人的注意力都比较集中,一个人掉队,看到另一个人还在写,就能再次振奋.

结对小妙招:指导书三连
- 不是我不行
- 是指导书不行
- 都怪指导书.

评价你的队友,使用汉堡点评法评价你的结对伙伴,务必给TA 提改进意见。

1045

因为班里没多少我认识的人,认识的人都一起组了大作业的队伍,所以无法再进行结对编程,所以组队的时候都已经做好了可能要自己完成绝大部分代码的思想准备了,但是结对编程之后发现我的队友十分给力。事实上大部分代码都是他输入的,我做的更多的是后续的调整、优化和维护工作,尤其在lnln -scpmv部分,我自己是彻底乱掉了,但是他非常有条理地和我一起把逻辑整理顺了。

不过我在做“领航员”的时候发现,对于多个函数中相同的部分,我的队友更倾向于进行进行复制粘贴,而不是将其抽象出来作为一个函数,这样导致某一部分需要修改的时候,需要将多个地方都进行修改。后来是我来进行的抽象工作。

我们两个都有一个共同的薄弱点——测试。虽然我写单元测试很熟练,但是时候反思的时候发现很多单元测试中的验证工作都是多余的;而我的队友最开始的时候连单元测试写起来都有些生疏。我们虽然搭建了整体的黑箱测试,但是最后我们基本只用这个框架测试了文档中的样例和开放的数据点。

3298

队友相当给力,代码能力很强,对于java的使用,我只停留在表层,而队友对于内部类,接口,枚举等的使用都相当熟练,精妙的modify使得所有的方法必须先经过modify才能进行修改,使我深刻体会了链式编程的妙处。而且对于mkdir -p很多同学可能是先跑一遍进行检验,而我的队友使用回调机制手写回滚,从来没见过有人手写事件机制的。而且他总能想到一些奇奇怪怪的东西,比如通过间接方式创造软链接循环链。使得我不断的使用结对小妙招调整情绪。

写代码随叫随到,到点了就回去睡觉。有问题敢找助教,带我飞不说不要。这么好的队友哪里找。

描述在本次结对编程的过程中,你们使用了哪些软件工具,是如何应用于实践的。

IDE使用的IDEA,项目管理采用Maven,并通过Maven插件的JaCoCo工具进行单元测试的覆盖率检查,整体的黑箱测试使用python,通过命令行接口和重定向来执行,然后进行比较。

maven是我进行结对项目前现学的,maven提供了强大的项目管理能力,并且maven丰富的插件可以实现很多实用的功能,我们这里只利用maven将官方包的依赖导入,然后利用maven进行单元测试并导出覆盖率信息。

JaCoCo通过字节码的指令注入的方式来进行覆盖率的检查,但是存在一些影响覆盖率的小问题,导致覆盖率较高时难以继续提高,或者只能编写无意义的单元测试来继续提高:

  1. 对于有基类或者实现了接口的类,如果没有将该类的对象向上转型,那么会认为没有覆盖某一部分代码;
  2. 对于Java内置的assert,如果从未发生过断言错误,视作缺少覆盖;但事实上有些assert是为了将来修改代码时能够快速暴露错误而书写的,或者是为了帮助IDE进行分析而书写的:
abstract class Base { 
   public abstract String foo();
}

class Sub1 extends Base {
   @Override
   public String foo() {
      return "Sub1.foo()";
   }
}

class Sub2 extends Base {
   @Override
   public String foo() {
      return "Sub2.foo()";
   }
}

public class Main {
   public static void foo(Base obj) {
      if (obj instanceof Sub1) {
         produceSub1((Sub1) obj);
         return;
      }
      if (obj instanceof Sub2) {
         produceSub2((Sub2) obj);
         return;
      }
      assert obj == null;
      produceNull();
   }
}

一般而言,目前的代码中不会出现assert错误的情况,这个assert只是为了防止以后增加Sub3时忘记增加分支而写下的,但为了提高测试覆盖率,不得不写如下代码:

try {
    Main.foo(new Base() {
    @Override
    public String foo() {
        return "Nameless";
    }
    });
    Assert.fail();
} catch (AssertionError ignore) { }

描述通过本次结对编程的感悟和体会,对本次作业的有哪些想吐槽的,觉得本次结对作业内容可以在哪些方面做出改进?

1045

整个结对编程作业,我感觉最不舒服的就是编程环境,习惯了外接键鼠、多个屏幕的开发环境,让我到食堂里只有一个笔记本来进行开发,感觉手脚施展不开。另一方面,虽然结对编程在攻克难点时,有一个人时刻一起讨论是十分有帮助的,但是就算不进行结对编程,我们也可以很方便地和同学进行交流沟通,感觉体现不出太多优势。不过,这两点在工程实践上,情况就完全不同了,在企业的结对编程实践中,肯定会提供合适的工作环境,另一方面,工程时间上,如果不进行结对编程,是很难找到人沟通的,因为大家负责的是不同的工作,而不像在学校完成作业那样,大家完成的是同一份工作。

这次作业最痛苦的还是lnln -scpmv部分,我觉得可以删去软/硬链接,因为加了这两个东西之后,整个系统的复杂度直接翻倍,覆盖的情况也变得复杂。

还有一个痛苦之源就是“不在赘述”,在写后面的代码的时候,还老得往前翻文档,使得注意力无法集中,建议在相关部分放一个链接,或者干脆复制一遍。

3298

本次作业最想吐槽的还是占用的时间有点多。其实对于结对编程我觉得还是不要搞成oo那套比较好,一个是oo已经体验过了,再来一遍完全没有必要,另一个是需求不是很轻易就能想出来的,指导书写的这么详细,少了很多需要设计的地方。就单纯的软链接硬链接我觉得其实怎么实现都是有道理的,这个东西基于内存和基于磁盘是不一样的,所以没有必要按照linux来,可以搞成自定义实现。或者结对项目为团队项目做一定的技术积累也是不错的选项。

posted @ 2021-04-07 14:43  SnowPhoenix  阅读(212)  评论(2编辑  收藏  举报