对于大多数开发者来说,在遗留代码基础上开发是日常工作的一部分,毕竟从头开始创建全新系统的机会不是很多。架构师、《漫谈设计模式》作者刘济华结合自身的实际经历分享了如何在遗留代码基础上开发的经验。
刘济华首先指出,大多数系统是构建在之前的遗留系统之上的,在开始,很难把遗留系统直接丢弃,特别是一些业务逻辑非常复杂的金融电信系统。 这些代码往往有如下特点:
- 旧的编程语言开发低效。
- 代码冗繁,质量差。
- 添加新的功能和修改错误(Bugs)的周期时间长而痛苦。
- 这些代码没有单元测试,甚至没有功能测试、冒烟测试、回归测试。
- 无法交接这些代码,因为写代码的这些人很多已经离职。
- 维护这些代码代价高,大家心惊肉跳,特别是系统遇见特殊情况(节假日,高峰访问期等),无法安宁。
但是这些代码能够完成当时的功能,直接抛弃这些代码,重新开发将会耗费很大的资源,且不一定成功,如果新的需求不断变化,往往没有时间来重新开发这些代码。而这些遗留代码的功能没有完善的文档说明,甚至没有。 在此现状下,如何改善这些代码,将是考验程序员和一个团队的智慧。
接着,刘济华分析了对于遗留代码取舍的亲身经历。
1. 具有性能瓶颈的遗留系统——曾经遇到过一个应用服务,是C语言写的,周围其他系统都是采用Java开发,与此系统的交互都是采用非标准的协议完成,而且此系统处于比较核心的位置,但是维护非常复杂,不稳定,访问压力大是经常宕机,无法水平扩展,只能提高硬件设备水平等方式考虑。
在原有协议基础上开发,使其具有水平扩展能力,代价和开发一套标准协议的实现没有任何区别,往往会带了协议不够完善所产生的问题。于是,我们为其开发新的标准协议,WS,MQ等等,然后在标准协议上负载均衡实施水平扩展,非常方便。
随着后来的发展,此遗留代码也慢慢被新开发系统取代,期间经历了新系统和旧系统同时存在,此时新系统未完全具有旧系统的全部功能,这部分功能还是使用旧系统完成,只不过在原有均衡负载层多了层查找和分配的,后来到旧系统完全取代。此过渡还算平顺。
2. 功能性改造型系统——大多数就是这种系统,保留的话,代码极其复杂,维护麻烦,丢弃的话,无法一夜之间写出新的系统。曾经接手了一个开发失败的项目,包括代码和文档都未完善,如果重新来过,终究不划算,后来在此系统上进行改造,特别是花了大量时间写单元测试,保证代码测试覆盖率极高,这样一边熟悉代码,一边重构代码,系统的健壮性发生根本改变,前提是有时间。这只是特例。很多时候遇见的系统,同样测试代码很少,在添加新的功能和修复错误(Bugs)时,为这些能够接触到的代码完善测试,新代码必须测试覆盖率必须很高,经过4个月,,此系统代码覆盖率已经达到50%以上。以后的迭代开发越来越快。
那么如何在在遗留代码上编程呢?刘济华觉得首先要找到代码修改点:
遗留系统代码往往测试少,或者没有,导致软件开发者对软件发布没有信心。但是为所有的遗留代码写单元测试,初始代价非常高,在添加一个很小的功能时,并没有时间大动干戈。
如何找切入点呢?刘济华总结了几种情况:
- 修改一处代码即可,这个时候非常简单,修改代码处即为切入点,找到这处修改即可,为此处代码写完善的单元测试代码,特别是对于输入条件和测试条件尽量能够完整测试。
- 修改多处代码,位置分散,并且修改代码如果有多种方案,我们找出最少修改代码的地方,而不是最佳的修改方式,很多时候,此时最佳的代码修改会修改很多代码,导致测试代码无法一下子完善,另外,此时认为的最佳方案随着时间的推移,或许又是糟糕的代码,所以没有必要花费更多的精力在上面,当然也可以选择比较中庸的方式。
在修改时需要一些技巧,其中包括:
①找测试方便、改动较小的方式来修改遗留代码。
②重构在一个类中那些重复的方法,并且保证其健壮性。
③为依赖的具体类提取新的接口,并使用注入依赖技术,使得测试更加容易,不管是使用Mock tool还是自己编写Mock对象,都会非常容易测试。
比如:
1
2
3
4
5
6
7
8
9
|
class Manager{ ... public void kickOff(){ ... DoSomething doSomething = new DoSomething(...); doSomething.doSomething(objects); } ... } |
可改为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
class Manager{ ... DoSomething doSomething; public void setAction(Action doSomething){ this .doSomething = doSomething; } public void kickOff(){ ... doSomething.doSomething(objects); } ... } interface Action { void doSomething(List marks); } |
④尽量使测试的范围缩小在受修改影响的类中,对类中的改动进行全面测试。保证每处修改完全测试,保证测试类减少。
⑤类之间交互的代码重构,如果这些交互仅在修改的代码之中,只要保证修改的代码完全测试即可。而对于那些可能影响此时其他不需要进行修改代码的类,可以先放下,为其创建新的方法,在此次修改和以后修改中,使用和重构新的方法。对于老的方法,等到以后代码覆盖率提高,能够覆盖所有此类交互方法的代码时,重构此方法,这是你会发现,修改很简单,并且如果修改错误,或者不能处理极端的逻辑,也会和容易找出问题所在。
⑥努力汲取业务逻辑知识。
⑦《修改代码的艺术》(Michael Feathers 著)建议找到切入点(Inflection Point),往往我们找的点很多,每一次修改都可能不一样,为此花很多代价找寻,还不如直接进入修改,找出最佳的修改方式避免代码过度重构和修改,减少影响,这才是有有实践价值和有意义的。
如何保证质量呢?刘济华建议:
- 尽可能让一切自动化——单元测试自动化是最基本要求,尽可能让一切测试变得自动化,不管是单元测试,还是功能测试,还是压力测试、冒烟测试、回归测试。发布自动化也是非常重要的。
- 为项目加入冒烟测试和回归测试。逐渐保证代码质量。
- 坚持可控变化、逐渐渗透的原则,保持系统稳步的朝健壮的方向进行。