刚刚读完《修改代码的艺术》,我觉得译名没有原名贴切:Work effectively with legacy code。如题目所云,这本书系统而全面地讲述了work with legacy code的思想和诸多具体方法,对于我们每个人维护、重构,修改遗留代码或者基于遗留代码新增、修改功能很有帮助。 在我读来,work with legacy code只有两种方法:写单元测试,解依赖。写单元测试可以验证代码修改的正确性,保护新增或更改的代码;解依赖可以去除遗留代码的不合理依赖,使单元测试的编写成为可能——归根结底还是写单元测试。这样一来本书的主旨浓缩到了三个英文字母:TDD。至于用到的种种方法手段,无不是为了达到让单元测试覆盖所有改动这一目的。 我的日常工作其实很大程度是working with legacy code,上古时代,宇宙洪荒,第一代程序员们为了从无到有做出一个系统,筚路蓝缕披星戴月,终于完成了,收获了英雄般的待遇。但是,斗转星移,隔代的程序员们接手了代码,听了新需求,却发现加功能、改特性需要小心翼翼,如履薄冰:文档没有,注释没有或者看不懂,代码纷繁复杂,难以理解而不敢下手——当然终究还是要下手的,好吧,just edit then pray,希望上帝保佑,没有bug——但bug还是大摇大摆地来访。于是修修补补,终于没什么问题了,交工了,松了一口气。转眼又是一代程序员,以上轮回继续…… 这样的状况作为开发人员当然是不喜欢的,于是重构成了改善遗留代码结构的必然选择。下面讲讲我第一次对遗留代码动大手术的故事。 我进入公司并接手一个业务模块时,该模块有一个讨厌的操作页面:这个页面的功能是显示一张单据,并对这张单据做一些编辑操作,单据本身有一个header体和二级的detail体,由于不同用户希望看到的展现不同,单据明细的展现有两种方式:a.二级grid;b.上下分离的两个grid,对应系统里的两个文件。由于只是展现不同,很多功能以一式两份又略有不同的方式写在两个文件中。这样,每次需求变更,在文件一里做的改动需要copy到文件二的相应方法中去,这就造成了不少重复代码,况且由于我总是记不得要到另一个文件里修改,我被QA报过很多bug。此外,由于允许用户在这两种模式之间切换,我还要考虑新做的功能如何支持切换,这同样容易产生bug。 在忍受了一段时间这样的工作之后,我下决心改变这一切,首先开刀的是那些高度重复的代码:其实很多代码只是循环遍历所有明细的方法不同,同样的数据,一个遍历第二级grid的所有行,一个遍历下面一个grid的所有行,那么我只要对“循环遍历”方法做接口提取,对循环体内完全一致的操作代码做方法提取,重复代码就被合并成一份了。这一改动立竿见影,重复代码被删除了两千行,维护代价和QA的测试代价也折半了。其次是那些在两种展现下都会用到,但确实实现没有重合部分的操作,我对这些操作也做接口提取,这样的改动对旧功能没有影响,但改善了代码可读性,并且和上一条改动一起,保证了今后新特性的增加都在公共代码部分:如果操作一致最好,在公共部分写实现代码;如果在两种展现下操作不同,在公共部分写接口,各自实现。在这两步重构中我也感受到——设计是自己浮现出来的。最后,通过和BA讨论用户实际使用习惯,讨价还价,成功地砍掉了“在两种模式之间切换”的功能。自从这次重构到现在,在这个页面添加或修改相关功能都没有什么大问题。 下面用简化的UML说明一下这次重构,原来的大致结构: 新的结构:

读完《修改代码的艺术》,反思我当时做的这次重构,还是不够科学,有改进余地的。最大的问题,是我做的是草稿式重构:没有单元测试的保护,直接上去改,改过之后也没有单元测试的验证。当时不写测试自然是有一些借口的:遗留代码没有写单元测试的先例;客户端代码只有一些静态基础类写有单元测试;页面操作严重依赖界面的输入和展现,难以解依赖。但这种重构难免带来一些bug,于是也承受了来自QA不小的压力。现在看来,如果当时用TDD的思想,先写单元测试再做重构,后人改动起来会更得心应手,单元测试本身也是不错的文档(现在这个模块已经移交别人了),而这就需要解除大量对界面操作的依赖,写一些伪对象来替换依赖部分。另外一点:对单一职责原则应用得还不够,现在看来,有些方法虽然公用,但仍然是大段的项目列表式方法,可以进一步细分。 最近有一位同事刚刚做完一个Refactor,这个重构的效果很显著:把某一份月度报表的产生时间从9个小时左右减少到了15分钟以内。他给我们分享了他的重构过程:写单元测试验证遗留代码、重构、用单元测试验证重构后代码、修改bug、完成并交付QA测试。这次重构减少了存储过程的代码量,更加结构化,消除了大量难以在编译期发现问题的拼SQL代码,将主流程置于单元测试保护之下,新写了许多注释,并发现了旧版本的两个bug。

Work with legacy code确实不是件容易的事,重构也不是一件轻松的事,为了把重构做得更可靠,千方百计解依赖、TDD是值得的,这会帮助我们改善既有设计,让更好的设计通过重构,自己浮现出来。当然,如果抱有的是”Après moi, le déluge”的想法,那么,洪水早晚会来。