TDD(测试驱动开发)程序员开源工具技巧

Code Kata 之读自己过去的代码

2017-01-24  本文已影响263人  武可

临近过年的日子,是个回顾的好机会。
正好前一段看到这篇翻译介绍 读自己以前代码的Kata,拿来练习练习。
其实Thomas在提出Kata概念的时候,涵盖的范围是远大于编码层面的。只是由于其它类型的Kata的判定标准比较“虚”,所以不常拿来练习。
这个Kata的关注点在于刻意练习阅读代码,而且是自己写的代码,来体会代码中各种细节的长期影响。通过主动的回顾来形成良好习惯,并增强review能力。
我自诩是个“代码考古学家”,常常修一个bug把80%的时间用在了翻历史记录,来琢磨它为啥能写成这个样子。同时也是个热衷于review别人代码的人,不惮于对别人的代码说三道四,想必很惹人厌了。
这次算是找个机会把自己的代码摆出来,自食其果吧。

程序员读到自己的代码

代码的选择

Kata的要求是写了超过一年,记忆有些模糊。代码量在500至1000行。
我选择了一个小项目,小到基本由我一个人开发。而且除了对口的产品经理,也再没其他人关心了。所以当初可以随心所欲的搞。
然而即便这么小的项目,做完以后客户却要求与最初商定的需求文档几乎完全不一样的功能。最终,所有的业务对象推倒重新做了一遍。所以也可以算是个典型的软件项目吧。
基本上是用outside-in 式TDD开发,虽然当时还不是特别自觉的采用这种流程。

从中选择了一个类来做Review。选择时参考了《修改代码的艺术》作者关于技术债分析的建议:一个类修改越频繁,说明它越可能将来被改动,越可能存在技术债。
我统计了所有的提交记录,选出一个提交次数最多的类,名叫WorkflowManager。和标准工作流没什么关系,是一个实现简单工作步骤流转的类。

第一遍读:假定这是大牛写的代码,记录惊艳的地方

第二遍读:假定这是个二货写的代码,记录烂的地方

其实在看第一遍的时候就开始忍不住想这些地方应该写的更好了。我果然是不善于发现优点,专爱挑毛病。

上面这些严格来说,应该算是可以改进的地方,还称不上是二。不过下面这段就……

public List<Item> setSelectedItems(List<Item> items, WorkSet workSet) {
    List<Item> remainItems = new ArrayList<Item>(items);
    nextInputItem:
    for (Item inputItem : items) {
        for (Item item : workSet.items) {
            if (item.key.equals(inputItem.key)) {
                item.selected = inputItem.selected;
                remainItems.remove(inputItem);
                continue nextInputItem;
            }
        }
        if (!inputItem.selected) {
            remainItems.remove(inputItem);
        }
    }
    return remainItems;
}

两层for嵌套,不但嵌套for,里面还套if,不但if,还continue,不但continue,还从内层continue到外层……
感觉差不多把今天我认为不好的风格全拿出来演练了一遍。瞬间有点“这是我写出来的么?”的感觉。不过稍稍回忆一下,就想起来我当时还去专门查了查continue到外层的写法。

你能看出来这是在干嘛么?
其实是数据库里的workset保存有若干个项目,每个项目有选中状态。每次页面提交时候会返回这些项目通过页面操作后的选中状态。需要更新到数据库中。
而且页面还有可能增加原来数据库里没有项目,这些项目要在数据库新建项目记录,并且和workset关联起来。
这个双层循环做了两件事

  1. 对于已经有的项目,按照页面传入的列表更新选中状态
  2. 对于没有的项目,放在另一个列表中作为返回值,方便给下一步做创建项目等操作。

如果让你来重构它,你会怎么做呢?

第三遍读:假定这段代码有个严重Bug,今天找不出来你就完了。记录找到的Bug

虽然没有专人QC,也完全没有测试阶段,但是因为由着我性子写了一堆单元测试和集成测试。每次本地保存代码的时候都会执行一遍。因而我对项目的质量是相当自信的。
最直观的感受是在客户验收演示会上。
当初开发完成后扔在一边,拖了半年多客户才验收。已经记不清具体代码细节了。客户在操作演示过程中,走到一步走不下去了。当时我心情毫无波动,一点也没打开源代码检查的冲动。
果然,稍稍回忆了一下就发现不是bug,而是操作失误。

所以一开始我认为是没法完成这一轮的目标的。准备能找到个错误处理或者很特殊情况下的缺陷就交差吧。
而且读代码找bug真的好难,要完完全全读懂每一个细节在做什么。这时候真巴不得当初代码写的好读一点就好了。
没想到,真找到了一个

public void statedChanged(WorkSetState originalState, WorkSet workSet) {
    WorkPackage workPackage = workSet.workPackage;
    if (isApproved(workSet)) {
        /* ... */
    } else {
        for (WorkSet sibling : workPackage.workSets) {
            if (isPending(sibling)) break;
            workPackage.status = WorkPackageStatus.INPROGRESS;
        }
    }
    workPackageRepository.save(workPackage);
}

这段代码的本意,是一次审批计划(WorkPackage)中会分成多个分组(WorkSet),有任何一个分组还未有开始审批,那么整个计划都处于未开始状态。
然而真正实现的是,只要第一个分组是进行中,整个计划就会变为进行中。如果第一个未开始,则整个计划没有开始。

相当出乎意料竟然有这么明显的一个错。我反思了一下在重重测试包围之下还漏掉这个Bug的原因。

  1. 有集成测试,但是没有验收测试。没有一个地方把业务流程的故事集中讲一遍。因而在较大范围的测试中漏掉了这个点。
  2. 单元测试中,仅仅构造了一个计划有两个分组的情况,没覆盖到所有分支。
    往深一层想,这个方法需要判定的情况比较多。所以针对它的测试用例本来就很多,显得在这个测试类中占了很大的比例。也就是在第二遍读时发现的好像是内嵌了一个小的Test类的情况。写的时候心里就有种喧宾夺主的感觉,不由得怀疑是不是对这个方法过度测试了。进而没有仔细考虑来设计用例。
    其实内心产生不协调的真正的原因是实现类违反了单一职责,而不是测试重心失衡。

感受

这次练习的收获收获颇大,虽然理论上自己有着对代码质量,可维护性,以及测试用例设计等方面的各种观点。但是这次时间胶囊式的体验,还是带来了完全不同的心理感受和思索。
特别是第三轮找Bug的挑战。最后找到的时候是我记忆中找到Bug最开心的一次。

上一篇下一篇

猜你喜欢

热点阅读